TCP那些事兒

目錄:

  • TCP是什么
  • TCP報(bào)文結(jié)構(gòu)
  • TCP連接過(guò)程
  • TCP狀態(tài)轉(zhuǎn)移
  • TCP流量控制 —— 滑動(dòng)窗口
  • TCP擁塞控制
  • TCP可靠傳輸是怎么做到的
  • TCP一些有趣的問(wèn)題 —— 粘包/拆包等

為什么我們需要了解TCP

以前我也認(rèn)為T(mén)CP是相當(dāng)?shù)讓拥臇|西,我永遠(yuǎn)不需要去了解它。雖然差不多是這樣,但是實(shí)際生活中,你依然可能遇見(jiàn)和TCP算法相關(guān)的bug,這時(shí)候懂一些TCP的知識(shí)就至關(guān)重要了。(本文也可以引申為,系統(tǒng)調(diào)用,操作系統(tǒng)這些都很重要,這個(gè)道理適用于很多東西
這里推薦一篇小短文,人人都應(yīng)該懂點(diǎn)TCP

TCP是什么

TCP —— Transmission Control Protocol 傳輸控制協(xié)議

TCP通信基本流程

使用TCP協(xié)議通信的雙方必須先建立TCP連接,并在內(nèi)核中為該連接維持一些必要的數(shù)據(jù)結(jié)構(gòu),比如連接的狀態(tài)、讀寫(xiě)緩沖區(qū)、定時(shí)器等。當(dāng)通信結(jié)束時(shí),雙方必須關(guān)閉連接以釋放這些內(nèi)核數(shù)據(jù)。TCP服務(wù)基于流,源源不斷從一端流向另一端,發(fā)送端可以逐字節(jié)寫(xiě)入,接收端可以逐字節(jié)讀出,無(wú)需分段。

特點(diǎn)

  • 位于傳輸層,基本傳輸結(jié)構(gòu)是TCP報(bào)文段(TCP message segment
  • 面向連接 一對(duì)一,端對(duì)端,進(jìn)程與進(jìn)程之間通信,不適用于廣播、多播程序
  • 可靠傳輸 —— 發(fā)送應(yīng)答/超時(shí)重傳/報(bào)文排序/流量控制/擁塞處理
  • 端對(duì)端 進(jìn)程和進(jìn)程之間 端口和端口之間
  • 全雙工通信
  • 基于字節(jié)流服務(wù) 數(shù)據(jù)發(fā)送和接收沒(méi)有邊界限制和分段
這里解釋下字節(jié)流的概念:
TCP是基于字節(jié)流服務(wù),而UDP則是基于數(shù)據(jù)報(bào)服務(wù)。對(duì)應(yīng)到實(shí)際的編程中表        
現(xiàn)為通信雙方是否必須執(zhí)行相同次數(shù)的讀寫(xiě)操作。當(dāng)發(fā)送端連續(xù)執(zhí)行多次寫(xiě)操作時(shí),TCP模塊會(huì)先把這些
數(shù)據(jù)放入`發(fā)送緩沖區(qū)`。當(dāng)真正開(kāi)始發(fā)送數(shù)據(jù)的時(shí)候,發(fā)送緩沖區(qū)中的數(shù)據(jù)可能被封裝成一個(gè)或者多個(gè)TCP
報(bào)文段發(fā)出。TCP報(bào)文段的個(gè)數(shù)和寫(xiě)操作次數(shù)沒(méi)有關(guān)系。
當(dāng)接收端收到一個(gè)或者多個(gè)TCP報(bào)文段后,TCP模塊講它們攜帶的數(shù)據(jù)放入TCP`接收緩沖區(qū)`中,并通知
應(yīng)用程序讀取數(shù)據(jù)??梢砸淮涡匀孔x出,也可以分多次讀出。接收到的報(bào)文個(gè)數(shù)和讀次數(shù)也沒(méi)有關(guān)系。
綜上,這就是字節(jié)流的概念:應(yīng)用程序?qū)?shù)據(jù)的發(fā)送和接收沒(méi)有邊界限制。
相對(duì)的,UDP則是應(yīng)用程序沒(méi)執(zhí)行一次寫(xiě)操作,UDP模塊就將其封裝成一個(gè)UDP數(shù)據(jù)報(bào)并發(fā)送之,接收端
每收到一個(gè)UDP數(shù)據(jù)報(bào)就必須進(jìn)行一次讀操作,否則會(huì)丟包。

TCP報(bào)文結(jié)構(gòu)

TCP首部結(jié)構(gòu)

tcp報(bào)文header格式

英文版

需要注意的幾點(diǎn):

  • tcp的包沒(méi)有ip地址 那是ip層的事,但是有源端口和目標(biāo)端口
  • 一個(gè)tcp連接用一個(gè)四元組來(lái)表示(src_ip, src_port,dst_ip,dst_port)ps:準(zhǔn)確說(shuō)是五元組,加上一個(gè)協(xié)議
  • 幾個(gè)名詞
  • 固定長(zhǎng)度為20字節(jié),包含變長(zhǎng)部分,最大為60字節(jié)。
    • Sequence NumberSeq,包的序號(hào),用于解決包傳輸過(guò)程中的亂序問(wèn)題
    • Acknowledgement NumberAck,確認(rèn)號(hào),用于確認(rèn)包收到,用于解決丟包問(wèn)題
    • Synchronize Sequence NumbersSYN 同步序號(hào) 用于包同步
    • Window 窗口,即著名的滑動(dòng)窗口(Silding Window),用于流量擁塞控制
    • TCP Flag RST/SYN/FIN等,即包的類(lèi)型,用于操控tcp的各種狀態(tài)
