网络通信协议之-进阶学习

一、高速网络通信基础

  1. 继上一章节介绍过网络编程粘包问题之后,一般的网络传输过程中,大都为以数据包组织的方式进行传输。因此,对于一般的数据包都包括了包头包尾,包长度,包中的具体数据等信息。对于校验码等相关内容无需添加,TCP网络在接收之后会对数据自行校验,具体参见这里

  2. 对于之前介绍的Linux基础TCP通信的建立是不完备的,主要体现在一下几点:

    • 建立socket过程中并未处理假如某些服务已经占用了当前主机端口Port而导致冲突的情况。
    • 没有对客户端Client异常掉线失去连接的情况进行异常处理,这样可能导致Server端Crash崩溃!
    • 对于待进行传输的数据带宽需要进行基本的计算,考虑设备硬件的基本传输能力,避免导致Linux Socket底层数据缓冲空间溢出,send或recv函数出现阻塞等。
    • 对于不希望accept阻塞等待的情况,还需要考虑使用多线程或者select非阻塞等待函数来实现。

二、基本异常处理

1. 数据包结构体组织形式

typedef struct ProfileData{
    int iHeader;               // 0xFFFF FFFE
    int iReserveS1;            // 0x0000 0000
    int iProfileSEQ;           // 数据序号
    int iReserveS2;            // 0x0000 0000
    int iEncoderVal;           // 编码数据
    int iReserveS3;            // 0x0000 0000
    int *piProfileData;        // 数据指针
    int piProfileDataLen;      // 数据总长
    int iPacketEnd;            // 0x0000 0000  
}T_StructProfileData;

2. Client客户端失联信号处理

static void socket_sig_deal(void)
{
    struct sigaction sa;
    sigemptyset(&sa.sa_mask); // 设定信号处理过程中被屏蔽的阻塞信号
    sa.sa_handler = handle_pipe; // 设定信号处理函数地址, 一般情况下sa_handler只传入信号量值, 不携带其他信号相关信息
    sa.sa_flags = 0; // 标志位设定, 配合sa_handler/sa_sigaction不同的处理函数使用, 需要传入其他参数进入处理函数时需要选择sa.sa_sigaction + SA_SIGINFO
    sigaction(SIGPIPE,&sa,NULL); // SIGPIPE 管道断开信号, 产生的 error = EPIPE, 不做捕获处理会导致程序奔溃
}

3. 主动判断socket连接状态

static int net__socket_established(int iSock)
{
    struct tcp_info info; // 创建tcp_info结构体用来存储tcp状态信息
    int len = sizeof(info); 
    getsockopt(iSock, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len); // 获取TCP当前状态信息
    if ((info.tcpi_state == TCP_ESTABLISHED)) // 判断当前TCP是否处在建立连接状态
	{
        return SUCCESS; // 返回连接状态
    }
	else
	{
        return ERR_CONN_LOST; // 返回失联状态
    }
}

三、高速通信基本代码实现

1. 服务端代码(Server.c)

#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/tcp.h> // SOL_SOCKET,SO_RCVTIMO,SO_SNDTIMEO,IPPOTO_TCP,TCP_NODELAY

typedef struct ProfileData{
    int iHeader;               // 0xFFFF FFFE
    int iReserveS1;            // 0x0000 0000
    int iProfileSEQ;           // 数据序号
    int iReserveS2;            // 0x0000 0000
    int iEncoderVal;           // 编码数据
    int iReserveS3;            // 0x0000 0000
    int *piProfileData;        // 数据指针
    int piProfileDataLen;      // 数据总长
    int iPacketEnd;            // 0x0000 0000  
}T_StructProfileData;

static void socket_sig_deal(void);
static int net__socket_established(int iSock);
int highSpeedCommunicateSetup(int *ipState);
int highSpeedCommunicateTransimit(int iSock, const T_StructProfileData t_StructDataPacket);

#define host "0.0.0.0"
#define port 50000

#define SERVICE "50000"
#define socket_domain 1

#define SUCCESS 1
#define INVALID_SOCKET -1
#define ERR_CONN_LOST -2
#define ERR_ERRNO -3

