至此,我们已经学习了简单的回射/客户服务器,处理多客户连接以及点对点的通信等内容。今天我们将学习一下内容:流协议与粘包、粘包产生的原因、粘包处理方案、readn和writen以及对回射客户/服务器的一些改进。
1.流协议与粘包
我们知道TCP是基于字节流的传输服务,意味着TCP传输的数据之间是没有边间的;而UDP是基于消息的传输服务,传输的是数据报,是有边界的。
这就导致了TCP传输过程会产生一个问题——粘包问题。
2.粘包产生的原因
(1)应用程序通过套接字调用一个write方法,将应用层缓冲区中的数据拷贝到套接口发送缓冲区中,而发送缓冲区有SO_SNDBUF大小的限制。若应用层一条消息的缓冲区大小超过发送缓冲区大小,就可能会被分隔。
(2)TCP传输有最大段MSS(maximum segment size)大小的限制,也有可能产生粘包问题。
(3)链路层传输数据有最大传输单元MTU的限制,如果传输数据超过MTU就会在IP层进行分片。也会导致粘包问题。
(4)其他情况,例如TCP拥塞控制等也会导致粘包问题。
如下图所示:
3.如何解决粘包问题
本质上是要在应用层维护消息与消息的边界,有如下解决方案:
- 定长包
- 包尾加\r\n(ftp)
- 包头加上包体长度
- 更复杂的应用层协议
4.readn、writen函数的封装
用来接收和读取确切数目的读写操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
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) break; /* EOF */ 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) break; bufp += nwritten; nleft -= nwritten; } return count; } |
(1)发送定长包
如果单单是改进发送定长包也存在一种“浪费”情况:比如服务端和客户端规定了缓冲区大小为1024字节,即每次发送、接收数据都要处理1024字节,尽管发送的数据未到1024字节,这就增加了网络负担。那么,怎么处理这种情况呢?
(2)我们自己定义协议,定义一个包的结构体来应对上述情况。
1 2 3 4 |
struct packet { int len; //包头,存放的是包体实际的数据长度 char buf[1024]; //包体 }; |
1)发送时,一共发送字节数为头部字节4(int 类型为4字节)加上实际字节n(sendbuf.buf)。
1 2 3 |
n = strlen(sendbuf.buf); sendbuf.len = htonl(n); writen(sock, &sendbuf, 4+n); |
2)接收时,分两次接收:先接收4个字节(recvbuf.len),然后根据recvbuf.len(假设recvbuf.len为n)的长度,接受n个字节。
1 2 3 |
int ret = readn(sock, &recvbuf.len, 4); n = ntohl(recvbuf.len); ret = readn(sock, recvbuf.buf, n); |
(3)完整源码如下:
echosrv.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
#include<unistd.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<stdlib.h> #include<stdio.h> #include<errno.h> #include<string.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; } void do_service(int conn) { 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(5581); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 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("liten"); struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int conn; pid_t pid; while(1) { if((conn = accept(listenfd, (struct sockaddr*) &peeraddr, &peerlen)) < 0) ERR_EXIT("accept"); printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); pid = fork(); if(pid == -1) ERR_EXIT("fork"); if(pid == 0) { close(listenfd); do_service(conn); exit(EXIT_SUCCESS); } else close(conn); } return 0; } |
echoscli.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
#include<unistd.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<stdlib.h> #include<stdio.h> #include<errno.h> #include<string.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 sock; if((sock = 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(5581); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(sock, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0) ERR_EXIT("connect"); 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){ n = strlen(sendbuf.buf); sendbuf.len = htonl(n); writen(sock, &sendbuf, 4+n); int ret = readn(sock, &recvbuf.len, 4); if(ret == -1) ERR_EXIT("read"); else if(ret <4){ printf("client close\n"); break; } n = ntohl(recvbuf.len); ret = readn(sock, 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 (sock); return 0; } |
总结:
(1)在处理粘包问题时,加上了自定义的协议——包头+包体,从而实现了消息与消息的边界。
(2)包头就是数据长度,包体是数据部分
(3)对方接收的时候,先接收长度(固定是4个字节),然后根据这4个字节计算包体的实际数据长度再接收n个字节。