粘包问题:应用层要发送数据,需要调用write函数将数据发送到套接口发送缓冲区。如果应用层数据大小大于SO_SNDBUF,
那么,可能产生这样一种情况,应用层的数据一部分已经被发送了,还有一部分还在套接口缓冲区待发送。此时,对方延迟接收,就容易产生粘包。
另一方面,TCP传输有MSS限制,也会对数据进行分割。第三个原因,由于MTU存在,也可能分割数据。都会产生粘包问题
粘包问题解决方案:本质上是要在应用层维护消息与消息的边界。
1、定长包
2、包尾加\r\n(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 大于字符串的长度则多余的部分系统会自动用 '\0' 填充。所以假如你定义的字符数组长度为 n,那么 fgets() 中的 size 就指定为 n–1,留一个给 '\0' 就行了。
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\n");
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\n");
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、包尾加 \r\n (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\n");
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\n");
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\n",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 }