/*
TCP/IP协议中针对TCP默认开启了Nagle算法。Nagle算法通过减少需要传输的数据包,来优化网络。在内核实现中,数据包的发送和接受会先做缓存,分别对应于写缓存和读缓存。
启动TCP_NODELAY,就意味着禁用了Nagle算法,允许小包的发送。对于延时敏感型,同时数据传输量比较小的应用,开启TCP_NODELAY选项无疑是一个正确的选择。
比如,对于SSH会话,用户在远程敲击键盘发出指令的速度相对于网络带宽能力来说,绝对不是在一个量级上的,所以数据传输非常少;而又要求用户的输入能够及时获得返回,
有较低的延时。如果开启了Nagle算法,就很可能出现频繁的延时,导致用户体验极差。当然,你也可以选择在应用层进行buffer,比如使用java中的buffered stream,
尽可能地将大包写入到内核的写缓存进行发送;vectored I/O(writev接口)也是个不错的选择。
对于关闭TCP_NODELAY,则是应用了Nagle算法。数据只有在写缓存中累积到一定量之后,才会被发送出去,这样明显提高了网络利用率(实际传输数据payload与协议头的比例大大提高)。
但是这又不可避免地增加了延时;与TCP delayed ack这个特性结合,这个问题会更加显著,延时基本在40ms左右。当然这个问题只有在连续进行两次写操作的时候,才会暴露出来。
连续进行多次对小数据包的写操作,然后进行读操作,本身就不是一个好的网络编程模式;在应用层就应该进行优化。
*/

#define set_tcp_nodelay 0 // 

#define DATA_LEN 3200

int net__socket_listen(void);
int net__socket_accept(int listensock);

ssize_t net__write(int sock,const void *buf, size_t count);
int packet__write(int sock,const void *buf, size_t count);

int main(void)
{
	int flag = 0;
	T_StructProfileData ProfileData;
	memset(&ProfileData,0,sizeof(T_StructProfileData));

	ProfileData.iHeader = 0xFAFFF6F8;
	ProfileData.iReserveS1 = ProfileData.iReserveS2 = ProfileData.iReserveS3 = ProfileData.iPacketEnd = 0x00000000;
	ProfileData.iProfileSEQ = 0x00000000;	
	ProfileData.iEncoderVal = 0xEFFFD2E3;
	ProfileData.piProfileDataLen = 3200;
	ProfileData.piProfileData = (int *)malloc(sizeof(int)*ProfileData.piProfileDataLen);

	memset(ProfileData.piProfileData, 21,sizeof(int)*ProfileData.piProfileDataLen);

	int sock_ok  = highSpeedCommunicateSetup(&flag);
	if(flag == -1){
		printf("net__socket_accept failed.\n");
		return 0;
	}

	int temp = sock_ok;
	printf("####################################################### sock_ok=%d\n",temp);
	sleep(3);

	while(1){
		if(sock_ok != temp){
			printf("####################################################### sock_ok=%d\n",sock_ok);
		}
		ProfileData.iProfileSEQ += 1;
		flag = highSpeedCommunicateTransimit(sock_ok,ProfileData);
		if(flag == ERR_CONN_LOST){
			printf("ERR_CONN_LOST ####################################################################!!!!!!!!!!!!!!\n");
		}
		if(flag >= 0){
			printf("packet__write successful.\n");
		}else{
			printf("packet__write failed.\n");
		}
		sleep(1);
	}
	return 0;
}

int highSpeedCommunicateSetup(int *ipState)
{
	int isock = 0;
	socket_sig_deal();
	isock = net__socket_listen();
	if(-1 == isock)
	{
		return INVALID_SOCKET;
	}
	isock = net__socket_accept(isock);
	if(-1 == isock)
	{
		return INVALID_SOCKET;
	}
	return isock;
}

