TCP傳輸協(xié)議是面向流的咆贬,就是沒有界限的一串?dāng)?shù)據(jù)危融。TCP底層并不了解上層業(yè)務(wù)數(shù)據(jù)的具體含義,它會根據(jù)TCP緩沖區(qū)的實際情況進(jìn)行包的劃分擎析,所以在業(yè)務(wù)上認(rèn)為畦韭,一個完整的包可能會被TCP拆分成多個包就行發(fā)送湿颅,也有可能把多個小的包封裝成一個大的數(shù)據(jù)包發(fā)送艘虎,這就是所謂的TCP拆包和粘包問題斋射。
TCP拆包/粘包問題
假設(shè)客戶端分別發(fā)送了兩個數(shù)據(jù)包D1和D2給服務(wù)端症概,由于服務(wù)端一次讀取到字節(jié)數(shù)是不確定的蕾额,故可能存在以下四種情況:
服務(wù)端分兩次讀取到了兩個獨立的數(shù)據(jù)包,分別是D1和D2彼城,沒有粘包和拆包
服務(wù)端一次接受到了兩個數(shù)據(jù)包诅蝶,D1和D2粘合在一起,稱之為TCP粘包
服務(wù)端分兩次讀取到了數(shù)據(jù)包募壕,第一次讀取到了完整的D1包和D2包的部分內(nèi)容调炬,第二次讀取到了D2包的剩余內(nèi)容,這稱之為TCP拆包
服務(wù)端分兩次讀取到了數(shù)據(jù)包司抱,第一次讀取到了D1包的部分內(nèi)容D1_1筐眷,第二次讀取到了D1包的剩余部分內(nèi)容D1_2和完整的D2包。
特別要注意的是习柠,如果TCP的接受滑窗非常小匀谣,而數(shù)據(jù)包D1和D2比較大照棋,很有可能會發(fā)生第五種情況,即服務(wù)端分多次才能將D1和D2包完全接受武翎,期間發(fā)生多次拆包烈炭。
TCP拆包/粘包發(fā)生的原因
在網(wǎng)絡(luò)通信的過程中,每次可以發(fā)送的數(shù)據(jù)包大小是受多種因素限制的宝恶,如 MTU 傳輸單元大小符隙、MSS 最大分段大小、滑動窗口等垫毙。如果一次傳輸?shù)木W(wǎng)絡(luò)包數(shù)據(jù)大小超過傳輸單元大小霹疫,那么我們的數(shù)據(jù)可能會拆分為多個數(shù)據(jù)包發(fā)送出去。如果每次請求的網(wǎng)絡(luò)包數(shù)據(jù)都很小综芥,一共請求了 10000 次丽蝎,TCP 并不會分別發(fā)送 10000 次。因為 TCP 采用的 Nagle 算法對此作出了優(yōu)化膀藐。
MTU 最大傳輸單元和 MSS 最大分段大小
MTU(Maxitum Transmission Unit) 是鏈路層一次最大傳輸數(shù)據(jù)的大小屠阻。MTU 一般來說大小為 1500 byte。MSS(Maximum Segement Size) 是指 TCP 最大報文段長度额各,它是傳輸層一次發(fā)送最大數(shù)據(jù)的大小国觉。如下圖所示,MTU 和 MSS 一般的計算關(guān)系為:MSS = MTU - IP 首部 - TCP首部虾啦,如果 MSS + TCP 首部 + IP 首部 > MTU麻诀,那么數(shù)據(jù)包將會被拆分為多個發(fā)送。這就是拆包現(xiàn)象缸逃。
滑動窗口
滑動窗口是 TCP 傳輸層用于流量控制的一種有效措施针饥,也被稱為通告窗口⌒杵担滑動窗口是數(shù)據(jù)接收方設(shè)置的窗口大小,隨后接收方會把窗口大小告訴發(fā)送方筷凤,以此限制發(fā)送方每次發(fā)送數(shù)據(jù)的大小昭殉,從而達(dá)到流量控制的目的。這樣數(shù)據(jù)發(fā)送方不需要每發(fā)送一組數(shù)據(jù)就阻塞等待接收方確認(rèn)藐守,允許發(fā)送方同時發(fā)送多個數(shù)據(jù)分組挪丢,每次發(fā)送的數(shù)據(jù)都會被限制在窗口大小內(nèi)。由此可見卢厂,滑動窗口可以大幅度提升網(wǎng)絡(luò)吞吐量乾蓬。
現(xiàn)在來看一下滑動窗口是如何造成粘包、拆包的慎恒?
粘包:假設(shè)發(fā)送方的每256 bytes表示一個完整的報文任内,接收方由于數(shù)據(jù)處理不及時撵渡,這256個字節(jié)的數(shù)據(jù)都會被緩存到SO_RCVBUF(接收緩存區(qū))中。如果接收方的SO_RCVBUF中緩存了多個報文死嗦,那么對于接收方而言趋距,這就是粘包。
拆包:考慮另外一種情況越除,假設(shè)接收方的窗口只剩了128节腐,意味著發(fā)送方最多還可以發(fā)送128字節(jié),而由于發(fā)送方的數(shù)據(jù)大小是256字節(jié)摘盆,因此只能發(fā)送前128字節(jié)翼雀,等到接收方ack后,才能發(fā)送剩余字節(jié)孩擂。這就造成了拆包锅纺。
Nagle算法
TCP/IP協(xié)議中,無論發(fā)送多少數(shù)據(jù)肋殴,總是要在數(shù)據(jù)(DATA)前面加上協(xié)議頭(TCP Header+IP Header)囤锉,同時,對方接收到數(shù)據(jù)护锤,也需要發(fā)送ACK表示確認(rèn)官地。
即使從鍵盤輸入的一個字符,占用一個字節(jié)烙懦,可能在傳輸上造成41字節(jié)的包驱入,其中包括1字節(jié)的有用信息和40字節(jié)的首部數(shù)據(jù)。這種情況轉(zhuǎn)變成了4000%的消耗氯析,這樣的情況對于重負(fù)載的網(wǎng)絡(luò)來是無法接受的亏较。
為了盡可能的利用網(wǎng)絡(luò)帶寬,TCP總是希望盡可能的發(fā)送足夠大的數(shù)據(jù)掩缓。(一個連接會設(shè)置MSS參數(shù)雪情,因此,TCP/IP希望每次都能夠以MSS尺寸的數(shù)據(jù)塊來發(fā)送數(shù)據(jù))你辣。
Nagle算法就是為了盡可能發(fā)送大塊數(shù)據(jù)巡通,避免網(wǎng)絡(luò)中充斥著許多小數(shù)據(jù)塊。
Nagle算法的基本定義是任意時刻舍哄,最多只能有一個未被確認(rèn)的小段宴凉。 所謂“小段”,指的是小于MSS尺寸的數(shù)據(jù)塊表悬,所謂“未被確認(rèn)”弥锄,是指一個數(shù)據(jù)塊發(fā)送出去后,沒有收到對方發(fā)送的ACK確認(rèn)該數(shù)據(jù)已收到。
Nagle算法的規(guī)則:
- 如果SO_SNDBUF(發(fā)送緩沖區(qū))中的數(shù)據(jù)長度達(dá)到MSS籽暇,則允許發(fā)送温治;
- 如果該SO_SNDBUF中含有FIN,表示請求關(guān)閉連接图仓,則先將SO_SNDBUF中的剩余數(shù)據(jù)發(fā)送罐盔,再關(guān)閉;
- 設(shè)置了TCP_NODELAY=true選項救崔,則允許發(fā)送惶看。TCP_NODELAY是取消TCP的確認(rèn)延遲機制,相當(dāng)于禁用了Nagle 算法六孵。
- 未設(shè)置TCP_CORK選項時纬黎,若所有發(fā)出去的小數(shù)據(jù)包(包長度小于MSS)均被確認(rèn),則允許發(fā)送;
- 上述條件都未滿足劫窒,但發(fā)生了超時(一般為200ms)本今,則立即發(fā)送。
TCP拆包/粘包問題的解決策略
由于底層的TCP無法理解上層的業(yè)務(wù)數(shù)據(jù)主巍,所以在底層是無法保證數(shù)據(jù)包不被拆分和重組的冠息,這個問題只能通過上層的應(yīng)用協(xié)議棧設(shè)計來解決,根據(jù)業(yè)界的主流協(xié)議的解決方案孕索,可以歸納如下:
- 消息定長逛艰,例如每個報文的大小為固定長度200字節(jié),如果不夠搞旭,空位補空格散怖。假設(shè)我們需要發(fā)送的數(shù)據(jù)是| AB | CDEF | GHIJ | K | LM | 5條數(shù)據(jù),我們的固定長度為 4 字節(jié)肄渗,那么這5 條數(shù)據(jù)一共需要發(fā)送 4 個報文:
+------+------+------+------+
| ABCD | EFGH | IJKL | M000 |
+------+------+------+------+
消息定長法使用非常簡單镇眷,但是缺點也非常明顯,無法很好設(shè)定固定長度的值翎嫡,如果長度太大會造成字節(jié)浪費欠动,長度太小又會影響消息傳輸,所以在一般情況下消息定長法不會被采用钝的。
- 在包尾增加回車換行符進(jìn)行分割翁垂,以下例子采用\n來分割
+-------------------------+
| AB\nCDEF\nGHIJ\nK\nLM\n |
+-------------------------+
由于在發(fā)送報文時尾部需要添加特定分隔符,所以對于分隔符的選擇一定要避免和消息體中字符相同硝桩,以免沖突。否則可能出現(xiàn)錯誤的消息拆分枚荣。比較推薦的做法是將消息進(jìn)行編碼碗脊,例如 base64 編碼,然后可以選擇 64 個編碼字符之外的字符作為特定分隔符。特定分隔符法在消息協(xié)議足夠簡單的場景下比較高效衙伶,例如大名鼎鼎的 Redis 在通信過程中采用的就是換行分隔符祈坠。
- 將消息分為消息頭和消息體,消息頭中包含表示消息總長度(或消息體長度)的字段矢劲,通常設(shè)計思路為消息頭的第一個字段使用int32表示消息的總長度
消息頭 消息體
+--------+----------+
| Length | Content |
+--------+----------+
+-----+-------+-------+----+-----+
| 2AB | 4CDEF | 4GHIJ | 1K | 2LM |
+-----+-------+-------+----+-----+
消息長度 + 消息內(nèi)容的使用方式非常靈活赦拘,且不會存在消息定長法和特定分隔符法的明顯缺陷。當(dāng)然在消息頭中不僅只限于存放消息的長度芬沉,而且可以自定義其他必要的擴展字段躺同,例如消息版本、算法類型等丸逸。