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