Autoware源码学习笔记(一):Vehicle_socket

本文为博主原创内容,未经博主允许不得转载。尊重他人知识成果,共同建立良好学习分享氛围,谢谢!

一、前言

  Vehicle_socket这个Package包含有vehicle_receiver和vehicle_sender两个node,主要是用于autoware平台应用车辆和Android设备的数据传输和通信,其中还涉及到了线程、客户机和服务器之间TCP/IP通信的知识和常用函数。我之前对这方面也不了解,很多内容也是现学现用的,如有错误,还希望各位提出指正。

  看源代码之前,先附上一张图,讲述的是客户机和服务器通信的基本框架思路,以便于更好的帮助理解源代码。
在这里插入图片描述

图1 客户机和服务器通信的基本框架

  博主主要对vehicle_receiver的函数和代码做了详细注释和解释,而vehicle_sender中的基本思路和使用函数与vehicle_receiver基本相似,故vehicle_sender中重复使用的函数没有加以说明。

二、vehicle_receiver

  在以autoware平台应用车辆作为服务器端,Android设备作为客户端的基础上,vehicle_receiver节点建立基本的TCP/IP通信机制,等待Android设备客户端向车辆服务器端发起通信请求,若请求则读取服务器端的数据,并按照一定的格式解析消息内容。其中主要包括了main函数、receiverCaller函数、getCanValue函数和parseCanValue函数。

2.1 main函数

  main函数主要创建了vehicle_receiver节点,然后创建mode_pub和can_pub两个publisher,随后创建一个线程,创建线程成功后,新创建的线程从指定的起始地址执行线程函数receiverCaller,而原来的线程则继续运行下一行代码。

#include <ros/ros.h>
#include <tablet_socket_msgs/mode_info.h>
#include "autoware_can_msgs/CANInfo.h"//先从自定义的文件中找头文件,如果找不到再从函数库中寻找文件。
#include <netinet/in.h>
#include <pthread.h>//在Linux编写多线程程序需要包含该头文件
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>//要使用istringstream、getline等类创建对象就必须包含该头文件
#include <string>
#include <vector>//在函数中使用vector函数,需添加该头文件

#define CAN_KEY_MODE (0)
#define CAN_KEY_TIME (1)
#define CAN_KEY_VELOC (2)
#define CAN_KEY_ANGLE (3)
#define CAN_KEY_TORQUE (4)
#define CAN_KEY_ACCEL (5)
#define CAN_KEY_BRAKE (6)
#define CAN_KEY_SHIFT (7)

static ros::Publisher can_pub;//创建publisher对象can_pub
static ros::Publisher mode_pub;//创建publisher对象mode_pub
static int mode;

int main(int argc, char **argv)
{
  ros::init(argc, argv, "vehicle_receiver");//ros定义名字为vehicle_receiver的节点
  ros::NodeHandle nh;//ros实例化句柄对象nh
  
  std::cout << "vehicle receiver" << std::endl;
  
  can_pub = nh.advertise<autoware_can_msgs::CANInfo>("can_info", 100);//定义"can_info"topic的内容
  mode_pub = nh.advertise<tablet_socket_msgs::mode_info>("mode_info", 100);//定义"mode_info"topic的内容
  
  pthread_t th;//声明线程ID
  int ret = pthread_create(&th, nullptr, receiverCaller, nullptr);//创建线程
  /*
    原型:int  pthread_create((pthread_t  *thread,  pthread_attr_t  *attr,  void  *(*start_routine)(void  *),  void  *arg)
    头文件:#include  <pthread.h>
    功能:创建线程(实际上就是确定调用该线程函数的入口点),在线程创建以后,就开始运行相关的线程函数。
    说明:thread:线程标识符;
          attr:线程属性设置;
          start_routine:线程函数的起始地址;
          arg:传递给start_routine的参数;
          返回值:若成功,返回0;若出错,返回-1。
  */

  if (ret != 0)
  {
    std::perror("pthread_create");
    /*
    perror(s) 用来将上一个函数发生错误的原因输出到标准设备。参数s所指的字符串会先打印出,后面再加上错误原因字符串。
    此错误原因依照全局变量errno的值来决定要输出的字符串。在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。
    当你调用"某些"函数出错时,该函数已经重新设置了errno的值。perror函数只是将你输入的一些信息和现在的errno所对应的错误一起输出。
    */
    std::exit(1);
    /*
    每个进程都会有一个返回值的. 进程开始时是由系统的一个启动函数掉用了main函数的:
    int   nMainRetVal   =   main(); 
    当从main函数退出后,启动函数便调用exit函数,并且把nMainRetVa传递给它. 所以,任何时候都会调用exit函数的。
    正常情况下,main函数不会调用exit函数的,而是由return 0 返回值给nMainRetVal的,exit再接收这个值作为参数的。
    所以,正常情况下是以exit(0)退出的,但是如果你的程序发生异常,你可以在main函数中调用exit(1),强制退出程序,强制终止进程.其中1表示不正常退出。    
    */
  }

  ret = pthread_detach(th);
  /*
  从状态上实现线程分离,注意不是指该线程独自占用地址空间。分离成功返回0,分离失败返回错误号。
  线程分离状态:指定该状态,线程主动与主控线程断开关系。
  线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源)。网络、多线程服务器常用。  
  */

  if (ret != 0)
  {
    std::perror("pthread_detach");
    std::exit(1);
  }

  ros::spin();
  return 0;
}

