自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

為什么 TCP 協(xié)議有粘包問(wèn)題

網(wǎng)絡(luò) 網(wǎng)絡(luò)管理
為什么這么設(shè)計(jì)(Why’s THE Design)是一系列關(guān)于計(jì)算機(jī)領(lǐng)域中程序設(shè)計(jì)決策的文章,我們?cè)谶@個(gè)系列的每一篇文章中都會(huì)提出一個(gè)具體的問(wèn)題并從不同的角度討論這種設(shè)計(jì)的優(yōu)缺點(diǎn)、對(duì)具體實(shí)現(xiàn)造成的影響。

TCP/IP 協(xié)議簇建立了互聯(lián)網(wǎng)中通信協(xié)議的概念模型,該協(xié)議簇中的兩個(gè)主要協(xié)議就是 TCP 和 IP 協(xié)議。TCP/ IP 協(xié)議簇中的 TCP 協(xié)議能夠保證數(shù)據(jù)段(Segment)的可靠性和順序,有了可靠的傳輸層協(xié)議之后,應(yīng)用層協(xié)議就可以直接使用 TCP 協(xié)議傳輸數(shù)據(jù),不在需要關(guān)心數(shù)據(jù)段的丟失和重復(fù)問(wèn)題。

圖 1 - TCP 協(xié)議與應(yīng)用層協(xié)議

IP 協(xié)議解決了數(shù)據(jù)包(Packet)的路由和傳輸,上層的 TCP 協(xié)議不再關(guān)注路由和尋址[^2],那么 TCP 協(xié)議解決的是傳輸?shù)目煽啃院晚樞騿?wèn)題,上層不需要關(guān)心數(shù)據(jù)能否傳輸?shù)侥繕?biāo)進(jìn)程,只要寫(xiě)入 TCP 協(xié)議的緩沖區(qū)的數(shù)據(jù),協(xié)議棧幾乎都能保證數(shù)據(jù)的送達(dá)。

當(dāng)應(yīng)用層協(xié)議使用 TCP 協(xié)議傳輸數(shù)據(jù)時(shí),TCP 協(xié)議可能會(huì)將應(yīng)用層發(fā)送的數(shù)據(jù)分成多個(gè)包依次發(fā)送,而數(shù)據(jù)的接收方收到的數(shù)據(jù)段可能有多個(gè)『應(yīng)用層數(shù)據(jù)包』組成,所以當(dāng)應(yīng)用層從 TCP 緩沖區(qū)中讀取數(shù)據(jù)時(shí)發(fā)現(xiàn)粘連的數(shù)據(jù)包時(shí),需要對(duì)收到的數(shù)據(jù)進(jìn)行拆分。

粘包并不是 TCP 協(xié)議造成的,它的出現(xiàn)是因?yàn)閼?yīng)用層協(xié)議設(shè)計(jì)者對(duì) TCP 協(xié)議的錯(cuò)誤理解,忽略了 TCP 協(xié)議的定義并且缺乏設(shè)計(jì)應(yīng)用層協(xié)議的經(jīng)驗(yàn)。本文將從 TCP 協(xié)議以及應(yīng)用層協(xié)議出發(fā),分析我們經(jīng)常提到的 TCP 協(xié)議中的粘包是如何發(fā)生的:

  • TCP 協(xié)議是面向字節(jié)流的協(xié)議,它可能會(huì)組合或者拆分應(yīng)用層協(xié)議的數(shù)據(jù);
  • 應(yīng)用層協(xié)議的沒(méi)有定義消息的邊界導(dǎo)致數(shù)據(jù)的接收方無(wú)法拼接數(shù)據(jù);

很多人可能會(huì)認(rèn)為粘包是一個(gè)比較低級(jí)的甚至不值得討論的問(wèn)題,但是在作者看來(lái)這個(gè)問(wèn)題還是很有趣的,不是所有人都系統(tǒng)性地學(xué)過(guò)基于 TCP 的應(yīng)用層協(xié)議設(shè)計(jì),也不是所有人對(duì) TCP 協(xié)議也沒(méi)有那么深入的理解,相信很多人學(xué)習(xí)編程的過(guò)程都是自底向上的,所以作者認(rèn)為這是一個(gè)值得回答的問(wèn)題,我們應(yīng)該傳遞正確的知識(shí),而不是負(fù)面的和居高臨下的情緒。