具體分析:
  • 端口號(hào):16位。 一般服務(wù)端會(huì)使用知名端口號(hào),而客戶端一般使用系統(tǒng)自動(dòng)選擇的臨時(shí)端口號(hào)。所有知名服務(wù)所使用的端口號(hào)都定義在 /etc/services文件中[1]。
  • Seq序號(hào):32位。一次TCP通信過(guò)程中王某一個(gè)傳輸方向上的字節(jié)流中每個(gè)字節(jié)的編號(hào)。第一個(gè)報(bào)文段Seq會(huì)被初始化為ISN(Initial Sequence Number初始序號(hào)),這是一個(gè)隨機(jī)值,后續(xù)的報(bào)文段中序號(hào)值將為ISN+報(bào)文第一個(gè)字節(jié)的偏移值。

eg. 某個(gè)tcp報(bào)文傳輸?shù)氖亲止?jié)流中的1025~2048字節(jié),Seq將為ISN+1025

  • Ack序號(hào):32位。用來(lái)對(duì)另一方發(fā)來(lái)的TCP報(bào)文進(jìn)行響應(yīng)。值為seq+1
  • Offset:4位,標(biāo)識(shí)TCP頭部的長(zhǎng)度,有多少字(32-bit words)因?yàn)槭?位,TCP頭部最大為60字節(jié)。(4位最大15,15*4=60)
  • 六個(gè)標(biāo)志位:TCP Flags.
  • URG 表示緊急指針(urgent pointer)是否有效
  • ACK 表示確認(rèn)號(hào)是否有效。帶ACK標(biāo)志的TCP報(bào)文段叫確認(rèn)報(bào)文段
  • PSH 提示接收端應(yīng)該立即從TCP接收緩沖區(qū)中讀走數(shù)據(jù),為后續(xù)數(shù)據(jù)騰出緩沖區(qū)空間。
  • RST 復(fù)位標(biāo)識(shí),表示要求對(duì)方重建連接。帶RST標(biāo)志的TCP報(bào)文段叫復(fù)位報(bào)文段,一般用于異常終止連接,一旦發(fā)送了RST報(bào)文段,發(fā)送端所有排隊(duì)等待的數(shù)據(jù)都會(huì)被丟棄。
  • SYN 表示請(qǐng)求建立連接。帶SYN標(biāo)志的TCP報(bào)文段叫同步報(bào)文段
  • FIN 表示通知對(duì)方本端要關(guān)閉連接了。帶FIN標(biāo)志的TCP報(bào)文段叫結(jié)束報(bào)文段
  • 窗口window:TCP流量控制的一個(gè)手段。指接收窗口的大小。它告訴對(duì)方本端的TCP接收緩沖區(qū)還能容納多少字節(jié)數(shù)據(jù),這樣可以讓對(duì)端控制發(fā)送數(shù)據(jù)的速度。
  • checksum:16位校驗(yàn)和。由發(fā)送端填充,接收端對(duì)這個(gè)字段用CRC校驗(yàn),校驗(yàn)TCP報(bào)文在傳輸過(guò)程中是否損壞(頭部和數(shù)據(jù)部分都會(huì)被校驗(yàn))。這是TCP可靠傳輸?shù)闹匾U稀?/li>
  • urgent pointer:緊急指針,用于發(fā)送端向接收端發(fā)送緊急數(shù)據(jù)。

Options: TCP頭部最后一個(gè)選項(xiàng)字段options是一個(gè)變長(zhǎng)的可選字段,最多包含40byte,這也是TCP頭部最長(zhǎng)為60字節(jié)的原因。
options包含的字段非常多,這里僅選取比較重要的幾個(gè)字段講一下。
options的第一個(gè)字段kind表示選項(xiàng)的類(lèi)型。其中,kind=2是最大報(bào)文長(zhǎng)度選項(xiàng),簡(jiǎn)稱(chēng)MSS。傳輸層每次傳輸數(shù)據(jù)有個(gè)最大限制MTU(Maximum Transmission Unit)。而TCP模塊通常會(huì)將MSS設(shè)置為(MTU-40)字節(jié),減掉的這40byte = 20 byte的TCP Header + 20 byte的IP Header(一般情況下TCP和IP 頭部都不包含選項(xiàng)字段),從而保證攜帶著TCP報(bào)文的IP數(shù)據(jù)報(bào)不會(huì)超過(guò)MTU,避免發(fā)生IP分片。


TCP連接過(guò)程

一般而言,TCP連接由客戶端發(fā)起,并通過(guò)三次握手建立連接(特殊情況是所謂同時(shí)打開(kāi))。
TCP關(guān)閉連接的時(shí)候,則可能是客戶端發(fā)起,也可能是服務(wù)器主動(dòng)發(fā)起(也可能是同時(shí)關(guān)閉,和同時(shí)打開(kāi)一樣,比較少見(jiàn))。

tcp連接流程圖