int highSpeedCommunicateTransimit(int iSock, const T_StructProfileData t_StructDataPacket)
{
	int flag = 0;
	flag = packet__write(iSock, &(t_StructDataPacket.iHeader), 4); if(0 != flag) {return flag;}// &(t_StructDataPacket.iHeader)
	flag = packet__write(iSock, &(t_StructDataPacket.iReserveS1), 4); if(0 != flag) {return flag;} // &(t_StructDataPacket.iHeader)
	flag = packet__write(iSock, &(t_StructDataPacket.iProfileSEQ), 4); if(0 != flag) {return flag;} // &(t_StructDataPacket.iHeader)
	flag = packet__write(iSock, &(t_StructDataPacket.iReserveS2), 4); if(0 != flag) {return flag;} // &(t_StructDataPacket.iHeader)
	flag = packet__write(iSock, &(t_StructDataPacket.iEncoderVal), 4); if(0 != flag) {return flag;} // &(t_StructDataPacket.iHeader)
	flag = packet__write(iSock, &(t_StructDataPacket.iReserveS3), 4); if(0 != flag) {return flag;} // &(t_StructDataPacket.iHeader)
	flag = packet__write(iSock, &(t_StructDataPacket.iReserveS3), 4); if(0 != flag) {return flag;} // &(t_StructDataPacket.iHeader)
	flag = packet__write(iSock, &(t_StructDataPacket.iReserveS3), 4); if(0 != flag) {return flag;} // &(t_StructDataPacket.iHeader)
	flag = packet__write(iSock, t_StructDataPacket.piProfileData, t_StructDataPacket.piProfileDataLen * 4); if(0 != flag) {return flag;} // &(t_StructDataPacket.iHeader)
	flag = packet__write(iSock, &(t_StructDataPacket.iPacketEnd), 4); if(0 != flag) {return flag;} // &(t_StructDataPacket.iHeader)
	return 0;
}

int net__socket_listen(void)
{
	int sock = INVALID_SOCKET;
	struct addrinfo hints;
	struct addrinfo *ainfo, *rp;
	char service[10];
	int rc;
	char ss_opt = 1;
	unsigned int sock_count = 0;

	snprintf(service, 10, "%d", port);
	memset(&hints, 0, sizeof(struct addrinfo)); // 初始化模板 hints 变量
	if(socket_domain){
		hints.ai_family = AF_INET; //AF_INET6
	}else{
		hints.ai_family = AF_UNSPEC;
	}
	hints.ai_flags = AI_PASSIVE;
	hints.ai_socktype = SOCK_STREAM;

	rc = getaddrinfo(NULL, SERVICE, &hints, &ainfo);
	if (rc){
		printf("Error creating listener: %s.", gai_strerror(rc));
		return INVALID_SOCKET;
	}

	for(rp = ainfo; rp; rp = rp->ai_next){
		if(rp->ai_family == AF_INET){
			printf("Opening ipv4 listen socket on port %d.\n", ntohs(((struct sockaddr_in *)rp->ai_addr)->sin_port));
		}else if(rp->ai_family == AF_INET6){
			printf("Opening ipv6 listen socket on port %d.\n", ntohs(((struct sockaddr_in6 *)rp->ai_addr)->sin6_port));
		}else{
			continue;
		}
		sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
		if(sock == INVALID_SOCKET){
			printf("ERR:%s-%d\n",__FUNCTION__,__LINE__);
			continue;
		}
		if(bind(sock, rp->ai_addr, rp->ai_addrlen) != 0){
			printf("ERR:%s-%d\n",__FUNCTION__,__LINE__);
			break;
		}
		close(sock);
	}

	if(listen(sock, 100) == -1){
		printf("Error:");
		close(sock);
		return INVALID_SOCKET;
	}
	freeaddrinfo(ainfo);

	return sock;
}

int net__socket_accept(int listensock)
{
	int new_sock = INVALID_SOCKET, struct_len = 0;
	struct sockaddr_in client_addr;

	struct_len = sizeof(struct sockaddr_in);

	new_sock = accept(listensock, (struct sockaddr *)&client_addr, &struct_len);
	if(new_sock == INVALID_SOCKET){
		return INVALID_SOCKET;
	}

	if(set_tcp_nodelay){
		int flag = 1;
		if(setsockopt(new_sock, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int)) != 0){
			printf("XiongGu Warning: Unable to set TCP_NODELAY.");
		}
	}
	return new_sock;
}

