IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 人工智能 -> ros学习记录5 Client Library CPP -> 正文阅读

[人工智能]ros学习记录5 Client Library CPP

5.Client Library CPP

client library:

提供ros编程的库/接口

例如:建立node、发布消息、调用服务等

提供cpp和python两种语言支持,一般使用cpp

  • roscpp

  • rospy

5.1 roscpp

ros提供的使用cpp和topic,service,param等交互的接口

roscpp位于 /opt/ros/kinetic 之下, 用cpp实现了ROS通信。 在ROS中, cpp的代码是通过catkin这个编译系统( 扩展的CMake) 来进行编译构建的。 所以简单地理解, 你也可以把roscpp就当作为一个cpp的库, 我们创建一个CMake工程, 在其中include了roscpp等ROS的libraries, 这样就可以在工程中使用ROS提供的函数了。通常我们要调用ROS的cpp接口, 首先就需要 #include <ros/ros.h>

roscpp的主要部分包括:

  • ros::init() : 解析传入的ROS参数, 创建node第一步需要用到的函数

  • ros::NodeHandle : 和topic、 service、 param等交互的公共接口

  • ros::master : 包含从master查询信息的函数

  • ros::this_node: 包含查询这个进程(node)的函数

  • ros::service: 包含查询服务的函数

  • ros::param: 包含查询参数服务器的函数, 而不需要用到NodeHandle

  • ros::names: 包含处理ROS图资源名称的函数

  • 具体可见: http://docs.ros.org/api/roscpp/html/index.html

5.1.1 ros::init()

void ros::init()   //解析ros参数,为本node命名

5.1.2 ros::NodeHandle Class (类)

// 创建话题的publisher
ros::Publisher advertise(const string &topic, uint32_t queue_size, bool latch=false);
//第一个参数为发布话题的名称
//第二个是消息队列的最大长度, 如果发布的消息超过这个长度而没有被接收, 那么就的消息就会出队。 通常设为一
个较小的数即可。
//第三个参数是是否锁存。 某些话题并不是会以某个频率发布, 比如/map这个topic, 只有在初次订阅或者地图更新
这两种情况下, /map才会发布消息。 这里就用到了锁存。
//创建话题的subscriber
ros::Subscriber subscribe(const string &topic, uint32_t queue_size, void(*)(M));
//第一个参数是订阅话题的名称
//第二个参数是订阅队列的长度, 如果受到的消息都没来得及处理, 那么新消息入队, 就消息就会出队
//第三个参数是回调函数指针, 指向回调函数来处理接收到的消息
//创建服务的server, 提供服务
ros::ServiceServer advertiseService(const string &service, bool(*srv_func)(Mreq &, Mre
s &));
//第一个参数是service名称
//第二个参数是服务函数的指针, 指向服务函数。 指向的函数应该有两个参数, 分别接受请求和响应。
//创建服务的client
ros::ServiceClient serviceClient(const string &service_name, bool persistent=false);
//第一个函数式service名称
//第二个参数用于设置服务的连接是否持续, 如果为true, client将会保持与远程主机的连接, 这样后续的请求会
快一些。 通常我们设为flase
//查询某个参数的值
bool getParam(const string &key, std::string &s);
bool getParam (const std::string &key, double &d) constbool getParam (const std::string &key, int &i) const//从参数服务器上获取key对应的值, 已重载了多个类型
//给参数赋值
void setParam (const std::string &key, const std::string &s) constvoid setParam (const std::string &key, const char *s) const;
void setParam (const std::string &key, int i) const;
//给key对应的val赋值, 重载了多个类型的val

5.1.3 ros::spin() 和 ros::spinOnce() 区别及详解

1 函数意义

首先要知道,这俩兄弟学名叫ROS消息回调处理函数。它俩通常会出现在ROS的主循环中,程序需要不断调用ros::spin() 或 ros::spinOnce(),两者区别在于前者调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而后者在调用后还可以继续执行之后的程序。

