Linux 网络编程详解五(TCP/IP协议粘包解决方案二)
ssize_t recv(int s, void *buf, size_t len, int flags); --与read相比,只能用于网络套接字文件描述符 --当flags参数的值设置为MSG_PEEK时,recv可以从socket缓存中读取数据,但是不会将缓存中该部分数据清除 使用read函数直接读取socket缓存区中的内容,会清空缓存区中的内容。假设两段报文粘包,read会清空缓存 区中所有内容,从而导致后一段报文中的粘包的部分数据丢失
--强调:粘包解决方案包尾加\n,必须使用recv()函数,并且设置参数flags的值是MSG_PEEK
//粘包解决方案--包尾加\r\n //服务器 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> /* * 思想,客户端发送报文时,每段报文都以\n结尾,服务端先用recv函数中的flag参数查看缓存区中的数据 * 以\n分割每个报文 * 注意,包尾加\r\n这个方案和包头加上包体长度方案最大的区别是包尾加\r\n这个方案并不清楚每次的包体长度是多少, * 而包头加上包体长度方案确定确定包体的长度 * */ ssize_t readn(int fd, const void * buf, ssize_t count) { if (buf == NULL || fd < 0) { printf("readn() params not allow NULL!\n"); return -1; } //定义剩余字符数 ssize_t lread = count; //定义每次读取字符个数 ssize_t nread = 0; //定义字符串移动指针 char *pbuf = (char *) buf; while (lread > 0) { nread = read(fd, pbuf, lread); if (nread == -1) { //屏蔽信号 if (errno == EINTR) continue; return -1; } else if (nread == 0) { printf("client is closed !\n"); return count - lread; } //重置剩余字节数 lread -= nread; //指针后移 pbuf += nread; } return count; } ssize_t writen(int fd, const void * buf, ssize_t count) { if (buf == NULL || fd < 0) { printf("writen() params not allow NULL!\n"); return -1; } //定义剩余字符数 ssize_t lread = count; //定义每次写入字符数 ssize_t nread = 0; //定义临时指针变量--假设pbuf的大小大于网络传输字符串的长度 char * pbuf = (char *) buf; while (lread > 0) { nread = write(fd, pbuf, lread); if (nread == -1) { //屏蔽信号 if (errno == EINTR) continue; return -1; } else if (nread == 0) { printf("client is closed !\n"); return count - lread; } //重置剩余字节数 lread -= nread; pbuf += nread; } return count; } ssize_t recv_peek(int fd, const void * buf, ssize_t count) { int ret = 0; while (1) { /* * 当recv中flags参数的值是MSG_PEEK时, * recv函数会将socket缓存区中的数据读取到内存,并且不会清空socket缓存区(read()获取recv()不加参数时读取完数据后,会清空缓存) * 遍历缓存区中的数据,找到\n,分割报文数据 * */ ret = recv(fd,(void *)buf, count, MSG_PEEK); if (ret == -1 && errno == EINTR) { continue; } return ret; } return -1; } //读取缓存区中以\n结尾的字符串 ssize_t mreadline(int fd, const void *buf, ssize_t count) { //定义临时指针变量 char *pbuf = (char *) buf; //定义自定义buf剩余的字节数 ssize_t lread = count; //定义recv每次使用的字节数 ssize_t nread = 0; int ret = 0, i = 0; while (1) { ret = recv_peek(fd, pbuf, lread); if (ret < 0) { printf("recv_peek() failed !\n"); return -1; } else if (ret == 0) { printf("client is closed !\n"); return -1; } nread = ret; //遍历读取到的数据 for (i = 0; i < ret; i++) { if (pbuf[i] == '\n') { //清空缓存区 memset(pbuf, 0, lread); ret = readn(fd, pbuf, i + 1); if (ret != i + 1) return -1; //返回已经读取到的数据 return ret; } } //如果没有读到\n,需要判断自定义buf是否还有空间--这种情况是一段报文被分割在多个包体中发送 //recv()函数的返回值只可能小于或者等于count if (nread == lread) { printf("自定义缓存buf的长度太小!\n"); return -1; } //说明自定义buf还有空间,可以再接收一次 //为了读取下一段报文,需要先把socket缓冲区数据全部读取完成 ret = readn(fd, pbuf, nread); if (ret != nread) return -1; lread -= nread; pbuf += nread; } return -1; } int main(int arg, char *args[]) { //create socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { perror("socket() err"); return -1; } //reuseaddr int optval = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) { perror("setsockopt() err"); return -1; } //bind struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8080); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (bind(listenfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) { perror("bind() err"); return -1; } //listen if (listen(listenfd, SOMAXCONN) == -1) { perror("listen() err"); return -1; } //accept struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int conn = accept(listenfd, (struct sockaddr *) &peeraddr, &peerlen); if (conn == -1) { perror("accept() err"); return -1; } char buf[1024] = { 0 }; while (1) { //获取一段报文 int rc = mreadline(conn, buf, 1024); if (rc == -1) { exit(0); } //打印报文数据 fputs(buf, stdout); //将原来的报文数据发送回去 rc = writen(conn, buf, strlen(buf)); if (rc == -1) { exit(0); } } return 0; }
//粘包解决方案--包尾加\r\n //客户端 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> ssize_t readn(int fd, const void *buf, ssize_t count) { //定义临时指针变量 char *pbuf = (char *)buf; //定义每次已读数据 ssize_t nread = 0; //定义剩余数据 ssize_t lread = count; while (lread > 0) { nread = read(fd, pbuf, lread); /* * 情况分析:假设b缓冲区buf足够大 * 如果nread==count,说明数据正好被读完 * nread<count,说明数据没有被读完,这种情况就是由于粘包产生的 * socket只接收了数据的一部分,TCP/IP协议不可能出现丢包情况 * nread==0,说明对方关闭文件描述符 * nread==-1,说明read函数报错 * nread>count,这种情况不可能存在 * */ if (nread == -1) { //read()属于可中断睡眠函数,需要做信号处理 if (errno == EINTR) continue; perror("read() err"); return -1; } else if (nread == 0) { printf("client is closed !\n"); //返回已经读取的字节数 return count - lread; } //重新获取 剩余的 需要读取的 字节数 lread = lread - nread; //指针后移 pbuf = pbuf + nread; } return count; } /* fd:文件描述符 * buf:数据缓存区 * count:读取字符数 * */ ssize_t writen(int fd, const void *buf, ssize_t count) { //定义临时指针变量 char *pbuf = (char *)buf; //每次写入字节数 ssize_t nwrite = 0; //剩余未写字节数 ssize_t lwrite = count; while (lwrite > 0) { nwrite = write(fd, pbuf, lwrite); if (nwrite == -1) { if (errno == EINTR) continue; perror("write() err"); return -1; } else if (nwrite == 0) { printf("client is closed !\n"); //对方关闭文件描述符,返回已经写完的字节数 return count - lwrite; } lwrite -= nwrite; pbuf += nwrite; } return count; } ssize_t recv_peek(int fd, const void * buf, ssize_t count) { int ret = 0; while (1) { /* * 当recv中flags参数的值是MSG_PEEK时, * recv函数会将socket缓存区中的数据读取到内存,并且不会清空socket缓存区(read()获取recv()不加参数时读取完数据后,会清空缓存) * 遍历缓存区中的数据,找到\n,分割报文数据 * */ ret = recv(fd, (void *)buf, count, MSG_PEEK); if (ret == -1 && errno == EINTR) { continue; } return ret; } return -1; } //读取缓存区中以\n结尾的字符串 ssize_t mreadall(int fd, const void *buf, ssize_t count) { //定义临时指针变量 char *pbuf = (char *) buf; //定义自定义buf剩余的字节数 ssize_t lread = count; //定义recv每次使用的字节数 ssize_t nread = 0; int ret = 0, i = 0; while (1) { ret = recv_peek(fd, pbuf, lread); if (ret < 0) { printf("recv_peek() failed !\n"); return -1; } else if (ret == 0) { printf("client is closed !\n"); return -1; } nread = ret; //遍历读取到的数据 for (i = 0; i < ret; i++) { if (pbuf[i] == '\n') { //清空缓存区 memset(pbuf, 0, lread); ret = readn(fd, pbuf, i + 1); if (ret != i + 1) return -1; //返回已经读取到的数据 return ret; } } //如果没有读到\n,需要判断自定义buf是否还有空间--这种情况是一段报文被分割在多个包体中发送 //recv()函数的返回值只可能小于或者等于count if (nread == lread) { printf("自定义缓存buf的长度太小!\n"); return -1; } //说明自定义buf还有空间,可以再接收一次 //为了读取下一段报文,需要先把socket缓冲区数据全部读取完成 ret = readn(fd, pbuf, nread); if (ret != nread) return -1; lread -= nread; pbuf += nread; } return -1; } int main(int arg, char *args[]) { //create socket int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket() err"); return -1; } //connect struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8080); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sockfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) { perror("connect() err"); return -1; } int rc = 0; char buf[1024]={0}; while (fgets(buf, sizeof(buf), stdin) != NULL) { //发送数据 rc=writen(sockfd,buf,strlen(buf)); if (rc != strlen(buf)) { return -1; } //接收数据 memset(buf, 0, sizeof(buf)); rc = mreadall(sockfd,buf,sizeof(buf)); if(rc==-1) { return -1; } //打印包体 fputs(buf,stdout); memset(buf, 0, sizeof(buf)); } return 0; }