/**********************************************************************
* 函数名称: // static ssize_t net__write(int sock,const void *buf, size_t count)

* 功能描述: // 尝试向Socket套接字缓冲区写入count字节数据
* 访问的表: //
* 修改的表: //
* 输入参数: // int sock 可用的socket句柄
* 输入参数: // void *buf 用于存储读准备写入的字节(一般为char)
* 输入参数: // size_t count 期望写入的字节总数, 此值需要小于等于buf空间大小/sizeof(char) >= count

* 输出参数: // 对输出参数的说明
* 返 回 值: // ssize_t: 表示实际写入的字节数
* 其它说明: // 其它说明
* 修改日期       修改人	     修改内容
* -----------------------------------------------
* 2021/11/02	  	    XXXX	      XXXX
***********************************************************************/
ssize_t net__write(int sock,const void *buf, size_t count)
{
	return send(sock, buf, count, 0);
}

/**********************************************************************
* 函数名称: // static int packet__write(int sock,const void *buf, size_t count)

* 功能描述: // 尝试从Socket套接字缓冲区写入count字节数据, 写入到count字节数据为止
* 访问的表: //
* 修改的表: //
* 输入参数: // int sock 可用的socket句柄
* 输入参数: // void *buf 用于存储读准备写入的字节(一般为char)
* 输入参数: // size_t count 期望写入的字节总数, 此值需要小于等于buf空间大小/sizeof(char) >= count

* 输出参数: // 对输出参数的说明
* 返 回 值: // ssize_t: 表示实际写入的字节数
* 其它说明: // 其它说明
* 修改日期       修改人	     修改内容
* -----------------------------------------------
* 2021/11/02	  	    XXXX	      XXXX
***********************************************************************/
int packet__write(int sock,const void *buf, size_t count)
{
	ssize_t write_length;
	char *data = (char *)buf;
	int pos=0;
	int to_process = count;
	while(to_process > 0)
	{
		write_length = net__write(sock,&data[pos],count);
		if(write_length > 0){
			to_process -= write_length;
			pos += write_length;
		}
		else
		{
			if(errno == EAGAIN || errno == EWOULDBLOCK)
			{
				return 0;
			}
			else
			{
				
				switch(errno)
				{
					case ECONNRESET:
						return ERR_CONN_LOST;
					default:
						return ERR_ERRNO;
				}
			}
		}
	}
	return 0;
}

/**********************************************************************
* 函数名称: // static void handle_pipe(int Sig)

* 功能描述: // 尝试从Socket套接字缓冲区读取count字节数据
* 访问的表: //
* 修改的表: //
* 输入参数: // int Sig 当前系统产生的信号编号

* 输出参数: // 
* 返 回 值: // 
* 其它说明: // 其它说明
* 修改日期       修改人	     修改内容
* -----------------------------------------------
* 2021/11/02	  	    XXXX	      XXXX
***********************************************************************/
static void handle_pipe(int Sig)
{
	printf("Handle the Disconnected Error Sig!\n"); // 捕捉信号不做任何处理, 只打印
}

/**********************************************************************
* 函数名称: // static void socket_sig_deal(void)

* 功能描述: // 尝试从Socket套接字缓冲区读取count字节数据
* 访问的表: //
* 修改的表: //
* 输入参数: // 

* 输出参数: // 
* 返 回 值: // 
* 其它说明: // 其它说明
* 修改日期       修改人	     修改内容
* -----------------------------------------------
* 2021/11/02	  	    XXXX	      XXXX
***********************************************************************/
static void socket_sig_deal(void)
{
    struct sigaction sa;
	sigemptyset(&sa.sa_mask); // 设定信号处理过程中被屏蔽的阻塞信号
	sa.sa_handler = handle_pipe; // 设定信号处理函数地址, 一般情况下sa_handler只传入信号量值, 不携带其他信号相关信息
	sa.sa_flags = 0; // 标志位设定, 配合sa_handler/sa_sigaction不同的处理函数使用, 需要传入其他参数进入处理函数时需要选择sa.sa_sigaction + SA_SIGINFO
	sigaction(SIGPIPE,&sa,NULL); // SIGPIPE 管道断开信号, 产生的 error = EPIPE, 不做捕获处理会导致程序奔溃
}