三次握手和四次分手
經(jīng)典的“三次握手”和“四次揮手”問(wèn)題
  • 為什么要三次握手?

    ??對(duì)于建鏈接的3次握手,主要是要初始化Sequence Number 的初始值。通信的雙方要互相通知對(duì)方自己的 初始化的Sequence Number(縮寫(xiě)為ISN:Inital Sequence Number)——所以叫SYN,全稱(chēng)Synchronize Sequence Numbers。也就上圖中的 x 和 y。這個(gè)號(hào)要作為以后的數(shù)據(jù)通信的序號(hào),以保證應(yīng)用層接收到的數(shù)據(jù)不會(huì)因?yàn)榫W(wǎng)絡(luò)上的傳輸?shù)膯?wèn)題而亂序(TCP會(huì)用這個(gè)序號(hào)來(lái)拼接數(shù)據(jù))。

  • 為什么要四次揮手?

    ??對(duì)于4次揮手,其實(shí)你仔細(xì)看是2次,因?yàn)門(mén)CP是全雙工的,所以,發(fā)送方和接收方都需要Fin和Ack。只不過(guò),有一方是被動(dòng)的,所以看上去就成了所謂的4次揮手。如果兩邊同時(shí)斷連接,那就會(huì)就進(jìn)入到CLOSING狀態(tài),然后到達(dá)TIME_WAIT狀態(tài)。

  • 為什么是三次握手?為什么不是兩次?

    第三次握手是為了防止失效的連接請(qǐng)求到達(dá)服務(wù)器,讓服務(wù)器錯(cuò)誤打開(kāi)連接。
    客戶端發(fā)送的連接請(qǐng)求如果在網(wǎng)絡(luò)中滯留,那么就會(huì)隔很長(zhǎng)一段時(shí)間才能收到服務(wù)器端發(fā)回的連接確認(rèn)。客戶端等待一個(gè)超時(shí)重傳時(shí)間之后,就會(huì)重新請(qǐng)求連接。但是這個(gè)滯留的連接請(qǐng)求最終還是會(huì)到達(dá)服務(wù)器,如果不進(jìn)行三次握手,那么服務(wù)器就會(huì)打開(kāi)兩個(gè)連接。如果有第三次握手,客戶端會(huì)忽略服務(wù)器之后發(fā)送的對(duì)滯留連接請(qǐng)求的連接確認(rèn),因此就不會(huì)再次打開(kāi)連接。

連接中的特殊狀態(tài)

  • 半關(guān)閉狀態(tài)

TCP作為全雙工連接,允許雙向的數(shù)據(jù)傳輸各自獨(dú)立的被關(guān)閉互不影響。通俗的說(shuō)就是,一端可以發(fā)送結(jié)束報(bào)文段FIN給對(duì)方,告訴它本端已經(jīng)發(fā)送完,但是還可以繼續(xù)接收來(lái)自對(duì)方的數(shù)據(jù)。此時(shí)這種單方向關(guān)閉的狀態(tài)稱(chēng)之為半關(guān)閉狀態(tài)


TCP狀態(tài)轉(zhuǎn)移

TCP狀態(tài)轉(zhuǎn)移圖

上半部分是TCP三路握手過(guò)程的狀態(tài)變遷,下半部分是TCP四次揮手過(guò)程的狀態(tài)變遷。

TCP狀態(tài)(11種):
eg.

netstat

CLOSED 初始狀態(tài),表示TCP連接是“關(guān)閉著的”或“未打開(kāi)的”。
LISTEN 表示服務(wù)器端的某個(gè)SOCKET處于監(jiān)聽(tīng)狀態(tài),可以接受客戶端的連接。
SYN_RECVD 表示服務(wù)器接收到了來(lái)自客戶端請(qǐng)求連接的SYN報(bào)文。在正常情況下,這個(gè)狀態(tài)是服務(wù) 器端的SOCKET在建立TCP連接時(shí)的三次握手會(huì)話過(guò)程中的一個(gè)中間狀態(tài),很短暫,基本上用netstat很難看到這種狀態(tài),除非故意寫(xiě)一個(gè)監(jiān)測(cè)程序,將三次TCP握手過(guò)程中最后一個(gè)ACK報(bào)文不予發(fā)送。當(dāng)TCP連接處于此狀態(tài)時(shí),再收到客戶端的ACK報(bào)文,它就會(huì)進(jìn)入到ESTABLISHED 狀態(tài)。
SYN_SENT 這個(gè)狀態(tài)與SYN_RCVD 狀態(tài)相呼應(yīng),當(dāng)客戶端SOCKET執(zhí)行connect()進(jìn)行連接時(shí),它首先發(fā)送SYN報(bào)文,然后隨即進(jìn)入到SYN_SENT 狀態(tài),并等待服務(wù)端的發(fā)送三次握手中的第2個(gè)報(bào)文。SYN_SENT 狀態(tài)表示客戶端已發(fā)送SYN報(bào)文。
ESTABLISHED 表示TCP連接已成功建立。

以上為T(mén)CP三次握手的狀態(tài)變遷


以下為T(mén)CP四次揮手的狀態(tài)變遷

