Socket编程实践(5) --TCP粘包问题与解决
TCP粘包問題
由于TCP協議是基于字節流且無邊界的傳輸協議,?因此很有可能產生粘包問題,?問題描述如下
? ?對于Host?A?發送的M1與M2兩個各10K的數據塊,?Host?B?接收數據的方式不確定,?有以下方式接收:
? ?先接收M1,?再接收M2(正確方式)
? ?先接收M2,?再接收M1(錯誤)
? ?一次性收到20k數據(錯誤)
? ?分兩次收到,第一次15k,第二次5k(錯誤)
? ?分兩次收到,第一次5k,第二次15k(錯誤)
? ?其他任何可能(錯誤)
?
粘包產生的原因?
? ?1、SQ_SNDBUF?套接字本身有緩沖區?(發送緩沖區、接受緩沖區)
? ?2、tcp傳送的端?mss大小限制
? ?3、鏈路層也有MTU大小限制,如果數據包大于>MTU要在IP層進行分片,導致消息分割。
? ?4、tcp的流量控制和擁塞控制,也可能導致粘包
? ?5、tcp延遲發送機制等
?
TCP與UDP關于粘包問題的對比
TCP | UDP |
字節流 | 數據報 |
無邊界 | 有邊界 |
對等方的一次讀操作并不能保證完全把消息讀完 | 對方接收數據包的個數是不確定的 |
?
粘包解決方案(本質上是要在應用層維護消息與消息的邊界)
(1)定長包
? ?該方式并不實用:?如果所定義的長度過長,?則會浪費網絡帶寬,?而又如果定義的長度過短,?則一條消息又會拆分成為多條,?僅在TCP的應用一層就增加了合并的開銷,?何況在其他層(因此我在博客中并未給出定長包的示例,?而是將之(一個不太完善的實現)與使用自定義報頭的示例放到了一起,?感興趣的讀者可以下載下來查看);
(2)包尾加\r\n(FTP使用方案)
? ?如果消息本身含有\r\n字符,則也分不清消息的邊界;
(3)報文長度+報文內容
(4)更復雜的應用層協議
?
readn?/?writen實現
Socket,?管道以及某些設備(特別是終端和網絡)有下列兩種性質:
? ?1)一次read操作所返回的數據可能少于所要求的數據,即使還沒到達文件尾端也可能這樣,但這不是一個錯誤,應當繼續讀該設備;
? ?2)一次write操作的返回值也可能少于指定輸入的字節數.這可能是由于某個因素造成的,如:內核緩沖區滿...但這也不是一個錯誤,應當繼續寫余下的數據(通常,只有非阻塞描述符,或捕捉到一個信號時,才發生這種write的中途返回)
? ?? ?在讀寫磁盤文件時從未見到過這種情況,除非是文件系統用完了空間,或者接近了配額限制,不能將所要求寫的數據全部寫出!
? ?? ?通常,在讀/寫一個網絡設備,管道或終端時,需要考慮這些特性.于是,我們就有了下面的這兩個函數:readn和writen,功能分別是讀/寫指定的count字節數據,并處理返回值可能小于要求值的情況:
/**實現: 這兩個函數只是按需多次調用read和write系統調用直至讀/寫了count個數據 **/ /**返回值說明:== count: 說明正確返回, 已經真正讀取了count個字節== -1 : 讀取出錯返回< count: 讀取到了末尾 **/ ssize_t readn(int fd, void *buf, size_t count) {size_t nLeft = count;ssize_t nRead = 0;char *pBuf = (char *)buf;while (nLeft > 0){if ((nRead = read(fd, pBuf, nLeft)) < 0){//如果讀取操作是被信號打斷了, 則說明還可以繼續讀if (errno == EINTR)continue;//否則就是其他錯誤elsereturn -1;}//讀取到末尾else if (nRead == 0)return count-nLeft;//正常讀取nLeft -= nRead;pBuf += nRead;}return count; } /**返回值說明:== count: 說明正確返回, 已經真正寫入了count個字節== -1 : 寫入出錯返回 **/ ssize_t writen(int fd, const void *buf, size_t count) {size_t nLeft = count;ssize_t nWritten = 0;char *pBuf = (char *)buf;while (nLeft > 0){if ((nWritten = write(fd, pBuf, nLeft)) < 0){//如果寫入操作是被信號打斷了, 則說明還可以繼續寫入if (errno == EINTR)continue;//否則就是其他錯誤elsereturn -1;}//如果 ==0則說明是什么也沒寫入, 可以繼續寫else if (nWritten == 0)continue;//正常寫入nLeft -= nWritten;pBuf += nWritten;}return count; }報文長度+報文內容實踐
? ?發報文時:前四個字節長度+報文內容一次性發送;
? ?收報文時:先讀前四個字節,求出報文內容長度;根據長度讀數據。
發送結構:
struct Packet {unsigned int msgLen; //數據部分的長度(網絡字節序)char text[1024]; //報文的數據部分 }; //server端echo部分的改進代碼 void echo(int clientfd) {struct Packet buf;int readBytes;//首先讀取首部while ((readBytes = readn(clientfd, &buf.msgLen, sizeof(buf.msgLen))) > 0){//網絡字節序 -> 主機字節序int lenHost = ntohl(buf.msgLen);//然后讀取數據部分readBytes = readn(clientfd, buf.text, lenHost);if (readBytes == -1)err_exit("readn socket error");else if (readBytes != lenHost){cerr << "client connect closed..." << endl;return ;}cout << buf.text;//然后將其回寫回socketif (writen(clientfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)err_exit("write socket error");memset(&buf, 0, sizeof(buf));}if (readBytes == -1)err_exit("read socket error");else if (readBytes != sizeof(buf.msgLen))cerr << "client connect closed..." << endl; } //client端發送與接收代碼 ...struct Packet buf;memset(&buf, 0, sizeof(buf));while (fgets(buf.text, sizeof(buf.text), stdin) != NULL){/**寫入部分**/unsigned int lenHost = strlen(buf.text);buf.msgLen = htonl(lenHost);if (writen(sockfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)err_exit("writen socket error");/**讀取部分**/memset(&buf, 0, sizeof(buf));//首先讀取首部ssize_t readBytes = readn(sockfd, &buf.msgLen, sizeof(buf.msgLen));if (readBytes == -1)err_exit("read socket error");else if (readBytes != sizeof(buf.msgLen)){cerr << "server connect closed... \nexiting..." << endl;break;}//然后讀取數據部分lenHost = ntohl(buf.msgLen);readBytes = readn(sockfd, buf.text, lenHost);if (readBytes == -1)err_exit("read socket error");else if (readBytes != lenHost){cerr << "server connect closed... \nexiting..." << endl;break;}//將數據部分打印輸出cout << buf.text;memset(&buf, 0, sizeof(buf));} ...完整實現代碼:
http://download.csdn.net/detail/hanqing280441589/8460557
?
按行讀取實踐
recv/send函數
ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags);與read相比,recv只能用于套接字文件描述符,而且多了一個flags
recv的flags參數常用取值:
MSG_OOB(帶外數據:?通過緊急指針發送的數據[需設置TCP頭部緊急指針位有效])
? ?This?flag?requests?receipt?of?out-of-band?data?that?would?not?be?received??
in?the?normal?data?stream. ?Some?protocols?place?expedited?data?at?the?head?of?
the?normal?data?queue,?and??thus??this?flag?cannot?be?used?with?such?protocols.
MSG_PEEK(可以讀數據,但不從緩存區中讀走[僅僅是一瞥],利用此特點可以方便的實現按行讀取數據;一個一個字符的讀,多次調用系統調用read方法,效率不高)
? ?This??flag??causes?the?receive?operation?to?return?data?from?the?beginning?of?
the?receive?queue?without?removing?that??data??from?the?queue.??Thus,?a?subsequent?
receive?call?will?return?the?same?data.
/**示例: 通過MSG_PEEK封裝一個recv_peek函數(僅查看數據, 但不取走)**/ ssize_t recv_peek(int sockfd, void *buf, size_t len) {while (true){int ret = recv(sockfd, buf, len, MSG_PEEK);//如果recv是由于被信號打斷, 則需要繼續(continue)查看if (ret == -1 && errno == EINTR)continue;return ret;} }/**使用recv_peek實現按行讀取readline(只能用于socket)**/ /** 返回值說明:== 0: 對端關閉== -1: 讀取出錯其他: 一行的字節數(包含'\n') **/ ssize_t readline(int sockfd, void *buf, size_t maxline) {int ret;int nRead = 0;int returnCount = 0;char *pBuf = (char *)buf;int nLeft = maxline;while (true){ret = recv_peek(sockfd, pBuf, nLeft);//如果查看失敗或者對端關閉, 則直接返回if (ret <= 0)return ret;nRead = ret;for (int i = 0; i < nRead; ++i)//在當前查看的這段緩沖區中含有'\n', 則說明已經可以讀取一行了if (pBuf[i] == '\n'){//則將緩沖區內容讀出//注意是i+1: 將'\n'也讀出ret = readn(sockfd, pBuf, i+1);if (ret != i+1)exit(EXIT_FAILURE);return ret + returnCount;}// 如果在查看的這段消息中沒有發現'\n', 則說明還不滿足一條消息,// 在將這段消息從緩沖中讀出之后, 還需要繼續查看ret = readn(sockfd, pBuf, nRead);;if (ret != nRead)exit(EXIT_FAILURE);pBuf += nRead;nLeft -= nRead;returnCount += nRead;}//如果程序能夠走到這里, 則說明是出錯了return -1; }readline實現思想:
? ?在readline函數中,我們先用recv_peek”偷窺”?一下現在緩沖區有多少個字符并讀取到pBuf,然后查看是否存在換行符'\n'。如果存在,則使用readn連同換行符一起讀取(作用相當于清空socket緩沖區);?如果不存在,也清空一下緩沖區,?且移動pBuf的位置,回到while循環開頭,再次窺看。注意,當我們調用readn讀取數據時,那部分緩沖區是會被清空的,因為readn調用了read函數。還需注意一點是,如果第二次才讀取到了'\n',則先用returnCount保存了第一次讀取的字符個數,然后返回的ret需加上原先的數據大小。
?
按行讀取echo代碼:
void echo(int clientfd) {char buf[512] = {0};int readBytes;while ((readBytes = readline(clientfd, buf, sizeof(buf))) > 0){cout << buf;if (writen(clientfd, buf, readBytes) == -1)err_exit("writen error");memset(buf, 0, sizeof(buf));}if (readBytes == -1)err_exit("readline error");else if (readBytes == 0)cerr << "client connect closed..." << endl; }client端讀取與發送代碼
...char buf[512] = {0};memset(buf, 0, sizeof(buf));while (fgets(buf, sizeof(buf), stdin) != NULL){if (writen(sockfd, buf, strlen(buf)) == -1)err_exit("writen error");memset(buf, 0, sizeof(buf));int readBytes = readline(sockfd, buf, sizeof(buf));if (readBytes == -1)err_exit("readline error");else if (readBytes == 0){cerr << "server connect closed..." << endl;break;}cout << buf;memset(buf, 0, sizeof(buf));} ...完整代碼實現:
http://download.csdn.net/detail/hanqing280441589/8460883
總結
以上是生活随笔為你收集整理的Socket编程实践(5) --TCP粘包问题与解决的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 爱上MVC3系列~同步与异步提交,在过滤
- 下一篇: 垃圾代码评析——关于《C程序设计伴侣》9