/**********************************************************************
* 函数名称: // static int net__socket_established(int iSock)

* 功能描述: // 尝试从Socket套接字缓冲区读取count字节数据
* 访问的表: //
* 修改的表: //
* 输入参数: // int iSock 待检测的socket句柄

* 输出参数: // 
* 返 回 值: // int: 0表示传输建立稳定 -2表示传输建立断开
* 其它说明: // 其它说明
* 修改日期       修改人	     修改内容
* -----------------------------------------------
* 2021/11/02	  	    XXXX	      XXXX
***********************************************************************/
static int net__socket_established(int iSock)
{
    struct tcp_info info; // 创建tcp_info结构体用来存储tcp状态信息
    int len = sizeof(info); 
    getsockopt(iSock, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len); // 获取TCP当前状态信息
    if ((info.tcpi_state == TCP_ESTABLISHED)) // 判断当前TCP是否处在建立连接状态
	{
        return SUCCESS; // 返回连接状态
    }
	else
	{
        return ERR_CONN_LOST; // 返回失联状态
    }
}

2. 客户端代码(Client.c)

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#define Debug 1
#define BUF_SIZE 12800

/**********************************************************************
* 函数名称: // static ssize_t net__read(int sock,void *buf, size_t count)

* 功能描述: // 尝试从Socket套接字缓冲区读取count字节数据
* 访问的表: //
* 修改的表: //
* 输入参数: // int sock 可用的socket句柄
* 输入参数: // void *buf 用于存储读取得到的字节(一般为char)
* 输入参数: // size_t count 期望读取到的字节总数, 此值需要小于等于buf空间大小/sizeof(char) >= count

* 输出参数: // 
* 返 回 值: // ssize_t: 表示实际读取到的字节数
* 其它说明: // 其它说明
* 修改日期       修改人	     修改内容
* -----------------------------------------------
* 2021/11/02	  	    XXXX	      XXXX
***********************************************************************/
ssize_t net__read(int sock,void *buf, size_t count);

/**********************************************************************
* 函数名称: // static int packet__read(int sock,const void *buf, size_t count)

* 功能描述: // 尝试从Socket套接字缓冲区读取count字节数据, 读取到count字节数据为止
* 访问的表: //
* 修改的表: //
* 输入参数: // int sock 可用的socket句柄
* 输入参数: // void *buf 用于存储读取得到的字节(一般为char)
* 输入参数: // size_t count 期望读取到的字节总数, 此值需要小于等于buf空间大小/sizeof(char) >= count

* 输出参数: // 
* 返 回 值: // int: 表示函数执行的情况
* 其它说明: // 其它说明
* 修改日期       修改人	     修改内容
* -----------------------------------------------
* 2021/11/02	  	    XXXX	      XXXX
***********************************************************************/
int packet__read(int sock,const void *buf, size_t count);