面向字節(jié)流

TCP 協(xié)議是面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議[^3],應(yīng)用層交給 TCP 協(xié)議的數(shù)據(jù)并不會(huì)以消息為單位向目的主機(jī)傳輸,這些數(shù)據(jù)在某些情況下會(huì)被組合成一個(gè)數(shù)據(jù)段發(fā)送給目標(biāo)的主機(jī)。

Nagle 算法是一種通過(guò)減少數(shù)據(jù)包的方式提高 TCP 傳輸性能的算法[^4]。因?yàn)榫W(wǎng)絡(luò) 帶寬有限,它不會(huì)將小的數(shù)據(jù)塊直接發(fā)送到目的主機(jī),而是會(huì)在本地緩沖區(qū)中等待更多待發(fā)送的數(shù)據(jù),這種批量發(fā)送數(shù)據(jù)的策略雖然會(huì)影響實(shí)時(shí)性和網(wǎng)絡(luò)延遲,但是能夠降低網(wǎng)絡(luò)擁堵的可能性并減少額外開(kāi)銷。

在早期的互聯(lián)網(wǎng)中,Telnet 是被廣泛使用的應(yīng)用程序,然而使用 Telnet 會(huì)產(chǎn)生大量只有 1 字節(jié)負(fù)載的有效數(shù)據(jù),每個(gè)數(shù)據(jù)包都會(huì)有 40 字節(jié)的額外開(kāi)銷,帶寬的利用率只有 ~2.44%,Nagle 算法就是在當(dāng)時(shí)的這種場(chǎng)景下設(shè)計(jì)的。

當(dāng)應(yīng)用層協(xié)議通過(guò) TCP 協(xié)議傳輸數(shù)據(jù)時(shí),實(shí)際上待發(fā)送的數(shù)據(jù)先被寫(xiě)入了 TCP 協(xié)議的緩沖區(qū),如果用戶開(kāi)啟了 Nagle 算法,那么 TCP 協(xié)議可能不會(huì)立刻發(fā)送寫(xiě)入的數(shù)據(jù),它會(huì)等待緩沖區(qū)中數(shù)據(jù)超過(guò)最大數(shù)據(jù)段(MSS)或者上一個(gè)數(shù)據(jù)段被 ACK 時(shí)才會(huì)發(fā)送緩沖區(qū)中的數(shù)據(jù)。

TCP

圖 2 - Nagle 算法

幾十年前還會(huì)發(fā)生網(wǎng)絡(luò)擁塞的問(wèn)題,但是今天的網(wǎng)絡(luò)帶寬資源不再像過(guò)去那么緊張,在默認(rèn)情況下,Linux 內(nèi)核都會(huì)使用如下的方式默認(rèn)關(guān)閉 Nagle 算法:

  1. TCP_NODELAY = 1 

Linux 內(nèi)核中使用如下所示的 tcp_nagle_test 函數(shù)測(cè)試我們是否應(yīng)該發(fā)送當(dāng)前的 TCP 數(shù)據(jù)段,感興趣的讀者可以以這段代碼為入口詳細(xì)了解 Nagle 算法在今天的實(shí)現(xiàn):

  1. static inline bool tcp_nagle_test(const struct tcp_sock *tp, const struct sk_buff *skb, 
  2.                   unsigned int cur_mss, int nonagle) 
  3.     if (nonagle & TCP_NAGLE_PUSH) 
  4.         return true; 
  5.  
  6.     if (tcp_urg_mode(tp) || (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)) 
  7.         return true; 
  8.  
  9.     if (!tcp_nagle_check(skb->len < cur_mss, tp, nonagle)) 
  10.         return true; 
  11.  
  12.     return false; 

