发送定长宝解决网络粘包问题

来源:互联网 发布:淘宝买书有什么好店铺 编辑:程序博客网 时间:2024/06/05 17:05
产生粘包问题的原因有以下几个:
• 第一 。应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。
• 第二种情况是,TCP所传输的报文段有MSS的限制,如果套接字缓冲区的大小大于MSS,也会导致消息的分割发送。
• 第三种情况由于链路层最大发送单元MTU,在IP层会进行数据的分片。

这些情况都会导致一个完整的应用层数据被分割成多次发送,导致接收对等方不是按完整数据包的方式来接收数据。

粘包的问题的解决思路
粘包问题的最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪。我们通过使用某种方案给出边界,例如:
[1] 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。
[2] 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。
[3] 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。
[4] 使用更加复杂的应用层协议。
解决方案
[1] 粘包解决方案一:使用定长包
[2] 粘包解决方案二:使用结构体,显式说明数据部分的长度
在这个方案中,我们需要定义一个‘struct packet’包结构,结构中指明数据部分的长度,用四个字节来表示。
[3] 粘包解决方案三:按行读取
ftp协议采用/r/n来识别一个消息的边界,我们在这里实现一个按行读取的功能,该功能能够按/n来识别消息的边界。这里介绍一个函数:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
与read函数相比,recv函数的区别在于两点:
recv函数只能够用于套接口IO。
recv函数含有flags参数,可以指定一些选项。
recv函数的flags参数常用的选项是:
MSG_OOB 接收带外数据,即通过紧急指针发送的数据
MSG_PEEK 从缓冲区中读取数据,但并不从缓冲区中清除所读数据
为了实现按行读取,我们需要使用recv函数的MSG_PEEK选项。PEEK的意思是"偷看",我们可以理解为窥视,看看socket的缓冲区内是否有某种内容,而清除缓冲区。

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<sys/socket.h>#include<errno.h>#include<netinet/in.h>#include<arpa/inet.h>#include<ctype.h>ssize_t readn(int fd, void *buf, size_t count){ssize_t nleft = count;ssize_t nread = 0;char *bufp = (char*)buf;while (nleft > 0){if ((nread = readn(fd, bufp, nleft)) < 0){if (errno == EINTR)continue;else return -1;}else if (nread == 0)return count - nleft;bufp = bufp + nread;nleft = nleft - nread;}return count;}ssize_t writen(int fd, const void *buf, size_t count){ssize_t nleft = count;ssize_t nwrite = 0;char *bufp = (char*)buf;while (nleft > 0){if ((nwrite = writen(fd, bufp, nleft)) < 0){if (errno == EINTR)continue;else return -1;}else if (nwrite == 0)continue;bufp = bufp + nwrite;nleft = nleft - nwrite;}return count;}void do_service(int connfd){int MAX = 128;char recvbuf[MAX];while (1){memset(recvbuf, 0, MAX);int n = read(connfd, recvbuf, MAX);if (n == 0){printf("client close\n");break;}fputs(recvbuf, stdout);for (int i = 0; i<n; i++)recvbuf[i] = toupper(recvbuf[i]);write(connfd, recvbuf, n);}}int main(){int listenfd;listenfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(5188);servaddr.sin_addr.s_addr = htonl(INADDR_ANY);int on = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));listen(listenfd, 20);while (1)                                                   {struct sockaddr_in cliaddr;socklen_t cliaddr_len;int connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len);printf("ip=%s port=%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));pid_t pid = fork();if (pid < 0){printf("error\n");}if (pid == 0){close(listenfd);do_service(connfd);exit(EXIT_SUCCESS);}else if (pid>0){close(connfd);}}close(listenfd);}
客户端同理,但是需要注意的是每次发送的时候,writen(connfd,buf,sizeof(buf)),此时不能写成strlen(buf),否则对方的读端会一直阻塞,该方案就是每次发送128个字节,尽管实际数据可能只有12个字节,因此该方案会造成数据量过大,可能会阻塞网络。