其实消息回调处理函数的原理非常简单。我们都知道,ROS存在消息发布订阅机制,什么?不知道?不知道还不快去:http://wiki.ros.org/ROS/Tutorials (ROS官方基础教程) 瞅瞅。

好,我们继续,如果你的程序写了相关的消息订阅函数,那么程序在执行过程中,除了主程序以外,ROS还会自动在后台按照你规定的格式,接受订阅的消息,但是所接到的消息并不是立刻就被处理,而是必须要等到ros::spin()或ros::spinOnce()执行的时候才被调用,这就是消息回调函数的原理

2 区别

就像上面说的,ros::spin() 在调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而 ros::spinOnce() 后者在调用后还可以继续执行之后的程序。

其实看函数名也能理解个差不多,一个是一直调用;另一个是只调用一次,如果还想再调用,就需要加上循环了

这里一定要记住,ros::spin()函数一般不会出现在循环中,因为程序执行到spin()后就不调用其他语句了,也就是说该循环没有任何意义,还有就是spin()函数后面一定不能有其他语句(return 0 除外),有也是白搭,不会执行的ros::spinOnce()的用法相对来说很灵活,但往往需要考虑调用消息的时机,调用频率,以及消息池的大小,这些都要根据现实情况协调好,不然会造成数据丢包或者延迟的错误

3 常见使用方法

这里需要特别强调一下,如果大兄弟你的程序写了相关的消息订阅函数,那千万千万千万不要忘了在相应位置**加上ros::spin()或者ros::spinOnce()**函数,不然你是永远都得不到另一边发出的数据或消息的,博主血的教训,万望紧记

3.1 ros::spin()

ros::spin()函数用起来比较简单,一般都在主程序的最后,加入该语句就可。例子如下:

发送端:

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
 
int main(int argc, char **argv)
{
    ros::init(argc, argv, "talker");
    ros::NodeHandle n;
    ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
    ros::Rate loop_rate(10);
 
    int count = 0;
    while (ros::ok())
    {
        std_msgs::String msg;
        std::stringstream ss;
        ss << "hello world " << count;
        msg.data = ss.str();
        ROS_INFO("%s", msg.data.c_str());
 
        /**
         * 向 Topic: chatter 发送消息, 发送频率为10Hz(1秒发10次);消息池最大容量1000。
         */
        chatter_pub.publish(msg);
 
        loop_rate.sleep();
        ++count;
    }
    return 0;
}

接收端

#include "ros/ros.h"
#include "std_msgs/String.h"
 
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
    ROS_INFO("I heard: [%s]", msg->data.c_str());
}
 
int main(int argc, char **argv)
{
    ros::init(argc, argv, "listener");
    ros::NodeHandle n;
    ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
 
    /**
     * ros::spin() 将会进入循环, 一直调用回调函数chatterCallback(),每次调用1000个数据。
     * 当用户输入Ctrl+C或者ROS主进程关闭时退出,
     */
    ros::spin();
    return 0;
}

3.2 ros::spinOnce()

对于ros::spinOnce()的使用,虽说比ros::spin()更自由,可以出现在程序的各个部位,但是需要注意的因素也更多。比如:

1 对于有些传输特别快的消息,尤其需要注意合理控制消息池大小和ros::spinOnce()执行频率; 比如消息送达频率为10Hz, ros::spinOnce()的调用频率为5Hz,那么消息池的大小就一定要大于2,才能保证数据不丢失,无延迟。

/**接收端**/
#include "ros/ros.h"
#include "std_msgs/String.h"
 
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
   /*...TODO...*/ 
}
 