Nagle 算法確實(shí)能夠在數(shù)據(jù)包較小時(shí)提高網(wǎng)絡(luò)帶寬的利用率并減少 TCP 和 IP 協(xié)議頭帶來(lái)的額外開(kāi)銷,但是使用該算法也可能會(huì)導(dǎo)致應(yīng)用層協(xié)議多次寫(xiě)入的數(shù)據(jù)被合并或者拆分發(fā)送,當(dāng)接收方從 TCP 協(xié)議棧中讀取數(shù)據(jù)時(shí)會(huì)發(fā)現(xiàn)不相關(guān)的數(shù)據(jù)出現(xiàn)在了同一個(gè)數(shù)據(jù)段中,應(yīng)用層協(xié)議可能沒(méi)有辦法對(duì)它們進(jìn)行拆分和重組。

除了 Nagle 算法之外,TCP 協(xié)議棧中還有另一個(gè)用于延遲發(fā)送數(shù)據(jù)的選項(xiàng) TCP_CORK,如果我們開(kāi)啟該選項(xiàng),那么當(dāng)發(fā)送的數(shù)據(jù)小于 MSS 時(shí),TCP 協(xié)議就會(huì)延遲 200ms 發(fā)送該數(shù)據(jù)或者等待緩沖區(qū)中的數(shù)據(jù)超過(guò) MSS[^5]。

無(wú)論是 TCP_NODELAY 還是 TCP_CORK,它們都會(huì)通過(guò)延遲發(fā)送數(shù)據(jù)來(lái)提高帶寬的利用率,它們會(huì)對(duì)應(yīng)用層協(xié)議寫(xiě)入的數(shù)據(jù)進(jìn)行拆分和重組,而這些機(jī)制和配置能夠出現(xiàn)的最重要原因是 — TCP 協(xié)議是基于字節(jié)流的協(xié)議,其本身沒(méi)有數(shù)據(jù)包的概念,不會(huì)按照數(shù)據(jù)包發(fā)送數(shù)據(jù)。

消息邊界

如果我們系統(tǒng)性地學(xué)習(xí)過(guò) TCP 協(xié)議以及基于 TCP 的應(yīng)用層協(xié)議設(shè)計(jì),那么設(shè)計(jì)一個(gè)能夠被 TCP 協(xié)議棧任意拆分和組裝數(shù)據(jù)包的應(yīng)用層協(xié)議就不會(huì)有什么問(wèn)題。既然 TCP 協(xié)議是基于字節(jié)流的,這其實(shí)就意味著應(yīng)用層協(xié)議要自己劃分消息的邊界。

如果我們能在應(yīng)用層協(xié)議中定義消息的邊界,那么無(wú)論 TCP 協(xié)議如何對(duì)應(yīng)用層協(xié)議的數(shù)據(jù)包進(jìn)程拆分和重組,接收方都能根據(jù)協(xié)議的規(guī)則恢復(fù)對(duì)應(yīng)的消息。在應(yīng)用層協(xié)議中,最常見(jiàn)的兩種解決方案就是基于長(zhǎng)度或者基于終結(jié)符(Delimiter)。

圖 3 - 實(shí)現(xiàn)消息邊界的方法

基于長(zhǎng)度的實(shí)現(xiàn)有兩種方式,一種是使用固定長(zhǎng)度,所有的應(yīng)用層消息都使用統(tǒng)一的大小,另一種方式是使用不固定長(zhǎng)度,但是需要在應(yīng)用層協(xié)議的協(xié)議頭中增加表示負(fù)載長(zhǎng)度的字段,這樣接收方才可以從字節(jié)流中分離出不同的消息,HTTP 協(xié)議的消息邊界就是基于長(zhǎng)度實(shí)現(xiàn)的:

  1. HTTP/1.1 200 OK 
  2. Content-Type: text/html; charset=UTF-8 
  3. Content-Length: 138 
  4. ... 
  5. Connection: close 
  6.  
  7. <html> 
  8.   <head> 
  9.     <title>An Example Page</title> 
  10.   </head> 
  11.   <body> 
  12.     <p>Hello World, this is a very simple HTML document.</p> 
  13.   </body> 
  14. </html> 

 

在上述 HTTP 消息中,我們使用 Content-Length 頭表示 HTTP 消息的負(fù)載大小,當(dāng)應(yīng)用層協(xié)議解析到足夠的字節(jié)數(shù)后,就能從中分離出完整的 HTTP 消息,無(wú)論發(fā)送方如何處理對(duì)應(yīng)的數(shù)據(jù)包,我們都可以遵循這一規(guī)則完成 HTTP 消息的重組[^6]。