FIN_WAIT_1 其實(shí)FIN_WAIT_1 和FIN_WAIT_2 兩種狀態(tài)的真正含義都是表示等待對(duì)方的FIN報(bào)文。而這兩種狀態(tài)的區(qū)別是:FIN_WAIT_1狀態(tài)實(shí)際上是當(dāng)SOCKET在ESTABLISHED狀態(tài)時(shí),它想主動(dòng)關(guān)閉連接,向?qū)Ψ桨l(fā)送了FIN報(bào)文,此時(shí)該SOCKET進(jìn)入到FIN_WAIT_1 狀態(tài)。而當(dāng)對(duì)方回應(yīng)ACK報(bào)文后,則進(jìn)入到FIN_WAIT_2 狀態(tài)。當(dāng)然在實(shí)際的正常情況下,無(wú)論對(duì)方處于任何種情況下,都應(yīng)該馬上回應(yīng)ACK報(bào)文,所以FIN_WAIT_1 狀態(tài)一般是比較難見(jiàn)到的,而FIN_WAIT_2 狀態(tài)有時(shí)仍可以用netstat看到。
FIN_WAIT_2 上面已經(jīng)解釋了這種狀態(tài)的由來(lái),實(shí)際上FIN_WAIT_2狀態(tài)下的SOCKET表示半連接,即有一方調(diào)用close()主動(dòng)要求關(guān)閉連接。注意:FIN_WAIT_2 是沒(méi)有超時(shí)的(不像TIME_WAIT 狀態(tài)),這種狀態(tài)下如果對(duì)方不關(guān)閉(不配合完成4次揮手過(guò)程),那這個(gè) FIN_WAIT_2 狀態(tài)將一直保持到系統(tǒng)重啟,越來(lái)越多的FIN_WAIT_2 狀態(tài)會(huì)導(dǎo)致內(nèi)核crash。
TIME_WAIT 表示收到了對(duì)方的FIN報(bào)文,并發(fā)送出了ACK報(bào)文。 TIME_WAIT狀態(tài)下的TCP連接會(huì)等待2*MSL(Max Segment Lifetime,最大分段生存期,指一個(gè)TCP報(bào)文在Internet上的最長(zhǎng)生存時(shí)間。每個(gè)具體的TCP協(xié)議實(shí)現(xiàn)都必須選擇一個(gè)確定的MSL值,RFC 1122建議是2分鐘,但BSD傳統(tǒng)實(shí)現(xiàn)采用了30秒,Linux可以cat /proc/sys/net/ipv4/tcp_fin_timeout看到本機(jī)的這個(gè)值),然后即可回到CLOSED 可用狀態(tài)了。如果FIN_WAIT_1狀態(tài)下,收到了對(duì)方同時(shí)帶FIN標(biāo)志和ACK標(biāo)志的報(bào)文時(shí),可以直接進(jìn)入到TIME_WAIT狀態(tài),而無(wú)須經(jīng)過(guò)FIN_WAIT_2狀態(tài)。(這種情況應(yīng)該就是四次揮手變成三次揮手的那種情況)
CLOSING 這種狀態(tài)在實(shí)際情況中應(yīng)該很少見(jiàn),屬于一種比較罕見(jiàn)的例外狀態(tài)。正常情況下,當(dāng)一方發(fā)送FIN報(bào)文后,按理來(lái)說(shuō)是應(yīng)該先收到(或同時(shí)收到)對(duì)方的ACK報(bào)文,再收到對(duì)方的FIN報(bào)文。但是CLOSING 狀態(tài)表示一方發(fā)送FIN報(bào)文后,并沒(méi)有收到對(duì)方的ACK報(bào)文,反而卻也收到了對(duì)方的FIN報(bào)文。什么情況下會(huì)出現(xiàn)此種情況呢?那就是當(dāng)雙方幾乎在同時(shí)close()一個(gè)SOCKET的話,就出現(xiàn)了雙方同時(shí)發(fā)送FIN報(bào)文的情況,這是就會(huì)出現(xiàn)CLOSING 狀態(tài),表示雙方都正在關(guān)閉SOCKET連接。
CLOSE_WAIT 表示正在等待關(guān)閉。怎么理解呢?當(dāng)對(duì)方close()一個(gè)SOCKET后發(fā)送FIN報(bào)文給自己,你的系統(tǒng)毫無(wú)疑問(wèn)地將會(huì)回應(yīng)一個(gè)ACK報(bào)文給對(duì)方,此時(shí)TCP連接則進(jìn)入到CLOSE_WAIT狀態(tài)。接下來(lái)呢,你需要檢查自己是否還有數(shù)據(jù)要發(fā)送給對(duì)方,如果沒(méi)有的話,那你也就可以close()這個(gè)SOCKET并發(fā)送FIN報(bào)文給對(duì)方,即關(guān)閉自己到對(duì)方這個(gè)方向的連接。有數(shù)據(jù)的話則看程序的策略,繼續(xù)發(fā)送或丟棄。簡(jiǎn)單地說(shuō),當(dāng)你處于CLOSE_WAIT 狀態(tài)下,需要完成的事情是等待你去關(guān)閉連接。
LAST_ACK 當(dāng)被動(dòng)關(guān)閉的一方在發(fā)送FIN報(bào)文后,等待對(duì)方的ACK報(bào)文的時(shí)候,就處于LAST_ACK 狀態(tài)。當(dāng)收到對(duì)方的ACK報(bào)文后,也就可以進(jìn)入到CLOSED 可用狀態(tài)了。

 1. 服務(wù)端狀態(tài)轉(zhuǎn)移過(guò)程 

