日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Socket编程实践(5) --TCP粘包问题与解决

發布時間:2025/3/17 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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粘包问题与解决的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。