粘包问题:应用层要发送数据,需要调用write函数将数据发送到套接口发送缓冲区。如果应用层数据大小大于SO_SNDBUF,
那么,可能产生这样一种情况,应用层的数据一部分已经被发送了,还有一部分还在套接口缓冲区待发送。此时,对方延迟接收,就容易产生粘包。
另一方面,TCP传输有MSS限制,也会对数据进行分割。第三个原因,由于MTU存在,也可能分割数据。都会产生粘包问题
粘包问题解决方案:本质上是要在应用层维护消息与消息的边界。
1、定长包
2、包尾加 (FTP协议)
3、包头加上包体长度
4、更加复杂的应用层协议
利用发送定常包解决粘包问题时,对于定长包的接收,是一个主要问题,在程序中,封装了readn(接收确切数目的读操作)与writen(发送。。。)函数来解决这个问题。
定长包发送程序:
1 /* 2 客户端程序中发送定长包解决粘包问题: 3 */ 4 #include<unistd.h> 5 #include<sys/types.h> 6 #include<sys/socket.h> 7 #include<string.h> 8 #include<stdlib.h> 9 #include<stdio.h> 10 #include<errno.h> 11 #include<netinet/in.h> 12 #include<arpa/inet.h> 13 #include<signal.h> 14 #define ERR_EXIT(m) 15 do 16 { 17 perror(m); 18 exit(EXIT_FAILURE); 19 }while(0) 20 struct packet 21 { 22 int len;//包头 23 char buf[1024];//包体 24 }; 25 //接收确切数目的读操作 26 ssize_t readn(int fd,void *buf,size_t count) 27 { 28 size_t nleft=count; 29 ssize_t nread; 30 char *bufp=(char*)buf; 31 //剩余字节数大于0就循环 32 while(nleft>0) 33 { 34 if((nread=read(fd,bufp,nleft))<0) 35 { 36 if(errno==EINTR) 37 continue; //被信号中断 38 else 39 return -1;//失败 40 } 41 //对等方关闭了 42 else if(nread==0) 43 return (count-nleft);//已经读取的字节数 44 bufp+=nread; 45 nleft-=nread; 46 } 47 return count; 48 } 49 //发送确切数目的写操作 50 ssize_t writen(int fd, const void *buf, size_t count) 51 { 52 size_t nleft=count; 53 ssize_t nwritten; 54 char *bufp=(char*)buf; 55 while(nleft>0) 56 { 57 if((nwritten=write(fd,bufp,nleft))<=0) 58 { 59 if(errno==EINTR) 60 continue;//信号中断 61 return -1; 62 }else if(nwritten==0)//write返回0,此时write()什么也不做,好像什么都没发生 63 continue; 64 bufp+=nwritten; 65 nleft-=nwritten; 66 } 67 return count; 68 69 } 70 int main(void) 71 { 72 int sock;//客户端创建套接字 73 if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0) 74 ERR_EXIT("socket error"); 75 76 struct sockaddr_in servaddr;//本地协议地址赋给一个套接字 77 memset(&servaddr,0,sizeof(servaddr)); 78 servaddr.sin_family=AF_INET; 79 servaddr.sin_port=htons(5188); 80 81 servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器段地址 82 //inet_aton("127.0.0.1",&servaddr.sin_addr); 83 84 if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) 85 ERR_EXIT("connect"); 86 struct packet sendbuf;//从标准输入接收发送包 87 struct packet recvbuf;//获得服务器端的回射包 88 memset(&sendbuf,0,sizeof(sendbuf)); 89 memset(&recvbuf,0,sizeof(recvbuf)); 90 int n;//fgets() 函数中的 size 如果小于字符串的长度,那么字符串将会被截取;如果 size 大于字符串的长度则多余的部分系统会自动用 ' ' 填充。所以假如你定义的字符数组长度为 n,那么 fgets() 中的 size 就指定为 n–1,留一个给 ' ' 就行了。 91 while(fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin)!=NULL)//默认有换行符 92 { 93 n=strlen(sendbuf.buf); 94 //包体长度要转成网络字节序。 95 sendbuf.len=htonl(n); 96 writen(sock,&sendbuf,4+n); 97 //客户端接收回射数据包头内容。 98 int ret=readn(sock,&recvbuf.len,4); 99 if(ret==-1) 100 ERR_EXIT("readn"); 101 else if(ret<4) //可能中途中断了 102 { 103 printf("clinet close "); 104 break; 105 } 106 n=ntohl(recvbuf.len); 107 //接收包体内容 108 ret=readn(sock,recvbuf.buf,n); 109 if(ret==-1) 110 ERR_EXIT("readn"); 111 else if(ret<n) 112 { 113 printf("clinet close "); 114 break; 115 } 116 fputs(recvbuf.buf,stdout); 117 memset(&sendbuf,0,sizeof(sendbuf)); 118 memset(&recvbuf,0,sizeof(recvbuf)); 119 } 120 close(sock); 121 122 return 0; 123 }
定长包的接收服务器程序:
1 /* 2 流协议与粘包 3 粘包产生的原因:若SO_SNDBUF的大小没有应用层一条消息大,可能产生粘包问题,因为因为应用层消息被分割,一部分发送了,一部分还在应用层缓冲区。详见UNP48页 4 粘包处理方案:本质上是要在应用层维护消息与消息的边界 5 1、定长包 2、包尾加 (ftp) 3、包头加上包体长度(例如包头定长4字节,收取时先读取包头算出包体长度) 4、更复杂的应用层协议 6 7 封装readn writen程序 8 ssize_t read(int fd, void *buf, size_t count); 9 ssize_t write(int fd, const void *buf, size_t count); 10 */ 11 #include<unistd.h> 12 #include<sys/types.h> 13 #include<sys/socket.h> 14 #include<string.h> 15 #include<stdlib.h> 16 #include<stdio.h> 17 #include<errno.h> 18 #include<netinet/in.h> 19 #include<arpa/inet.h> 20 #define ERR_EXIT(m) 21 do 22 { 23 perror(m); 24 exit(EXIT_FAILURE); 25 }while(0) 26 struct packet 27 { 28 int len;//包头 29 char buf[1024];//包体 30 }; 31 //接收确切数目的读操作 32 ssize_t readn(int fd,void *buf,size_t count) 33 { 34 size_t nleft=count; 35 ssize_t nread; 36 char *bufp=(char*)buf; 37 //剩余字节数大于0就循环 38 while(nleft>0) 39 { 40 if((nread=read(fd,bufp,nleft))<0) 41 { 42 if(errno==EINTR) 43 continue; 44 else 45 return -1; 46 } 47 //对等方关闭了 48 else if(nread==0) 49 return (count-nleft);//已经读取的字节数 50 bufp+=nread; 51 nleft-=nread; 52 } 53 return count; 54 } 55 //发送确切数目的写操作 56 ssize_t writen(int fd, const void *buf, size_t count) 57 { 58 size_t nleft=count; 59 ssize_t nwritten; 60 char *bufp=(char*)buf; 61 while(nleft>0) 62 { 63 if((nwritten=write(fd,bufp,nleft))<=0) 64 { 65 if(errno==EINTR) 66 continue; 67 return -1; 68 }else if(nwritten==0)//好像什么都没发生 69 continue; 70 bufp+=nwritten; 71 nleft-=nwritten; 72 } 73 return count; 74 75 } 76 //服务器回射。 77 void do_service(int conn) 78 { 79 struct packet recvbuf; 80 int n; 81 while(1) 82 { 83 memset(&recvbuf,0,sizeof(recvbuf)); 84 //使用readn之后客户端发送的数据不足n会阻塞 85 //在客户端程序中确定消息的边界,发送定长包 86 int ret=readn(conn,&recvbuf.len,4); 87 //客户端关闭 88 if(ret==-1) 89 ERR_EXIT("read error"); 90 else if(ret<4)//中途中断了。 91 { 92 printf("client close "); 93 break;//不用继续循环等待客户端数据 94 } 95 //接收包体 96 n=ntohl(recvbuf.len);//包体长度 97 ret=readn(conn,recvbuf.buf,n); 98 if(ret==-1) 99 ERR_EXIT("read error"); 100 else if(ret<n)//接收到的字节数不足,对端中途关闭 101 { 102 printf("client close "); 103 break;//不用继续循环等待客户端数据 104 } 105 fputs(recvbuf.buf,stdout); 106 writen(conn,&recvbuf,4+n); 107 } 108 } 109 int main(void) 110 { 111 int listenfd; 112 if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0) 113 ERR_EXIT("socket error"); 114 //if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0) 115 116 117 //本地协议地址赋给一个套接字 118 struct sockaddr_in servaddr; 119 memset(&servaddr,0,sizeof(servaddr)); 120 servaddr.sin_family=AF_INET; 121 servaddr.sin_port=htons(5188); 122 servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//表示本机地址 123 //servaddr.sin_addr.s_addr=inet_addr("127.0.0.1"); 124 //inet_aton("127.0.0.1",&servaddr.sin_addr); 125 126 //开启地址重复使用,关闭服务器再打开不用等待TIME_WAIT 127 int on=1; 128 if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0) 129 ERR_EXIT("setsockopt error"); 130 //绑定本地套接字 131 if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) 132 ERR_EXIT("bind error"); 133 if(listen(listenfd,SOMAXCONN)<0)//设置监听套接字(被动套接字) 134 ERR_EXIT("listen error"); 135 136 struct sockaddr_in peeraddr;//对方套接字地址 137 socklen_t peerlen=sizeof(peeraddr); 138 int conn;//已连接套接字(主动套接字) 139 pid_t pid; 140 while(1){ 141 if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0) 142 ERR_EXIT("accept error"); 143 //连接好之后就构成连接,端口是客户端的。peeraddr是对端 144 printf("ip=%s port=%d ",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); 145 pid=fork(); 146 if(pid==-1) 147 ERR_EXIT("fork"); 148 if(pid==0){ 149 close(listenfd); 150 do_service(conn); 151 //某个客户端关闭,结束该子进程,否则子进程也去接受连接 152 exit(EXIT_SUCCESS); 153 }else close(conn); 154 } 155 return 0; 156 }