流协议与粘包:网络
咱们知道TCP是一个基于字节流的传输服务,这意味着TCP所传输的数据之间是无边界的,像流水同样,是没法区分边界的;而UDP是基于消息的传输服务,它传输的是数据报文,是有边界的。socket
而对于数据之间有无边界,反映在对方接收程序的时候,是不同的:对于TCP字节流来讲,对等方在接收数据的时候,不可以保证一次读操做,可以返回多少个字节,是一个消息,仍是二个消息,这些都是不肯定的;而对于UDP消息服务来讲,它可以保证对等方一次读操做返回的是一条消息。函数
因为TCP的无边界性,就会产生粘包问题,那粘包问题具体体现是怎样的呢?下面用图来进行阐述:spa
假设主机A(Host A)要向主机B(Host B)发送两个数据包:M1,M23d
而对于对待接收方主机B来讲,可能会有如下几种状况:code
①server
也就是第一次读操做恰好返回第一条消息(M1)的所有,接下来第二次读操做返回第二条消息(M2)的所有,因此这就没有粘包问题。blog
②接口
一次读操做就返回了M1,M2的全部,这样M1和M2就粘在一块儿了,这就能比较直观的体会到粘包的表现了。ip
③
一次读操做返回了M1的所有,而且还有M2的一部分(m2_1);第二次读操做返回了M2的另一部分(M2_2)。
④
一次读操做返回了M1的一部分(M1_1);第二次读操做返回了M1的另一部分(M1_2),而且还有M2的所有。
固然除了上面四种状况,可能还存在其它组合,由于主机B一次能接收的字节数是不肯定的。
下面来探讨下产生的缘由。
粘包产生的缘由
① 应用程要将本身缓冲区中的数据发送出去,首先要调用一个write方法,将应用程序的缓冲区的数据拷贝到套接口发送缓冲区(SO_SNDBUF),而该缓冲区有一个SO_SNDBUF大小的限制,若是应用缓冲区一条消息的大小超过了SO_SNDBUF的大小,那这时候就有可能产生粘包问题,由于消息被分隔了,一部分已经发送给发送缓冲区,且对方已经接收到了,另一部分才放到了发送缓冲区,这样对方就延迟接收了消息的后一部分。这就致使了粘包问题的出现。
②TCP传输的段有最大段(MSS)的限制,因此也会对应用发送的消息进行分割而产生粘包问题。
③链路层它所传输的数据有一个最大传输单元(MTU)的限制,若是咱们所发送的数据包超过了最大传输单元,会在IP层进行分组,这也可能致使消息的分割,因此也有可能出现粘包问题。
固然还有其它缘由,如TCP的流量控制、拥塞控制、TCP的延迟发送机制,对于上面说的理论理解起来比较抽象,只要记住一条:TCP会产生粘包问题既可。
粘包解决方案:
怎么才能解决粘包问题呢?
既然TCP协议没有在传输层没有维护消息与消息之间的边界,因此:
咱们所要发送的消息是一个定长包,那么对等方在接收的时候已定长的方式来进行接收,就能确保消息与消息之间的边界。
这种方式有个问题,就是若是消息自己就带这些字符的话,就没法就没法区分消息的边界了,这时就须要用到转义字符了。
其中包头是定长的,如4个字节。
这些解决方案有一个很重要的问题,就是定长包的接收,咱们以前说了,TCP是一个流协议,它不能保证对方一次接收接收到了多少个字节,那咱们就须要封装一个函数:接收肯定字节数的读操做。
下面简单介绍一下两种方法的实现
server.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0); ssize_t readn(int fd, void *buf, size_t count)//读取count个字节数,其中size_t是无符号 的整数,ssize_t是有符号的整数 { size_t nleft = count;//剩余的字节数 printf("nleft = %d\n",nleft); ssize_t nread;//已接收的字节数 char *bufp = (char*)buf; while (nleft > 0) {//因为不能保证一次读操做可以返回字节数是多少,因此须要进行循环来接收 if ((nread = read(fd, bufp, nleft)) < 0) { if (errno == EINTR)//被信号中断了,则继续执行,由于不是出错 continue; return -1;//表示读取失败了 } else if (nread == 0)//对等方关闭了 return count - nleft;//返回已经读取的字节数 bufp += nread; nleft -= nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft = count; ssize_t nwritten; char *bufp = (char*)buf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nwritten == 0)//若是是这种状况,则表示什么都没发生,继续还得执行 continue; bufp += nwritten; nleft -= nwritten; } return count; } void do_service(int conn) { char recvbuf[1024]; while(1) { memset(recvbuf, 0, sizeof(recvbuf)); int ret=readn(conn,recvbuf,sizeof(recvbuf)); if(ret == 0) { printf("client close\n"); break; } else if(ret == -1) { ERR_EXIT("read"); } fputs(recvbuf,stdout); writen(conn,recvbuf,ret); } } int main(void) { int listenfd; if((listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0) { ERR_EXIT("socket"); } 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); /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/ /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/ //地址重用 int on=1; if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0) { ERR_EXIT("setsockopt"); } if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) { ERR_EXIT("bind"); } if(listen(listenfd,SOMAXCONN) < 0) { ERR_EXIT("listen"); } struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int confd; pid_t pid; while(1) { if((confd = accept(listenfd,(struct sockaddr*)&peeraddr, &peerlen)) < 0) { ERR_EXIT("accept"); } printf("ip = %s, port = %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peer addr.sin_port)); pid = fork(); if(pid == -1) { ERR_EXIT("fork"); } if(pid == 0) { close(listenfd); do_service(confd); exit(EXIT_SUCCESS); } else { close(confd); } } return 0; }
client.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0); ssize_t readn(int fd, void *buf, size_t count)//须要将函数的定义也挪过来 { size_t nleft = count; ssize_t nread; char *bufp = (char*)buf; while (nleft > 0) { if ((nread = read(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nread == 0) return count - nleft; bufp += nread; nleft -= nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft = count; ssize_t nwritten; char *bufp = (char*)buf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nwritten == 0) continue; bufp += nwritten; nleft -= nwritten; } return count; } int main(void) { int sockfd; if((sockfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0) { ERR_EXIT("socket"); } struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/ if (connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) { ERR_EXIT("connect"); } char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL) { writen(sockfd,sendbuf,sizeof(sendbuf)); readn(sockfd,recvbuf,sizeof(recvbuf)); fputs(recvbuf,stdout); memset(sendbuf,0,sizeof(sendbuf)); memset(recvbuf,0,sizeof(recvbuf)); } close(sockfd); return 0; }
Makefile
.PHONY: clean all CC=gcc CFLAGE= -G -Wall BIN=server client all:$(BIN) %.o:%.c $(CC) $(cflags) -C $< -O $@ clean: rm -f *.o $(BIN)
每次发送都是1024定长的字节,若是只发送几个字节的内容也会占用这么多字节,这就会增长网络的负担
server.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0); struct packet { int len; char buf[1024]; }; ssize_t readn(int fd, void *buf, size_t count)//读取count个字节数,其中size_t是无符号 的整数,ssize_t是有符号的整数 { size_t nleft = count;//剩余的字节数 printf("nleft = %d\n",nleft); ssize_t nread;//已接收的字节数 char *bufp = (char*)buf; while (nleft > 0) {//因为不能保证一次读操做可以返回字节数是多少,因此须要进行循环来接收 if ((nread = read(fd, bufp, nleft)) < 0) { if (errno == EINTR)//被信号中断了,则继续执行,由于不是出错 continue; return -1;//表示读取失败了 } else if (nread == 0)//对等方关闭了 return count - nleft;//返回已经读取的字节数 bufp += nread; nleft -= nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft = count; ssize_t nwritten; char *bufp = (char*)buf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) < 0) if (errno == EINTR) continue; return -1; } else if (nwritten == 0)//若是是这种状况,则表示什么都没发生,继续还得执行 continue; bufp += nwritten; nleft -= nwritten; } return count; } void do_service(int conn) { //char recvbuf[1024]; struct packet recvbuf; int n; while(1) { memset(&recvbuf, 0, sizeof(recvbuf)); int ret=readn(conn,&recvbuf.len,4); if(ret == -1) { ERR_EXIT("read"); } else if(ret < 4) { printf("client close\n"); break; } n = ntohl(recvbuf.len); ret = readn(conn,recvbuf.buf,n); if(ret == -1) { ERR_EXIT("read"); } else if(ret < n) { printf("client close\n"); break; } fputs(recvbuf.buf,stdout); writen(conn,&recvbuf,4+n); } } int main(void) { int listenfd; if((listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0) { ERR_EXIT("socket"); } 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); /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/ /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/ //地址重用 int on=1; if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0) { ERR_EXIT("setsockopt"); } if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) { ERR_EXIT("bind"); } if(listen(listenfd,SOMAXCONN) < 0) { ERR_EXIT("listen"); } struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int confd; pid_t pid; while(1) { if((confd = accept(listenfd,(struct sockaddr*)&peeraddr, &peerlen)) < 0) { ERR_EXIT("accept"); } printf("ip = %s, port = %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peer addr.sin_port)); pid = fork(); if(pid == -1) { ERR_EXIT("fork"); } if(pid == 0) { close(listenfd); do_service(confd); exit(EXIT_SUCCESS); } else { close(confd); } } return 0; }
client.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0); struct packet { int len; char buf[1024]; }; ssize_t readn(int fd, void *buf, size_t count)//须要将函数的定义也挪过来 { size_t nleft = count; ssize_t nread; char *bufp = (char*)buf; while (nleft > 0) { if ((nread = read(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nread == 0) return count - nleft; bufp += nread; nleft -= nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft = count; ssize_t nwritten; char *bufp = (char*)buf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nwritten == 0) continue; bufp += nwritten; nleft -= nwritten; } return count; } int main(void) { int sockfd; if((sockfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0) { ERR_EXIT("socket"); } struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/ if (connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) { ERR_EXIT("connect"); } //char sendbuf[1024] = {0}; //char recvbuf[1024] = {0}; struct packet sendbuf; struct packet recvbuf; memset(&sendbuf,0,sizeof(sendbuf)); memset(&recvbuf,0,sizeof(recvbuf)); int n; while(fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin) != NULL) { //writen(sockfd,sendbuf,sizeof(sendbuf)); //readn(sockfd,recvbuf,sizeof(recvbuf)); n =strlen(sendbuf.buf); sendbuf.len = htonl(n);//网络字节序 writen(sockfd,&sendbuf,4+n); int ret = readn(sockfd,&recvbuf.len,4); if(ret == -1) { ERR_EXIT("read"); } else if(ret < 4) { printf("client close\n"); break; } n = ntohl(recvbuf.len); ret = readn(sockfd,recvbuf.buf,n); if(ret == -1) { ERR_EXIT("read"); } else if(ret < n) { printf("client close\n"); break; } fputs(recvbuf.buf,stdout); memset(&sendbuf,0,sizeof(sendbuf)); memset(&recvbuf,0,sizeof(recvbuf)); } close(sockfd); return 0; }
Makefile
.PHONY: clean all CC=gcc CFLAGE= -G -Wall BIN=server client all:$(BIN) %.o:%.c $(CC) $(cflags) -C $< -O $@ clean: rm -f *.o $(BIN)
这样就很好的解决了粘包问题,在局域网中是不可能出现粘包问题的,可是若是将程序放到广域网,若是不处理粘包问题会存在很大问题的。