服務(wù)器通過(guò)listen系統(tǒng)調(diào)用進(jìn)入LISTEN狀態(tài),被動(dòng)等待客戶端連接,也就是所謂的被動(dòng)打開(kāi)。一旦監(jiān)聽(tīng)到SYN(同步報(bào)文段)請(qǐng)求,就將該連接放入內(nèi)核的等待隊(duì)列,并向客戶端發(fā)送帶SYN的ACK(確認(rèn)報(bào)文段),此時(shí)該連接處于SYN_RECVD狀態(tài)。如果服務(wù)器收到客戶端返回的ACK,則轉(zhuǎn)到ESTABLISHED狀態(tài)。這個(gè)狀態(tài)就是連接雙方能進(jìn)行全雙工數(shù)據(jù)傳輸?shù)臓顟B(tài)。
而當(dāng)客戶端主動(dòng)關(guān)閉連接時(shí),服務(wù)器收到FIN報(bào)文,通過(guò)返回ACK使連接進(jìn)入CLOSE_WAIT狀態(tài)。此狀態(tài)表示——等待服務(wù)器應(yīng)用程序關(guān)閉連接。通常,服務(wù)器檢測(cè)到客戶端關(guān)閉連接之后,也會(huì)立即給客戶端發(fā)送一個(gè)FIN來(lái)關(guān)閉連接,使連接轉(zhuǎn)移到LAST_ACK狀態(tài),等待客戶端對(duì)最后一個(gè)FIN結(jié)束報(bào)文段的最后一次確認(rèn),一旦確認(rèn)完成,連接就徹底關(guān)閉了。

2. 客戶端狀態(tài)轉(zhuǎn)移過(guò)程

客戶端通過(guò)connect系統(tǒng)調(diào)用主動(dòng)與服務(wù)器建立連接。此系統(tǒng)調(diào)用會(huì)首先給服務(wù)器發(fā)一個(gè)SYN,使連接進(jìn)入SYN_SENT狀態(tài)。
connect調(diào)用可能因?yàn)閮煞N原因失敗:1. 目標(biāo)端口不存在(未被任何進(jìn)程監(jiān)聽(tīng))護(hù)著該端口被TIME_WAIT狀態(tài)的連接占用(詳見(jiàn)后文)。2. 連接超時(shí),在超時(shí)時(shí)間內(nèi)未收到服務(wù)器的ACK。
如果connect調(diào)用失敗,則連接返回初始的CLOSED狀態(tài),如果調(diào)用成功,則轉(zhuǎn)到ESTABLISHED狀態(tài)。
客戶端執(zhí)行主動(dòng)關(guān)閉時(shí),它會(huì)向服務(wù)器發(fā)送一個(gè)FIN,連接進(jìn)入TIME_WAIT_1狀態(tài),如果收到服務(wù)器的ACK,進(jìn)入TIME_WAIT_2狀態(tài)。此時(shí)服務(wù)器處于CLOSE_WAIT狀態(tài),這一對(duì)狀態(tài)是可能發(fā)生辦關(guān)閉的狀態(tài)(詳見(jiàn)后文)。此時(shí)如果服務(wù)器發(fā)送FIN關(guān)閉連接,則客戶端會(huì)發(fā)送ACK進(jìn)行確認(rèn)并進(jìn)入TIME_WAIT狀態(tài)。

TIME_WAIT狀態(tài)存在的意義,為什么不是直接_CLOSED?

客戶端收到服務(wù)器的FIN報(bào)文之后,并不直接進(jìn)入CLOSED狀態(tài),而是TIME_WAIT狀態(tài)。客戶端會(huì)在此狀態(tài)等等2MSL的時(shí)長(zhǎng)之后,才會(huì)徹底關(guān)閉。(MSL是Maximum Segment Life,報(bào)文段最大生存時(shí)間,一般為2分鐘。

TIME_WAIT狀態(tài)存在的原因有兩點(diǎn):

  • 可靠的終止TCP連接
    如果 B 沒(méi)收到 A 發(fā)送來(lái)的確認(rèn)報(bào)文,那么就會(huì)重新發(fā)送連接釋放請(qǐng)求報(bào)文,A 等待一段時(shí)間就是為了處理這種情況的發(fā)生
  • 保證讓遲到的TCP報(bào)文段有足夠的時(shí)間被識(shí)別并丟棄
    在Linux系統(tǒng)中,一個(gè)TCP端口不能被同時(shí)打開(kāi)多次。當(dāng)一個(gè)TCP連接處于TIME_WAIT狀態(tài)時(shí),我們無(wú)法使用此接口來(lái)建立新連接。如果不存在此狀態(tài),則可以建立一個(gè)和剛關(guān)閉的連接具有相同IP和端口的連接,也就是原來(lái)連接的化身。此化身可以收到屬于原來(lái)連接的在網(wǎng)絡(luò)中滯留遲到的報(bào)文段,這顯然不應(yīng)該存在,故需要TIME_WAIT狀態(tài)。
    另外,因?yàn)镸SL是一個(gè)TCP報(bào)文的最大生存時(shí)間,所以2MSL的時(shí)間可以保證雙向的數(shù)據(jù)都發(fā)送完畢,遲到的報(bào)文都已消失(被中轉(zhuǎn)路由器丟棄)。所以2MSL時(shí)間之后新的連接可以絕對(duì)安全的建立,這就是TIME_WAIT狀態(tài)需要持續(xù)2MSL的原因。
    但是,有時(shí)候我們希望避免TIME_WAIT狀態(tài),因?yàn)楫?dāng)程序退出后,我們希望能立即重啟它,因?yàn)榇藸顟B(tài)的存在,我們是無(wú)法立即重啟的。
    對(duì)于客戶端而言,我們一般不需要擔(dān)心此問(wèn)題。因?yàn)門(mén)CP連接中,客戶端通常使用的是系統(tǒng)自動(dòng)分配的臨時(shí)端口號(hào)來(lái)建立連接,這個(gè)端口號(hào)是隨機(jī)的,所以一般不會(huì)和上次的重復(fù)。
    但是對(duì)于服務(wù)端而言,如果是服務(wù)器主動(dòng)關(guān)閉連接然后異常終止,因?yàn)榉?wù)器提供服務(wù)的總是同一個(gè)知名端口號(hào),則會(huì)出現(xiàn)不能立即重啟的情況。我們可以通過(guò)socket選項(xiàng)的SO_REUSEADDR來(lái)強(qiáng)制進(jìn)程立即使用處于TIME_WAIT狀態(tài)的連接占用的端口,這涉及到Linux網(wǎng)絡(luò)編程,這里暫不討論。