不過(guò) HTTP 協(xié)議除了使用基于長(zhǎng)度的方式實(shí)現(xiàn)邊界,也會(huì)使用基于終結(jié)符的策略,當(dāng) HTTP 使用塊傳輸(Chunked Transfer)機(jī)制時(shí),HTTPz 頭中就不再包含 Content-Length 了,它會(huì)使用負(fù)載大小為 0 的 HTTP 消息作為終結(jié)符表示消息的邊界。

當(dāng)然除了這兩種方式之外,我們可以基于特定的規(guī)則實(shí)現(xiàn)消息的邊界,例如:使用 TCP 協(xié)議發(fā)送 JSON 數(shù)據(jù),接收方可以根據(jù)接收到的數(shù)據(jù)是否能夠被解析成合法的 JSON 判斷消息是否終結(jié)。

總結(jié)

TCP 協(xié)議粘包問(wèn)題是因?yàn)閼?yīng)用層協(xié)議開(kāi)發(fā)者的錯(cuò)誤設(shè)計(jì)導(dǎo)致的,他們忽略了 TCP 協(xié)議數(shù)據(jù)傳輸?shù)暮诵臋C(jī)制 — 基于字節(jié)流,其本身不包含消息、數(shù)據(jù)包等概念,所有數(shù)據(jù)的傳輸都是流式的,需要應(yīng)用層協(xié)議自己設(shè)計(jì)消息的邊界,即消息幀(Message Framing),我們重新回顧一下粘包問(wèn)題出現(xiàn)的核心原因:

  • TCP 協(xié)議是基于字節(jié)流的傳輸層協(xié)議,其中不存在消息和數(shù)據(jù)包的概念;
  • 應(yīng)用層協(xié)議沒(méi)有使用基于長(zhǎng)度或者基于終結(jié)符的消息邊界,導(dǎo)致多個(gè)消息的粘連;

網(wǎng)絡(luò)協(xié)議的學(xué)習(xí)過(guò)程非常有趣,不斷思考背后的問(wèn)題能夠讓我們對(duì)定義有更深的認(rèn)識(shí)。到最后,我們還是來(lái)看一些比較開(kāi)放的相關(guān)問(wèn)題,有興趣的讀者可以仔細(xì)思考一下下面的問(wèn)題:

  • 基于 UDP 協(xié)議的應(yīng)用層協(xié)議應(yīng)該如何設(shè)計(jì)?會(huì)出現(xiàn)粘包的問(wèn)題么?
  • 有哪些應(yīng)用層協(xié)議使用基于長(zhǎng)度的分幀?又有哪些使用基于終結(jié)符的分幀?

 

責(zé)任編輯:趙寧寧 來(lái)源: 真沒(méi)什么邏輯
相關(guān)推薦

2024-10-12 18:16:27

2019-10-17 11:06:32

TCP粘包通信協(xié)議

2024-12-19 11:00:00

TCP網(wǎng)絡(luò)通信粘包

2021-07-15 10:35:16

NettyTCPJava

2021-03-09 22:30:47

TCP拆包協(xié)議

2019-10-24 07:35:13

TCP粘包Netty

2020-02-18 09:17:45

TCPIP網(wǎng)絡(luò)協(xié)議

2022-10-08 00:00:00

websocket協(xié)議HTTP

2020-01-06 15:23:41

NettyTCP粘包

2019-08-15 07:43:38

TCP網(wǎng)絡(luò)協(xié)議丟包

2022-04-28 08:38:09

TCP協(xié)議解碼器

2024-10-15 09:48:56

2020-12-30 09:04:32

Go語(yǔ)言TCPUDP

2019-09-30 09:41:04

五層協(xié)議OSITCP

2020-12-23 07:53:01

TCP通信Netty

2022-07-19 08:01:32

HTTP協(xié)議RPC

2023-09-07 08:07:56

goHTTP網(wǎng)絡(luò)

2010-07-07 10:45:22

TCP UDP協(xié)議

2023-10-24 15:15:26

HTTPWebSocket

2021-10-12 18:48:07

HTTP 協(xié)議Websocket網(wǎng)絡(luò)通信
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)