int main(int argc, char **argv)
{
   ros::init(argc, argv, "listener");
   ros::NodeHandle n;
   ros::Subscriber sub = n.subscribe("chatter", 2, chatterCallback);
 
   ros::Rate loop_rate(5);
   while (ros::ok())
   {
       /*...TODO...*/ 

       ros::spinOnce();
       loop_rate.sleep();
   }
   return 0;

2. ros::spinOnce()用法很灵活,也很广泛,具体情况需要具体分析。但是对于用户自定义的周期性的函数,最好和ros::spinOnce并列执行,不太建议放在回调函数中;

/*...TODO...*/
ros::Rate loop_rate(100);
 
while (ros::ok())
{
   /*...TODO...*/
   user_handle_events_timeout(...);

   ros::spinOnce();                 
   loop_rate.sleep();
}

5.2 topic_demo

Topic是ROS里一种异步通信的模型, 一般是节点间分工明确, 有的只负责发送, 有的只负责接收处理。 对于绝大多数的机器人应用场景, 比如传感器数据收发, 速度控制指令的收发,Topic模型是最适合的通信方式。

一个消息收发的例子: 自定义一个类型为gps的消息( 包括位置x, y和工作状态state信息), 一个node
以一定频率发布模拟的gps消息, 另一个node接收并处理, 算出到原点的距离。

步骤:

  1. 建立package
  2. 自定义msg文件
  3. talker.cpp
  4. listener.cpp
  5. CmakeList.txt & package.xml

1.创建package

cd ~/tutorial_ws/src
catkin_creat_pkg topic_demo roscpp rospy std_msgs

2. 新建自定义msg

cd topic_demo/
mkdir msg
cd msg
gedit gps.msg

在这里插入图片描述
catkin_make 会把*.msg文件编译成*.h文件,#include一下就可以使用了

#include "topic_demo/gps.h"
topic_demo::gps msg;

3. talk.cpp

  • .pro文件的写法,主要是加入ros和msg生成的h文件
# topic_demo.pro
TEMPLATE = app
CONFIG += console cpp11
CONFIG -= app_bundle
CONFIG -= qt

SOURCES += \
    talker.cpp

LIBS += \
    -L/usr/local/lib \
    -L/opt/ros/kinetic/lib \
    -lroscpp -lrospack -lpthread -lrosconsole -lrosconsole_log4cxx\
    -lrosconsole_backend_interface -lxmlrpcpp -lroscpp_serialization -lrostime  -lcpp_common  -lroslib -lroslib


HEADERS += \


INCLUDEPATH +=\
/opt/ros/kinetic/include\
/home/swc/tutorial_ws/devel/include/     # 这个就是ros工作空间的/devel/include/
/*talker.cpp*/
#include <iostream>
#include "ros/ros.h"
#include "topic_demo/gps.h"

using namespace std;

int main(int argc,char** argv)
{
    ros::init(argc,argv,"talker");  //初始化,解析参数,命名节点为"talker"
    ros::NodeHandle nh;             //创建句柄,实例化node
    topic_demo::gps msg;            //创建gps消息
    msg.x = 1.0;                    //msg初始化
    msg.y = 1.0;
    msg.state = "working";
    ros::Publisher pub = nh.advertise<topic_demo::gps>("gps_info",1);//创建publisher
    //ros::Publisher pub = nh.advertise<消息格式>("topic名称",queue_size)
    //queue_size表示缓存队列长度,随时发送接收,设置成1这样比较小的数是可以的
    ros::Rate loop_rate(1.0);//ros::Rate是控制循环的一个类,定义循环发布的频率,这里是1Hz
    while(ros::ok())         //ros::ok() 表示只要ros没有关闭,就一直循环
    {
        //模拟数据的变化
        msg.x = 1.03 * msg.x;//以指数增长,每隔1秒
        msg.y = 1.01 * msg.y;
        //输出当前msg,ROS_INFO和printf(),cout 类似
        ROS_INFO("Talker_____:GPS:x = %f,y = %f",msg.x,msg.y); 
        pub.publish(msg);       //发布消息
        loop_rate.sleep();      //根据定义的发布频率,sleep一秒钟
    }
    return 0;
}

4.listener.cpp

/*listener.cpp*/
#include "ros/ros.h"
#include "topic_demo/gps.h"

//topic_demo::gps::ConstPtr也是一个类
//它是一个常指针,指向topic_demo::gps
//回调函数
void gpsCallback(const topic_demo::gps::ConstPtr& msg)
{
    float distance;
    float x = msg->x;
    float y = msg->y;
    distance = sqrt(pow(x,2)+pow(y,2));
    ROS_INFO("Listener_____distance to ogigin:%f,state:%s",distance,msg->state);
}

int main(int argc, char **argv)
{
    ros::init(argc,argv,"listener");
    ros::NodeHandle nh;
    /*
    创建subscribe
    ros::Subscriber sub = nh.subscribe("监听话题名称",消息队列长度,回调函数(指针);
    "监听话题名称":和publisher设置成一样
    消息队列长度,一般消息来了马上就会被处理掉,所以不要设置的太大,除非想对数据进行缓存,延迟处理
    回调函数:处理从"监听话题"上收到的消息
    */
    ros::Subscriber sub = nh.subscribe("gps_info",1,gpsCallback);
    /*
    实际上,没来一个消息,就把它放在队列里,并不是来一个就会自动处理一个,要调用一个spin()函数
    spin()函数,查看当前队列里面有没有消息,有的话,就调用回调函数进行处理,把队列清空,如果队列是空的,就不会处理
    反复调用当前可触发的回调函数,是一个阻塞的函数,执行到这一句,反复查看有没有可执行的回调函数
    还有一个ros::spinOnce()函数,只查看一次有没有可执行的回调函数,如果没有就往下执行
    */
    ros::spin();
    return 0;
}

5.修改CMakeLists.txt和package.xml

CMakeLists.txt

cmake_minimum_required(VERSION 2.8.3)   # CMAKE版本
project(topic_demo)		        # 项目名称

find_package(catkin REQUIRED COMPONENTS	# 指定依赖
  roscpp
  rospy
  std_msgs
message_generation
)
				
add_message_files(			# 添加自定义的msg
   FILES
gps.msg
)

generate_messages(DEPENDENCIES std_msgs)# 生成msg对应的h文件

catkin_package(CATKIN_DEPENDS roscpp rospy std_msgs message_runtime)# 用于配置ROS和package配置文件和Cmake文件

include_directories(include ${catkin_INCLUDE_DIRS})    # 指定C/cpp的头文件路径

add_executable(talker src/talker.cpp)			# 生成可执行目标文件
add_dependencies(talker topic_demo_generate_message_cpp)# 添加依赖,必须有这句来生成msg?
target_link_libraries(talker ${catkin_LIBRARIES})		# 链接

add_executable(listener src/listener.cpp)
add_dependencies(listener topic_demo_generate_message_cpp)
target_link_libraries(listener ${catkin_LIBRARIES})

package.xml 中添加两句

<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>

6.编译运行

catkin_make

rosrun topic_demo talker

rosrun topic_demo listener
在这里插入图片描述
也可以在qt里面编译运行listener.cpp,效果一样的

查看话题间关系:rqt_graph

在这里插入图片描述
7.先接收消息,处理之后发出去
在这里插入图片描述

/*listener.cpp*/
#include "ros/ros.h"
#include "topic_demo/gps.h"

topic_demo::gps gps_data;//转存接收到的数据

void gpsCallback(const topic_demo::gps::ConstPtr& msg)
{
    gps_data = *msg;
    gps_data.state = "received";
    float distance;
    float x = msg->x;
    float y = msg->y;
    distance = sqrt(pow(x,2)+pow(y,2));
    //ROS_INFO("Listener_____distance to ogigin:%f,state:%s",distance,msg->state.c_str());
}

int main(int argc, char **argv)
{
    ros::init(argc,argv,"listener");
    ros::NodeHandle nh;
    ros::Publisher pub = nh.advertise<topic_demo::gps>("gps_receive",1);
    ros::Subscriber sub = nh.subscribe("gps_info",1,gpsCallback);
    ros::Rate loop_rate(1.0);
    while(ros::ok())
    {
        ros::spinOnce();//要设置成spinOnce(),不然跑不出来
        pub.publish(gps_data);//也可以在回调函数里面pub
	ROS_INFO("publish  success");
        loop_rate.sleep();
    }
    return 0;
}
/*listener2.cpp*/
#include "ros/ros.h"
#include "topic_demo/gps.h"

void gpsCallback(const topic_demo::gps::ConstPtr& msg)
{
    float distance;
    float x = msg->x;
    float y = msg->y;
    distance = sqrt(pow(x,2)+pow(y,2));
    ROS_INFO("Listener2_____distance to ogigin:%f,state:%s",distance,msg->state.c_str());
}

int main(int argc, char **argv)
{
    ros::init(argc,argv,"listener2");
    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe("gps_receive",1,gpsCallback);
    ros::Rate loop_rate(1.0);
    while(ros::ok())
    {
        ros::spin();
	loop_rate.sleep();
    }
    return 0;
}

8.话题名称相关

以下代码显示了常用的话题的声明,我们通过修改话题名称来理解名称的用法。

int main(int argc, char** argv) 		 // 节点主函数
{
    ros::init( argc, argv, "node1"); 		 // 初始化节点
	{
    ros::NodeHandle nh; 				
    // 声明发布者,话题名 = bar
    // 声明节点句柄
    ros::Publisher node1_pub = nh.advertise<std_msg::Int32>("bar" , 10);
	}
}

? 这里的节点名称是/node1。如果您用一个没有任何字符的相对形式的bar来声明一个发布者,这个话题将和/bar具有相同的名字。

? 如果以如下所示使用斜杠(/)字符用作全局形式,话题名也是/bar。

ros::Publisher node1_pub = nh.advertise<std_msg::Int32>(“/bar”, 10);
但是,如果使用波浪号(~)字符将其声明为私有,则话题名称将变为/node1/bar。

ros::Publisher node1_pub = nh.advertise<std_msg::Int32>(“~bar”, 10);

这可以按照下表所示的各种方式使用。其中/wg意味着命名空间的修改。这在下面的描述中更详细地讨论。

NodeRelative(基本)GlobalPrivate
/node1bar->/bar/bar->/bar~bar->/node1/bar
/wg/node2bar->/wg/bar/bar->/bar~bar->/wg/node2/bar
/wg/node3foo/bar->/wg/foo/bar/foo/bar->/foo/ba~foo/bar->/wg/node3/foo/bar

5.3 service_demo

services是另一种ros通信的方式,具体介绍3.2.4 3.2.5
在这里插入图片描述

demo功能:

实现两个整数的相加

客户端:发送两个整数

服务端:返回求和

1.自定义srv文件

和topic的msg类似,在功能包中新建一个srv文件夹,里面保存自定义的srv文件

# 客户端发送的
int64 a
int64 b
# 客户端和服务端用---隔开
---
# 服务端返回的
int64 sum

2.生成cpp可以调用的格式

修改Cmakelists.txt package.xml

package.xml中添加功能包依赖:

<?xml version="1.0"?>
<package format="2">
  <name>service_demo</name>
  <version>0.0.0</version>
  <description>service_demo</description>
  <maintainer email="swc@todo.todo">swc</maintainer>

  <license>TODO</license>
    
  <buildtool_depend>catkin</buildtool_depend>
  <build_depend>roscpp</build_depend>
  <build_depend>rospy</build_depend>
  <build_depend>std_msgs</build_depend>
  <build_depend>message_generation</build_depend>

  <build_export_depend>roscpp</build_export_depend>
  <build_export_depend>rospy</build_export_depend>
  <build_export_depend>std_msgs</build_export_depend>

  <exec_depend>roscpp</exec_depend>
  <exec_depend>rospy</exec_depend>
  <exec_depend>std_msgs</exec_depend>
  <exec_depend>message_runtime</exec_depend>  

  <!-- The export tag contains other, unspecified, tags -->
  <export>
    <!-- Other tools can request additional information be placed here -->

  </export>
</package>

Cmakelists.txt中添加依赖选项

cmake_minimum_required(VERSION 3.0.2)
project(service_demo)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  message_generation
)