TCP流量控制 —— 滑動(dòng)窗口(Sliding Window)

流量控制是為了控制發(fā)送方發(fā)送速率,保證接收方來(lái)得及接收。

接收方發(fā)送的確認(rèn)報(bào)文中的窗口字段可以用來(lái)控制發(fā)送方窗口大小,從而影響發(fā)送方的發(fā)送速率。將窗口字段設(shè)置為 0,則發(fā)送方不能發(fā)送數(shù)據(jù)。

窗口是緩存的一部分,用來(lái)暫時(shí)存放字節(jié)流。發(fā)送方和接收方各有一個(gè)窗口,接收方通過(guò) TCP 報(bào)文段中的窗口字段告訴發(fā)送方自己的窗口大小,發(fā)送方根據(jù)這個(gè)值和其它信息設(shè)置自己的窗口大小。
發(fā)送窗口內(nèi)的字節(jié)都允許被發(fā)送,接收窗口內(nèi)的字節(jié)都允許被接收。如果發(fā)送窗口左部的字節(jié)已經(jīng)發(fā)送并且收到了確認(rèn),那么就將發(fā)送窗口向右滑動(dòng)一定距離,直到左部第一個(gè)字節(jié)不是已發(fā)送并且已確認(rèn)的狀態(tài);接收窗口的滑動(dòng)類(lèi)似,接收窗口左部字節(jié)已經(jīng)發(fā)送確認(rèn)并交付主機(jī),就向右滑動(dòng)接收窗口。
接收窗口只會(huì)對(duì)窗口內(nèi)最后一個(gè)按序到達(dá)的字節(jié)進(jìn)行確認(rèn),例如接收窗口已經(jīng)收到的字節(jié)為 {31, 34, 35},其中 {31} 按序到達(dá),而 {34, 35} 就不是,因此只對(duì)字節(jié) 31 進(jìn)行確認(rèn)。發(fā)送方得到一個(gè)字節(jié)的確認(rèn)之后,就知道這個(gè)字節(jié)之前的所有字節(jié)都已經(jīng)被接收。


A發(fā)送了11個(gè)字節(jié)的數(shù)據(jù)

TCP擁塞控制

如果網(wǎng)絡(luò)出現(xiàn)擁塞,分組將會(huì)丟失,此時(shí)發(fā)送方會(huì)繼續(xù)重傳,從而導(dǎo)致網(wǎng)絡(luò)擁塞程度更高。因此當(dāng)出現(xiàn)擁塞時(shí),應(yīng)當(dāng)控制發(fā)送方的速率。這一點(diǎn)和流量控制很像,但是出發(fā)點(diǎn)不同。流量控制是為了讓接收方能來(lái)得及接收,而擁塞控制是為了降低整個(gè)網(wǎng)絡(luò)的擁塞程度。

擁塞控制所起的作用

TCP 主要通過(guò)四種算法來(lái)進(jìn)行擁塞控制:慢開(kāi)始、擁塞避免、快重傳、快恢復(fù)。

在Linux下有多種實(shí)現(xiàn),比如reno算法,vegas算法和cubic算法等。
發(fā)送方需要維護(hù)一個(gè)叫做擁塞窗口(cwnd)的狀態(tài)變量,注意擁塞窗口與發(fā)送方窗口的區(qū)別:擁塞窗口只是一個(gè)狀態(tài)變量,實(shí)際決定發(fā)送方能發(fā)送多少數(shù)據(jù)的是發(fā)送方窗口。
為了便于討論,做如下假設(shè):

  • 接收方有足夠大的接收緩存,因此不會(huì)發(fā)生流量控制;
  • 雖然 TCP 的窗口基于字節(jié),但是這里設(shè)窗口的大小單位為報(bào)文段。


    TCP擁塞狀況cwnd在擁塞控制時(shí)的變化情況

1. 慢開(kāi)始與擁塞避免

發(fā)送的最初執(zhí)行慢開(kāi)始,令 cwnd=1,發(fā)送方只能發(fā)送 1 個(gè)報(bào)文段;當(dāng)收到確認(rèn)后,將 cwnd 加倍,因此之后發(fā)送方能夠發(fā)送的報(bào)文段數(shù)量為:2、4、8 ...