2.2 receiverCaller函数

  receiverCaller函数主要通过socket函数创建服务器端的流式套接字,然后使用bind函数将该套接字和指定地址相连接,随后使用listen函数表示服务器已做好准备,可以通信,最后利用accept函数接纳客户端请求并返回客户端套接字标识,并把该标识作为参数传递给线程函数getCanValue。

static void *receiverCaller(void *unused)
{
  constexpr int listen_port = 10000;
  /*
  constexpr是C++11中新增的关键字,其语义是“常量表达式”,其是在编译期就可以计算出结果的表达式。
  常量表达式的好处:
  1、允许一些计算只在编译时进行一次,而不是每次程序运行时;
  2、编译器可以在编译期对constexpr的代码进行非常大的优化,比如将用到的constexpr表达式都直接替换成最终结果等;
  3、是一种很强的约束,更好地保证程序的正确语义不被破坏;
  4、相比宏来说,没有额外的开销,但更安全可靠。
  */

  int sock = socket(AF_INET, SOCK_STREAM, 0);
  /*
    socket函数的使用方法如下:
  int socket(int domain, int type, int protocol),若返回值为非负描述符,则表示成功,若返回值为-1,则表示出错。
  在参数表中,domain指定使用何种的地址类型,比较常用的有:
    PF_INET, AF_INET: Ipv4网络协议;
    PF_INET6, AF_INET6: Ipv6网络协议。
  type参数的作用是设置通信的协议类型,可能的取值如下所示:
    SOCK_STREAM: 提供面向连接的稳定数据传输,即TCP协议。其应用在C语言socket编程中,在进行网络连接前,需要用socket函数向系统申请一个通信端口;
    OOB: 在所有数据传送前必须使用connect()来建立连接状态;
    SOCK_DGRAM: 使用不连续不可靠的数据包连接;
    SOCK_SEQPACKET: 提供连续可靠的数据包连接;
    SOCK_RAW: 提供原始网络协议存取;
    SOCK_RDM: 提供可靠的数据包连接;
    SOCK_PACKET: 与网络驱动程序直接通信。
  参数protocol用来指定socket所使用的传输协议编号。这一参数通常不具体设置,一般设置为0即可。
  */

  if (sock == -1)
  {
    std::perror("socket");
    return nullptr;
  }

  sockaddr_in client;//sockaddr_in是系统封装的一个结构体,这里实例化一个client来存放地址信息。
  socklen_t len = sizeof(client);//socklen_t是一种数据类型,它其实和int差不多,在32位机下,size_t和int的长度相同,都是32 bits,但在64位机下,size_t(32bits)和int(64 bits)的长度是不一样的。
  sockaddr_in addr;//实例化一个addr来存放地址信息

  std::memset(&addr, 0, sizeof(sockaddr_in));//从addr的初始地址开始到后面的sizeof(sockaddr_in)个字节用0替换并返回addr。这个函数在socket中多用于清空数组

  addr.sin_family = PF_INET;
  addr.sin_port = htons(listen_port);
  addr.sin_addr.s_addr = INADDR_ANY;
    /*
    sockaddr_in是系统封装的一个结构体,具体包含了成员变量:sin_family、sin_port、sin_addr、sin_zero等等。
    sin_family主要用于定义地址族,PF_INET代表TCP/IP
    sin_port主要用来保存端口号,将监听套接字的端口设置为listen_port,htons表示host to network short,用来进行主机字节序和网络字节序的转换。
    sin_addr主要用来保存IP地址信息,INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。
        比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某个端口,那他要监听哪个网卡地址的端口呢?
        如果绑定某个具体的ip地址,你只能监听你所设置的ip地址所在的网卡的端口,其它两块网卡无法监听端口,如果我需要三个网卡都监听,那就需要绑定3个ip,也就等于需要管理3个套接字进行数据交换,这样岂不是很繁琐?
        所以出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。
    sin_zero无特殊含义,只是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。用来将sockaddr_in结构填充到与struct
        sockaddr同样的长度,可以用bzero()或memset()函数将其置为零。  
    */

  // make it available immediately to connect
  // setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
  int ret = bind(sock, (sockaddr *)&addr, sizeof(addr));//bind函数把名字和套接字相关联
  /*
  int bind(int sockfd, const struct sockaddr *addr,socklen_t *addrlen);
  当用socket()函数创建套接字以后,套接字在名称空间(网络地址族)中存在,但没有任何地址给它赋值。
  bind()把用addr指定的地址赋值给用文件描述符代表的套接字sockfd。addrlen指定了以addr所指向的地址结构体的字节长度。
  一般来说,该操作称为“给套接字命名”。
  */
  
  if (ret == -1)
  {
    std::perror("bind");
    goto error;
  }

  ret = listen(sock, 5);
  /*
  int PASCAL FAR listen (SOCKET s, int backlog);
  第一个参数是服务端套接字,你要聆听,总得出来说个话啊,好,就指定你了;
  第二个参数是等待连接队列的最大长度,比方说,你将backlog定为10, 当有15个连接请求的时候,前面10个连接请求就被放置在请求队列中,后面5个请求被拒绝。
  再看函数的返回值,成功返回0, 失败返回-1。
  */

  if (ret == -1)
  {
    std::perror("listen");
    goto error;
  }

  while (true)
  {
    // get connect to android
    std::cout << "Waiting access..." << std::endl;
    int *client_sock = new int();
    *client_sock = accept(sock, reinterpret_cast<sockaddr *>(&client), &len);
    /*
    unsigned int sockConn = accept(sockSrv,(SOCKADDR*)&addrClient, &len);
    接纳客户端请求的函数是accept,其指定服务端去接受客户端的连接,接收后,返回了客户端套接字的标识,
    并且获得了客户端套接字的“地方”(包括客户端IP和端口信息等)。
    第一个参数用来标识服务端套接字(也就是listen函数中设置为监听状态的套接字);
    第二个参数是用来保存客户端套接字对应的“地方”(包括客户端IP和端口信息等);
    第三个参数是“地方”的占地大小。返回值对应客户端套接字标识。
    */
    
    if (*client_sock == -1)
    {
      std::perror("accept");
      break;
    }

    std::cout << "Get connect" << std::endl;

    pthread_t th;
    if (pthread_create(&th, nullptr, getCanValue, static_cast<void *>(client_sock)))
        //static_cast用法:static_cast < type-id > ( expression ),该运算符把expression转换为type - id类型。
    {
      std::perror("pthread_create");
      break;
    }

    ret = pthread_detach(th);
    if (ret != 0)
    {
      std::perror("pthread_detach");
      break;
    }
  }

error:
  close(sock);
  return nullptr;
}

