流協議與粘包:網絡
咱們知道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)
這樣就很好的解決了粘包問題,在局域網中是不可能出現粘包問題的,可是若是將程序放到廣域網,若是不處理粘包問題會存在很大問題的。