注意到慢開(kāi)始每個(gè)輪次都將 cwnd 加倍,這樣會(huì)讓 cwnd 增長(zhǎng)速度非常快,從而使得發(fā)送方發(fā)送的速度增長(zhǎng)速度過(guò)快,網(wǎng)絡(luò)擁塞的可能也就更高。設(shè)置一個(gè)慢開(kāi)始門(mén)限 ssthresh,當(dāng) cwnd >= ssthresh 時(shí),進(jìn)入擁塞避免,每個(gè)輪次只將 cwnd 加 1。

如果出現(xiàn)了超時(shí),則令 ssthresh = cwnd/2,然后重新執(zhí)行慢開(kāi)始。

2. 快重傳與快恢復(fù)

在接收方,要求每次接收到報(bào)文段都應(yīng)該對(duì)最后一個(gè)已收到的有序報(bào)文段進(jìn)行確認(rèn)。例如已經(jīng)接收到 M1 和 M2,此時(shí)收到 M4,應(yīng)當(dāng)發(fā)送對(duì) M2 的確認(rèn)。

在發(fā)送方,如果收到三個(gè)重復(fù)確認(rèn),那么可以知道下一個(gè)報(bào)文段丟失,此時(shí)執(zhí)行快重傳,立即重傳下一個(gè)報(bào)文段。例如收到三個(gè) M2,則 M3 丟失,立即重傳 M3。

在這種情況下,只是丟失個(gè)別報(bào)文段,而不是網(wǎng)絡(luò)擁塞。因此執(zhí)行快恢復(fù),令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此時(shí)直接進(jìn)入擁塞避免。

慢開(kāi)始和快恢復(fù)的快慢指的是 cwnd 的設(shè)定值,而不是 cwnd 的增長(zhǎng)速率。慢開(kāi)始 cwnd 設(shè)定為 1,而快恢復(fù) cwnd 設(shè)定為 ssthresh。


快重傳示意圖

TCP的可靠傳輸是怎么做到的

1. 發(fā)送應(yīng)答

??發(fā)送端的每個(gè)TCP報(bào)文都必須得到接收方的應(yīng)答,才算傳輸成功。

2. 超時(shí)重傳

??TCP為每個(gè)TCP報(bào)文段都維護(hù)一個(gè)重傳定時(shí)器。
??發(fā)送端在發(fā)出一個(gè)TCP報(bào)文段之后就啟動(dòng)定時(shí)器,如果在定時(shí)時(shí)間類(lèi)未收到應(yīng)答,它就將重發(fā)該報(bào)文段并重置定時(shí)器。

3. 報(bào)文重排

??因?yàn)門(mén)CP報(bào)文段最終在網(wǎng)絡(luò)層是以IP數(shù)據(jù)報(bào)的形式發(fā)送,而IP數(shù)據(jù)報(bào)到達(dá)接收端可能是亂序或者重復(fù)的。TCP協(xié)議會(huì)對(duì)收到的TCP報(bào)文進(jìn)行重排、整理,確保順序正確。


TCP的數(shù)據(jù)流

TCP報(bào)文段所攜帶的應(yīng)用程序數(shù)據(jù)按照長(zhǎng)度分為兩種:交互數(shù)據(jù)成塊數(shù)據(jù)

  • 交互數(shù)據(jù) 用于進(jìn)行信息交互,僅包含很少的字節(jié),使用交互數(shù)據(jù)的程序或協(xié)議對(duì)實(shí)時(shí)性要求很高,比如telnet、ssh等
  • 成塊數(shù)據(jù) 用于大量數(shù)據(jù)傳輸,長(zhǎng)度通常為T(mén)CP報(bào)文段所允許的最大數(shù)據(jù)長(zhǎng)度,使用成塊數(shù)據(jù)的應(yīng)用程序?qū)鬏斝室蟾?,比如ftp等。

TCP一些有趣的問(wèn)題 —— 粘包/拆包等

什么是粘包拆包?

對(duì)于什么是粘包、拆包問(wèn)題,我想先舉兩個(gè)簡(jiǎn)單的應(yīng)用場(chǎng)景:

  1. 客戶端和服務(wù)器建立一個(gè)連接,客戶端發(fā)送一條消息,客戶端關(guān)閉與服務(wù)端的連接。
  2. 客戶端和服務(wù)器簡(jiǎn)歷一個(gè)連接,客戶端連續(xù)發(fā)送兩條消息,客戶端關(guān)閉與服務(wù)端的連接。

對(duì)于第一種情況,服務(wù)端的處理流程可以是這樣的:當(dāng)客戶端與服務(wù)端的連接建立成功之后,服務(wù)端不斷讀取客戶端發(fā)送過(guò)來(lái)的數(shù)據(jù),當(dāng)客戶端與服務(wù)端連接斷開(kāi)之后,服務(wù)端知道已經(jīng)讀完了一條消息,然后進(jìn)行解碼和后續(xù)處理...。對(duì)于第二種情況,如果按照上面相同的處理邏輯來(lái)處理,那就有問(wèn)題了,我們來(lái)看看第二種情況下客戶端發(fā)送的兩條消息遞交到服務(wù)端有可能出現(xiàn)的情況:

第一種情況:

服務(wù)端一共讀到兩個(gè)數(shù)據(jù)包,第一個(gè)包包含客戶端發(fā)出的第一條消息的完整信息,第二個(gè)包包含客戶端發(fā)出的第二條消息,那這種情況比較好處理,服務(wù)器只需要簡(jiǎn)單的從網(wǎng)絡(luò)緩沖區(qū)去讀就好了,第一次讀到第一條消息的完整信息,消費(fèi)完再?gòu)木W(wǎng)絡(luò)緩沖區(qū)將第二條完整消息讀出來(lái)消費(fèi)。

沒(méi)有發(fā)生粘包、拆包示意圖

第二種情況:

服務(wù)端一共就讀到一個(gè)數(shù)據(jù)包,這個(gè)數(shù)據(jù)包包含客戶端發(fā)出的兩條消息的完整信息,這個(gè)時(shí)候基于之前邏輯實(shí)現(xiàn)的服務(wù)端就蒙了,因?yàn)榉?wù)端不知道第一條消息從哪兒結(jié)束和第二條消息從哪兒開(kāi)始,這種情況其實(shí)是發(fā)生了TCP粘包。

TCP粘包示意圖

第三種情況:

服務(wù)端一共收到了兩個(gè)數(shù)據(jù)包,第一個(gè)數(shù)據(jù)包只包含了第一條消息的一部分,第一條消息的后半部分和第二條消息都在第二個(gè)數(shù)據(jù)包中,或者是第一個(gè)數(shù)據(jù)包包含了第一條消息的完整信息和第二條消息的一部分信息,第二個(gè)數(shù)據(jù)包包含了第二條消息的剩下部分,這種情況其實(shí)是發(fā)送了TCP拆,因?yàn)榘l(fā)生了一條消息被拆分在兩個(gè)包里面發(fā)送了,同樣上面的服務(wù)器邏輯對(duì)于這種情況是不好處理的。

TCP拆包示意圖

產(chǎn)生tcp粘包和拆包的原因

我們知道tcp是以流動(dòng)的方式傳輸數(shù)據(jù),傳輸?shù)淖钚挝粸橐粋€(gè)報(bào)文段(segment)。tcp Header中有個(gè)Options標(biāo)識(shí)位,常見(jiàn)的標(biāo)識(shí)為mss(Maximum Segment Size)指的是,連接層每次傳輸?shù)臄?shù)據(jù)有個(gè)最大限制MTU(Maximum Transmission Unit),一般是1500比特,超過(guò)這個(gè)量要分成多個(gè)報(bào)文段,mss則是這個(gè)最大限制減去TCP的header,光是要傳輸?shù)臄?shù)據(jù)的大小,一般為1460比特。換算成字節(jié),也就是180多字節(jié)。

tcp為提高性能,發(fā)送端會(huì)將需要發(fā)送的數(shù)據(jù)發(fā)送到緩沖區(qū),等待緩沖區(qū)滿了之后,再將緩沖中的數(shù)據(jù)發(fā)送到接收方。同理,接收方也有緩沖區(qū)這樣的機(jī)制,來(lái)接收數(shù)據(jù)。

發(fā)生TCP粘包、拆包主要是由于下面一些原因:

  • 應(yīng)用程序?qū)懭氲臄?shù)據(jù)大于套接字緩沖區(qū)大小,這將會(huì)發(fā)生拆包。
  • 應(yīng)用程序?qū)懭霐?shù)據(jù)小于套接字緩沖區(qū)大小,網(wǎng)卡將應(yīng)用多次寫(xiě)入的數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)上,這將會(huì)發(fā)生粘包。
  • 進(jìn)行mss(最大報(bào)文長(zhǎng)度)大小的TCP分段,當(dāng)TCP報(bào)文長(zhǎng)度-TCP頭部長(zhǎng)度>mss的時(shí)候?qū)l(fā)生拆包。
  • 接收方法不及時(shí)讀取套接字緩沖區(qū)數(shù)據(jù),這將發(fā)生粘包。
    ……

如何解決拆包粘包

既然知道了tcp是無(wú)界的數(shù)據(jù)流,且協(xié)議本身無(wú)法避免粘包,拆包的發(fā)生,那我們只能在應(yīng)用層數(shù)據(jù)協(xié)議上,加以控制。通常在制定傳輸數(shù)據(jù)時(shí),可以使用如下方法:

  • 使用帶消息頭的協(xié)議、消息頭存儲(chǔ)消息開(kāi)始標(biāo)識(shí)及消息長(zhǎng)度信息,服務(wù)端獲取消息頭的時(shí)候解析出消息長(zhǎng)度,然后向后讀取該長(zhǎng)度的內(nèi)容。
  • 設(shè)置定長(zhǎng)消息,服務(wù)端每次讀取既定長(zhǎng)度的內(nèi)容作為一條完整消息。
  • 設(shè)置消息邊界,服務(wù)端從網(wǎng)絡(luò)流中按消息編輯分離出消息內(nèi)容。

總結(jié)

寫(xiě)了一個(gè)簡(jiǎn)單的golang版的tcp服務(wù)器實(shí)例,僅供參考:
例子


參考和推薦閱讀書(shū)目:

  • 《TCP/IP協(xié)議詳解:卷一》
  • 《Linux高性能服務(wù)器編程》
  • 《圖解TCP/IP》

注釋?zhuān)?/p>

eg.

/etc/services


  1. ?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容