################################################
## Declare ROS messages, services and actions ##
################################################

## Generate services in the 'srv' folder
add_service_files(
  FILES
  AddTwoInts.srv
)


## Generate added messages and services with any dependencies listed here
generate_messages(
  DEPENDENCIES
  std_msgs
)


###################################
## catkin specific configuration ##
###################################
catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES service_demo
 CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
#  DEPENDS system_lib
)

###########
## Build ##
###########

## Specify additional locations of header files
## Your package locations should be listed before other locations
include_directories(
include
  ${catkin_INCLUDE_DIRS}
)

然后编译,生成h文件

3.客户端程序编写

在这里插入图片描述

/**
 * AddTwoInts Client
 */
 
#include <cstdlib>
#include "ros/ros.h"
#include "learning_communication/AddTwoInts.h"

int main(int argc, char **argv)
{
  // ROS节点初始化
  ros::init(argc, argv, "add_two_ints_client");
  
  // 从终端命令行获取两个加数
  if (argc != 3)
  {
    ROS_INFO("usage: add_two_ints_client X Y");
    return 1;
  }

  // 创建节点句柄
  ros::NodeHandle n;
  
  // 创建一个client,请求add_two_int service,service消息类型是learning_communication::AddTwoInts
  // 发送到的服务器提供的服务名:add_two_ints
  ros::ServiceClient client = n.serviceClient<learning_communication::AddTwoInts>("add_two_ints");
  
  // 创建learning_communication::AddTwoInts类型的service消息
  learning_communication::AddTwoInts srv;
  srv.request.a = atoll(argv[1]);
  srv.request.b = atoll(argv[2]);
  
  // 发布service请求,等待加法运算的应答结果,调用服务,如果call成功,返回true,并且srv.response中的值有效,反之亦然
  if (client.call(srv))
  {
    ROS_INFO("Sum: %ld", (long int)srv.response.sum);
  }
  else
  {
    ROS_ERROR("Failed to call service add_two_ints");
    return 1;
  }

  return 0;
}