2.3 getCanValue函数

  getCanValue函数主要是客户端通过recv函数从服务器端读取CAN原始数据,然后关闭客户端套接字,随后parseCanValue函数解析CAN原始数据,最后定义can_info和mode_info两个topic的结构内容。

static void *getCanValue(void *arg)
{
  int *client_sockp = static_cast<int *>(arg);
  int sock = *client_sockp;
  delete client_sockp;

  char recvdata[1024];
  std::string can_data("");
  constexpr int LIMIT = 1024 * 1024;

  while (true)
  {
    ssize_t n = recv(sock, recvdata, sizeof(recvdata), 0);
    /*
    recv函数 int recv( SOCKET s,     char FAR *buf,      int len,     int flags     );  
    不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
    该函数的第一个参数指定接收端套接字描述符;
    第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
    第三个参数指明buf的长度;
    第四个参数一般置0。
    这里只描述同步Socket的recv函数的执行流程。当应用程序调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,
    如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,
    如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,
    如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到 协议把数据接收完毕。
    当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,
    所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),
    recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0
    */

    if (n < 0)
    {
      std::perror("recv");
      can_data = "";
      break;
    }
    else if (n == 0)
    {
      break;
    }
    can_data.append(recvdata, n);
    /*
    从字符串recvdata的第0到第n个字符连接在string字符串can_data后面
    */

    // recv data is bigger than 1M,return error
    if (can_data.size() > LIMIT)
    {
      std::cerr << "recv data is too big." << std::endl;
      can_data = "";
      break;
    }
  }

  if (close(sock) < 0)
  {
    std::perror("close");
    return nullptr;
  }

  if (can_data.empty())
    return nullptr;

  autoware_can_msgs::CANInfo can_msg;
  bool ret = parseCanValue(can_data, can_msg);
  if (!ret)
    return nullptr;

  can_msg.header.frame_id = "/can";
  can_msg.header.stamp = ros::Time::now();
  can_pub.publish(can_msg);

  tablet_socket_msgs::mode_info mode_msg;
  mode_msg.header.frame_id = "/mode";
  mode_msg.header.stamp = ros::Time::now();
  mode_msg.mode = mode;
  mode_pub.publish(mode_msg);

  return nullptr;
}

