TCP/IP协议是一种流协议,流协议是字节流,只有开始和结束,包与包之间没有边界,所以容易产生粘包,但是不会丢包。 UDP/IP协议是数据报,有边界,不存在粘包,但是可能丢包。
产生粘包问题的原因 1.SQ_SNDBUF套接字本身有缓冲区(发送缓冲区,接收缓冲区) 2.tcp传送的网络数据最大值MSS大小限制 3.链路层也有MTU(最大传输单元)大小限制,如果数据包大于>MTU要在IP层进行分片,导致消息分割。(可以简单的认为MTU是MSS加包头数据) 4.tcp的流量控制和拥塞控制,也可能导致粘包 5.tacp延迟发送机制等等 结论:TCP/IP协议,在传输层没有处理粘包问题,必须由程序员处理
粘包的解决方案--本质上是要在应用层维护消息与消息的边界 1.定包长 2.包尾加\r\n(比如ftp协议) 3.包头加上包体长度 4.更复杂的应用层协议
粘包的几种状态
//粘包解决方案--包头加上包体长度 //服务器 #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>typedef struct _packet {int len; //定义包体长度char buf[1024]; //定义包体 } Packet;/** fd:文件描述符* buf:数据缓存区* count:读取字符数* */ 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; }int main(int arg, char *args[]) {//create socketint listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd == -1){perror("socket() err");return -1;}//reuseaddrint optval = 1;if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))== -1){perror("setsockopt() err");return -1;}//bindstruct 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;}//listenif(listen(listenfd,SOMAXCONN)==-1){perror("listen() err");return -1;}//acceptstruct sockaddr_in peeraddr;socklen_t peerlen = sizeof(peeraddr);int conn = accept(listenfd, (struct sockaddr *)&peeraddr,&peerlen);if (conn == -1){perror("accept() err");return -1;}Packet _packet;while (1){memset(&_packet, 0, sizeof(_packet));//获取报文自定义包头int rc = readn(conn, &_packet.len, 4);if (rc == -1){exit(0);} else if (rc < 4){exit(0);}//把网络字节序转化成本地字节序int n = ntohl(_packet.len);//获取报文自定义包体rc = readn(conn, _packet.buf, n);if (rc == -1){exit(0);} else if (rc < n){exit(0);}//打印报文数据 fputs(_packet.buf, stdout);//将原来的报文数据发送回去printf("发送报文的长度%d\n", 4 + n);rc = writen(conn, &_packet, 4 + n);if (rc == -1){exit(0);} else if (rc < 4 + n){exit(0);}}return 0; }
//粘包解决方案--包头加上包体长度 //客户端 #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>typedef struct _packet {int len; //定义包体长度char buf[1024]; //定义包体 } Packet;/** fd:文件描述符* buf:数据缓存区* count:读取字符数* */ 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; }int main(int arg, char *args[]) {//create socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1){perror("socket() err");return -1;}//connectstruct 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, numx = 0;Packet _packet;memset(&_packet, 0, sizeof(_packet));while (fgets(_packet.buf, sizeof(_packet.buf), stdin) != NULL){//发送数据numx = strlen(_packet.buf);//将本地字节转化成网络字节序_packet.len = htonl(numx);rc = writen(sockfd, &_packet, 4 + numx);if (rc == -1){return -1;} else if (rc < 4 + numx){return -1;}//接收数据memset(&_packet, 0, sizeof(_packet));//获取包头rc = readn(sockfd, &_packet.len, 4);if (rc == -1){return -1;} else if (rc < 4){return -1;}//将网络字节转化成本地字节numx = ntohl(_packet.len);//printf("接收数据的大小是%d\n",numx);//获取包体rc = readn(sockfd, &_packet.buf, numx);if (rc == -1){return -1;} else if (rc < numx){return -1;}//打印包体 fputs(_packet.buf,stdout);memset(&_packet, 0, sizeof(_packet));}return 0; }