4.服务端程序编写
在这里插入图片描述

/**
 * AddTwoInts Server
 */
 
#include "ros/ros.h"
#include "learning_communication/AddTwoInts.h"

// service回调函数,输入参数req,输出参数res
// 回调函数的参数好像只能这么写:req;res,尝试只传AddTwoInts报错
bool add(learning_communication::AddTwoInts::Request  &req,
         learning_communication::AddTwoInts::Response &res)
{
  // 将输入参数中的请求数据相加,结果放到应答变量中
  res.sum = req.a + req.b;
  ROS_INFO("request: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
  ROS_INFO("sending back response: [%ld]", (long int)res.sum);
  
  return true;
}

int main(int argc, char **argv)
{
  // ROS节点初始化
  ros::init(argc, argv, "add_two_ints_server");
  
  // 创建节点句柄
  ros::NodeHandle n;

  // 创建一个名为add_two_ints的server,将其广播到ros
  // 注册回调函数add()
  ros::ServiceServer service = n.advertiseService("add_two_ints", add);
  
  // 循环等待回调函数
  ROS_INFO("Ready to add two ints.");
  ros::spin();

  return 0;
}
  人工智能 最新文章
2022吴恩达机器学习课程——第二课(神经网
第十五章 规则学习
FixMatch: Simplifying Semi-Supervised Le
数据挖掘Java——Kmeans算法的实现
大脑皮层的分割方法
【翻译】GPT-3是如何工作的
论文笔记:TEACHTEXT: CrossModal Generaliz
python从零学(六)
详解Python 3.x 导入(import)
【答读者问27】backtrader不支持最新版本的
上一篇文章      下一篇文章      查看所有文章
加:2021-11-22 12:20:40  更:2021-11-22 12:21:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 4:33:53-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码