2.4 parseCanValue函数

  parseCanValue函数主要解析客户端通过recv函数从服务器端读取到的数据,并更新can_msg和mode的内容。

static bool parseCanValue(const std::string &can_data, autoware_can_msgs::CANInfo &msg)
{
  std::istringstream ss(can_data);
  std::vector<std::string> columns;//初始化列表空对象columns

  std::string column;
  while (std::getline(ss, column, ','))
      /*
      getline()的原型:getline(istream &is,string &str,char delim)
      istream &is表示一个输入流,譬如cin,string表示把从输入流读入的字符串存放在这个字符串中(&str其实就是一个变量)
      char delim是终止符(默认为回车,还可以是别的符号,如#,*之类的都可以)
      而对于while(getline(cin,str))来讲,while语句的真实判断对象是cin,也就是当前是否存在有效的输入流,如果存在就不会结束循环
      */
  {
    columns.push_back(column);//列表尾部插入column
  }

  for (std::size_t i = 0; i < columns.size(); i += 2)
      /*
      size_t是一种“整型”类型,里面保存的是一个整数,就像int, long那样。这种整数用来记录一个大小(size)。
      size_t的全称应该是size type,就是说“一种用来记录大小的数据类型”。
      */
  {
    int key = std::stoi(columns[i]);//stoi()可以将string转化为int。
    switch (key)
    {
      case CAN_KEY_MODE:
        mode = std::stoi(columns[i + 1]);
        if (mode & 0x1)
        {
          msg.devmode = 1;
        }
        else
        {
          msg.devmode = 0;
        }

        if (mode & 0x2)
        {
          msg.strmode = 1;
        }
        else
        {
          msg.strmode = 0;
        }
        break;
      case CAN_KEY_TIME:
        msg.tm = columns[i + 1].substr(1, columns[i + 1].length() - 2);  // skip '
        break;
      case CAN_KEY_VELOC:
        msg.speed = std::stod(columns[i + 1]);
        break;
      case CAN_KEY_ANGLE:
        msg.angle = std::stod(columns[i + 1]);
        break;
      case CAN_KEY_TORQUE:
        msg.torque = std::stoi(columns[i + 1]);
        break;
      case CAN_KEY_ACCEL:
        msg.drivepedal = std::stoi(columns[i + 1]);
        break;
      case CAN_KEY_BRAKE:
        msg.brakepedal = std::stoi(columns[i + 1]);
        break;
      case CAN_KEY_SHIFT:
        msg.driveshift = std::stoi(columns[i + 1]);
        break;
      default:
        std::cout << "Warning: unknown key : " << key << std::endl;
    }
  }

  return true;
}

三、vehicle_sender

  在以autoware平台应用车辆作为服务器端,Android设备作为客户端的基础上,vehicle_sender节点建立基本的TCP/IP通信机制,等待Android设备客户端向车辆服务器端发起通信请求,若请求则把服务器端数据发送给客户端。其中主要包括了main函数、vehicleCmdCallback函数、receiverCaller函数和sendCommand函数。

3.1 main函数

  main函数主要创建了vehicle_sender节点,然后创建sub这个subscriber和回调函数vehicleCmdCallback,随后创建一个线程,创建线程成功后,新创建的线程从指定的起始地址执行线程函数receiverCaller,而原来的线程则继续运行下一行代码,最后通过阻塞函数反复调用队列中可执行的回调函数vehicleCmdCallback。

