Autoware源码学习笔记(一):Vehicle_socket
本文为博主原创内容,未经博主允许不得转载。尊重他人知识成果,共同建立良好学习分享氛围,谢谢!
一、前言
Vehicle_socket这个Package包含有vehicle_receiver和vehicle_sender两个node,主要是用于autoware平台应用车辆和Android设备的数据传输和通信,其中还涉及到了线程、客户机和服务器之间TCP/IP通信的知识和常用函数。我之前对这方面也不了解,很多内容也是现学现用的,如有错误,还希望各位提出指正。
看源代码之前,先附上一张图,讲述的是客户机和服务器通信的基本框架思路,以便于更好的帮助理解源代码。
博主主要对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;
}