int
main(int argc, char *argv[])
{
    struct addrinfo hints;
    struct addrinfo *result, *rp;
    int sfd, s, j;
    size_t len;
    ssize_t nread;
    char buf[BUF_SIZE];

    if (argc < 2) {
        fprintf(stderr, "Usage: %s host port msg...\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* Obtain address(es) matching host/port. */

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;    /* Allow IPv4 or IPv6 */
    hints.ai_socktype = SOCK_STREAM; /* Datagram socket */
    hints.ai_flags = AI_PASSIVE;
    hints.ai_protocol = 0;          /* Any protocol */

    s = getaddrinfo(argv[1], argv[2], &hints, &result);
    if (s != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
        exit(EXIT_FAILURE);
    }

    /* getaddrinfo() returns a list of address structures.
        Try each address until we successfully connect(2).
        If socket(2) (or connect(2)) fails, we (close the socket
        and) try the next address. */

    for (rp = result; rp != NULL; rp = rp->ai_next) {
        sfd = socket(rp->ai_family, rp->ai_socktype,
                    rp->ai_protocol);
        if (sfd == -1)
            continue;

        if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1){
            struct sockaddr_in * temp;
             printf("temp=NULL\n");
             if( rp->ai_addr == NULL )
             {
                 printf("addr is null\n");
             }
             else
             {  
                temp = (struct sockaddr_in *)(rp->ai_addr);
                //  char *ip = inet_ntoa(temp.sin_addr);
                 printf("%s\n",inet_ntoa(temp->sin_addr));
             }  
             printf("temp=NULL********\n");
            break;                  /* Success */
        }

        close(sfd);
    }

    freeaddrinfo(result);           /* No longer needed */

    if (rp == NULL) {               /* No address succeeded */
        fprintf(stderr, "Could not connect\n");
        exit(EXIT_FAILURE);
    }

    /* Send remaining command-line arguments as separate
        datagrams, and read responses from server. */

	unsigned int i = 1;
	int flag = 0;

    for (;;i++) {
#if Debug
		printf("iHeader:\n");
#endif
		flag = packet__read(sfd,buf, 4); // iHeader
		if(flag == 1){
#if Debug
			for(j=0;j<4;j++){
				printf("%d ", buf[j]);
			}
			printf("\n");
#endif
		}else
		{
			continue;
		}

#if Debug
		printf("iReserveS1-iProfileSEQ-iReserveS2-iEncoderVal-iReserveS3-iReserveS3-iReserveS3:\n");
#endif
		flag = packet__read(sfd,buf, 28); // iReserveS1-iProfileSEQ-iReserveS2-iEncoderVal-iReserveS3-iReserveS3-iReserveS3
		if(flag == 1){
#if Debug
			for(j=0;j<28;j++){
				printf("%d ", buf[j]);
			}
			printf("\n");
#endif
		}else
		{
			continue;
		}

#if Debug
		printf("piProfileData:\n");
#endif
		flag = packet__read(sfd,buf, 12800); // piProfileData 3200*4Bytes = 12800 Bytes
		if(flag == 1){
#if Debug
			for(j=0;j<12800;j++){
				printf("%d ", buf[j]);
			}
			printf("\n");
#endif
		}else
		{
			continue;
		}

#if Debug
		printf("iPacketEnd:\n");
#endif
		flag = packet__read(sfd,buf, 4); // iPacketEnd
		if(flag == 1){
#if Debug
			for(j=0;j<4;j++){
				printf("%d ", buf[j]);
			}
			printf("\n");
#endif
		}else
		{
			continue;
		}
#if Debug
		printf("######################################### Line[%d] #########################################\n",i);
#endif
    }

    exit(EXIT_SUCCESS);
}

ssize_t net__read(int sock,void *buf, size_t count)
{
	return recv(sock, buf, count, 0);
}

int packet__read(int sock,const void *buf, size_t count)
{
	ssize_t read_length;
	char *data = (char *)buf;
	int pos=0;
	int to_process = count;
	while(to_process > 0)
	{
		read_length = net__read(sock,&data[pos],to_process);
		if(read_length > 0){
			to_process -= read_length;
			pos += read_length;
		}
		else
		{
#if Debug
			// printf("Current Status: To_Process(%d)-Write_Len(%d)\n",to_process,read_length);
			// return -1; // 异常处理
#endif
		}
	}
	return 1;
}

五、高速通信代码测试

1. 编译及运行

gcc -o MS MyServer.c
gcc -o MC MyClient.c
./MS
./MC 192.168.1.110 50000

2. 测试结果如下:

Server服务端:
Opening ipv4 listen socket on port 50000.
ERR:net__socket_listen-177
####################################################### sock_ok=4
packet__write successful.

Client客户端:
21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 
iPacketEnd:
0 0 0 0 
######################################### Line[11] #########################################
iHeader:

Reference

怎样实时判断socket连接状态?:https://www.cnblogs.com/embedded-linux/p/7468442.html
SOCKET:SO_LINGER 选项:https://www.cnblogs.com/kuliuheng/p/3670353.html
Linux SIGPIPE信号产生原因与解决方法:https://blog.csdn.net/u010821666/article/details/81841755

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章