#include <ros/ros.h>
#include "autoware_msgs/VehicleCmd.h"
#include "autoware_msgs/Gear.h"
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>

struct CommandData
{
  double linear_x;
  double angular_z;
  int modeValue;
  int gearValue;
  int lampValue;
  int accellValue;
  int brakeValue;
  int steerValue;
  double linear_velocity;
  double steering_angle;

  void reset();
};

enum class ZMPGear
{
  Drive = 1,
  Reverse = 2,
  Low = 3,
  Neutral = 4,
  Park = 5,
};

void CommandData::reset()
{
  linear_x = 0;
  angular_z = 0;
  modeValue = 0;
  gearValue = 0;
  lampValue = 0;
  accellValue = 0;
  brakeValue = 0;
  steerValue = 0;
  linear_velocity = -1;
  steering_angle = 0;
}

static CommandData command_data;

int main(int argc, char** argv)
{
  ros::init(argc, argv, "vehicle_sender");
  ros::NodeHandle nh;

  std::cout << "vehicle sender" << std::endl;
  ros::Subscriber sub = nh.subscribe("/vehicle_cmd", 1, vehicleCmdCallback);

  command_data.reset();

  pthread_t th;
  if (pthread_create(&th, nullptr, receiverCaller, nullptr) != 0)
  {
    std::perror("pthread_create");
    std::exit(1);
  }

  if (pthread_detach(th) != 0)
  {
    std::perror("pthread_detach");
    std::exit(1);
  }

  ros::spin();//阻塞,反复查看有无可执行的回调函数
  return 0;
}

3.2 vehicleCmdCallback函数

  vehicleCmdCallback函数主要是定义command_data的内容,这个command_data之后要从服务器端发送给客户端。

static void vehicleCmdCallback(const autoware_msgs::VehicleCmd& msg)
{
  command_data.linear_x = msg.twist_cmd.twist.linear.x;
  command_data.angular_z = msg.twist_cmd.twist.angular.z;
  command_data.modeValue = msg.mode;
  if (msg.gear_cmd.gear == autoware_msgs::Gear::DRIVE)
  {
    command_data.gearValue = static_cast<int>(ZMPGear::Drive);
  }
  else if (msg.gear_cmd.gear == autoware_msgs::Gear::REVERSE)
  {
    command_data.gearValue = static_cast<int>(ZMPGear::Reverse);
  }
  else if (msg.gear_cmd.gear == autoware_msgs::Gear::LOW)
  {
    command_data.gearValue = static_cast<int>(ZMPGear::Low);
  }
  else if (msg.gear_cmd.gear == autoware_msgs::Gear::NEUTRAL)
  {
    command_data.gearValue = static_cast<int>(ZMPGear::Neutral);
  }
  else if (msg.gear_cmd.gear == autoware_msgs::Gear::PARK)
  {
    command_data.gearValue = static_cast<int>(ZMPGear::Park);
  }

  if (msg.lamp_cmd.l == 0 && msg.lamp_cmd.r == 0)
  {
    command_data.lampValue = 0;
  }
  else if (msg.lamp_cmd.l == 1 && msg.lamp_cmd.r == 0)
  {
    command_data.lampValue = 1;
  }
  else if (msg.lamp_cmd.l == 0 && msg.lamp_cmd.r == 1)
  {
    command_data.lampValue = 2;
  }
  else if (msg.lamp_cmd.l == 1 && msg.lamp_cmd.r == 1)
  {
    command_data.lampValue = 3;
  }
  command_data.accellValue = msg.accel_cmd.accel;
  command_data.steerValue = msg.steer_cmd.steer;
  command_data.brakeValue = msg.brake_cmd.brake;
  command_data.linear_velocity = msg.ctrl_cmd.linear_velocity;
  command_data.steering_angle = msg.ctrl_cmd.steering_angle;
}

3.3 receiverCaller函数

  receiverCaller函数又创建了一个服务器端的流式套接字,同样通过socket函数创建服务器端的流式套接字,然后使用bind函数将该套接字和指定地址相连接,随后使用listen函数表示服务器已做好准备,可以通信,最后利用accept函数接纳客户端请求并返回客户端套接字标识,并把该标识作为参数传递给线程函数sendCommand。

static void* receiverCaller(void* unused)
{
  constexpr int listen_port = 10001;

  int sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock == -1)
  {
    std::perror("socket");
    return nullptr;
  }

  sockaddr_in addr;
  sockaddr_in client;
  socklen_t len = sizeof(client);

  std::memset(&addr, 0, sizeof(sockaddr_in));
  addr.sin_family = PF_INET;
  addr.sin_port = htons(listen_port);
  addr.sin_addr.s_addr = INADDR_ANY;

  int ret = bind(sock, (struct sockaddr*)&addr, sizeof(addr));
  if (ret == -1)
  {
    std::perror("bind");
    goto error;
  }

  ret = listen(sock, 20);
  if (ret == -1)
  {
    std::perror("listen");
    goto error;
  }

  while (true)
  {
    // get connect to android
    std::cout << "Waiting access..." << std::endl;

    int* client_sock = new int();
    *client_sock = accept(sock, reinterpret_cast<sockaddr*>(&client), &len);
    if (*client_sock == -1)
    {
      std::perror("accept");
      break;
    }

    std::cout << "get connect." << std::endl;

    pthread_t th;
    if (pthread_create(&th, nullptr, sendCommand, static_cast<void*>(client_sock)) != 0)
    {
      std::perror("pthread_create");
      break;
    }

    if (pthread_detach(th) != 0)
    {
      std::perror("pthread_detach");
      break;
    }
  }

error:
  close(sock);
  return nullptr;
}

3.4 sendCommand函数

  sendCommand函数主要使用write函数把服务器端的数据往客户端写入。

static void* sendCommand(void* arg)
{
  int* client_sockp = static_cast<int*>(arg);
  int client_sock = *client_sockp;
  delete client_sockp;

  std::ostringstream oss;
  oss << command_data.linear_x << ",";
  oss << command_data.angular_z << ",";
  oss << command_data.modeValue << ",";
  oss << command_data.gearValue << ",";
  oss << command_data.accellValue << ",";
  oss << command_data.brakeValue << ",";
  oss << command_data.steerValue << ",";
  oss << command_data.linear_velocity << ",";
  oss << command_data.steering_angle << ",";
  oss << command_data.lampValue;

  std::string cmd(oss.str());
  ssize_t n = write(client_sock, cmd.c_str(), cmd.size());
  /*
  ssize_t write(int filedes, const void *buf, size_t nbytes);
  返回值:写入文件的字节数(成功);-1(出错)
  write 函数向 filedes 中写入 nbytes 字节数据,数据来源为 buf 。
  返回值一般总是等于 nbytes,否则就是出错了。常见的出错原因是磁盘空间满了或者超过了文件大小限制。
  */

  if (n < 0)
  {
    std::perror("write");
    return nullptr;
  }

  if (close(client_sock) == -1)
  {
    std::perror("close");
    return nullptr;
  }

  std::cout << "cmd: " << cmd << ", size: " << cmd.size() << std::endl;
  return nullptr;
}
  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Authware古诗教程是一款专门为学习和欣赏古诗而设计的软件。它提供了丰富的古诗资源和相关的学习功能,帮助用户更好地理解和感受古代文化。 首先,Authware古诗教程拥有庞大的古诗库,涵盖了各个朝代的经典古诗。用户可以通过浏览功能,随时查阅自己喜欢的古诗,了解作者背景和作品的内涵。 其次,Authware古诗教程提供了详细的古诗解析和注释。对于初学者来说,读懂古诗可能会有一些难度,但通过软件提供的解析和注释,用户可以更好地理解古人的用词和意境,进一步欣赏古诗之美。 此外,Authware古诗教程还提供了古诗背景介绍和相关阅读材料。了解古代社会背景和文化氛围,能够更加全面地把握古诗的内涵和艺术价值。通过相关阅读材料的学习,用户可以拓展自己的知识面,提升古诗欣赏的层次。 最后,Authware古诗教程还提供了互动学习和分享社区。用户可以与其他热爱古诗的人交流学习,分享自己的感悟和见解。这种互动交流可以激发更多的灵感和思考,使学习和欣赏古诗变得更加有趣和有意义。 总之,Authware古诗教程是一款功能丰富、易于操作的软件,它可以帮助用户更好地学习和欣赏古诗,了解古代文化,并与其他人分享自己的想法。无论是初学者还是资深爱好者,都可以在这个平台上找到自己需要的资源和交流机会。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值