TCP那些事兒

目錄:

  • TCP是什么
  • TCP報文結構
  • TCP連接過程
  • TCP狀態(tài)轉移
  • TCP流量控制 —— 滑動窗口
  • TCP擁塞控制
  • TCP可靠傳輸是怎么做到的
  • TCP一些有趣的問題 —— 粘包/拆包等

為什么我們需要了解TCP

以前我也認為TCP是相當?shù)讓拥臇|西,我永遠不需要去了解它秤掌。雖然差不多是這樣,但是實際生活中,你依然可能遇見和TCP算法相關的bug定欧,這時候懂一些TCP的知識就至關重要了。(本文也可以引申為沮协,系統(tǒng)調用抛姑,操作系統(tǒng)這些都很重要,這個道理適用于很多東西
這里推薦一篇小短文榔昔,人人都應該懂點TCP

TCP是什么

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

TCP通信基本流程

使用TCP協(xié)議通信的雙方必須先建立TCP連接驹闰,并在內(nèi)核中為該連接維持一些必要的數(shù)據(jù)結構,比如連接的狀態(tài)撒会、讀寫緩沖區(qū)嘹朗、定時器等。當通信結束時诵肛,雙方必須關閉連接以釋放這些內(nèi)核數(shù)據(jù)屹培。TCP服務基于流,源源不斷從一端流向另一端怔檩,發(fā)送端可以逐字節(jié)寫入惫谤,接收端可以逐字節(jié)讀出,無需分段珠洗。

特點

  • 位于傳輸層,基本傳輸結構是TCP報文段(TCP message segment
  • 面向連接 一對一若专,端對端许蓖,進程與進程之間通信,不適用于廣播调衰、多播程序
  • 可靠傳輸 —— 發(fā)送應答/超時重傳/報文排序/流量控制/擁塞處理
  • 端對端 進程和進程之間 端口和端口之間
  • 全雙工通信
  • 基于字節(jié)流服務 數(shù)據(jù)發(fā)送和接收沒有邊界限制和分段
這里解釋下字節(jié)流的概念:
TCP是基于字節(jié)流服務膊爪,而UDP則是基于數(shù)據(jù)報服務。對應到實際的編程中表        
現(xiàn)為通信雙方是否必須執(zhí)行相同次數(shù)的讀寫操作嚎莉。當發(fā)送端連續(xù)執(zhí)行多次寫操作時米酬,TCP模塊會先把這些
數(shù)據(jù)放入`發(fā)送緩沖區(qū)`。當真正開始發(fā)送數(shù)據(jù)的時候趋箩,發(fā)送緩沖區(qū)中的數(shù)據(jù)可能被封裝成一個或者多個TCP
報文段發(fā)出赃额。TCP報文段的個數(shù)和寫操作次數(shù)沒有關系。
當接收端收到一個或者多個TCP報文段后叫确,TCP模塊講它們攜帶的數(shù)據(jù)放入TCP`接收緩沖區(qū)`中跳芳,并通知
應用程序讀取數(shù)據(jù)≈衩悖可以一次性全部讀出飞盆,也可以分多次讀出。接收到的報文個數(shù)和讀次數(shù)也沒有關系。
綜上吓歇,這就是字節(jié)流的概念:應用程序對數(shù)據(jù)的發(fā)送和接收沒有邊界限制孽水。
相對的,UDP則是應用程序沒執(zhí)行一次寫操作城看,UDP模塊就將其封裝成一個UDP數(shù)據(jù)報并發(fā)送之女气,接收端
每收到一個UDP數(shù)據(jù)報就必須進行一次讀操作,否則會丟包析命。

TCP報文結構

TCP首部結構

tcp報文header格式

英文版

需要注意的幾點:

  • tcp的包沒有ip地址 那是ip層的事主卫,但是有源端口和目標端口
  • 一個tcp連接用一個四元組來表示(src_ip, src_port,dst_ip,dst_port)ps:準確說是五元組,加上一個協(xié)議
  • 幾個名詞
  • 固定長度為20字節(jié)鹃愤,包含變長部分簇搅,最大為60字節(jié)
    • Sequence NumberSeq软吐,包的序號瘩将,用于解決包傳輸過程中的亂序問題
    • Acknowledgement NumberAck,確認號凹耙,用于確認包收到姿现,用于解決丟包問題
    • Synchronize Sequence NumbersSYN 同步序號 用于包同步
    • Window 窗口,即著名的滑動窗口(Silding Window)肖抱,用于流量擁塞控制
    • TCP Flag RST/SYN/FIN等备典,即包的類型,用于操控tcp的各種狀態(tài)
具體分析:
  • 端口號:16位意述。 一般服務端會使用知名端口號提佣,而客戶端一般使用系統(tǒng)自動選擇的臨時端口號。所有知名服務所使用的端口號都定義在 /etc/services文件中[1]荤崇。
  • Seq序號:32位拌屏。一次TCP通信過程中王某一個傳輸方向上的字節(jié)流中每個字節(jié)的編號。第一個報文段Seq會被初始化為ISN(Initial Sequence Number初始序號)术荤,這是一個隨機值倚喂,后續(xù)的報文段中序號值將為ISN+報文第一個字節(jié)的偏移值。

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

  • Ack序號:32位端圈。用來對另一方發(fā)來的TCP報文進行響應。值為seq+1
  • Offset:4位子库,標識TCP頭部的長度枫笛,有多少字(32-bit words)因為是4位,TCP頭部最大為60字節(jié)刚照。(4位最大15,15*4=60)
  • 六個標志位:TCP Flags.
  • URG 表示緊急指針(urgent pointer)是否有效
  • ACK 表示確認號是否有效刑巧。帶ACK標志的TCP報文段叫確認報文段
  • PSH 提示接收端應該立即從TCP接收緩沖區(qū)中讀走數(shù)據(jù)喧兄,為后續(xù)數(shù)據(jù)騰出緩沖區(qū)空間。
  • RST 復位標識啊楚,表示要求對方重建連接吠冤。帶RST標志的TCP報文段叫復位報文段,一般用于異常終止連接恭理,一旦發(fā)送了RST報文段拯辙,發(fā)送端所有排隊等待的數(shù)據(jù)都會被丟棄。
  • SYN 表示請求建立連接颜价。帶SYN標志的TCP報文段叫同步報文段
  • FIN 表示通知對方本端要關閉連接了涯保。帶FIN標志的TCP報文段叫結束報文段
  • 窗口window:TCP流量控制的一個手段。指接收窗口的大小周伦。它告訴對方本端的TCP接收緩沖區(qū)還能容納多少字節(jié)數(shù)據(jù)夕春,這樣可以讓對端控制發(fā)送數(shù)據(jù)的速度。
  • checksum:16位校驗和专挪。由發(fā)送端填充及志,接收端對這個字段用CRC校驗,校驗TCP報文在傳輸過程中是否損壞(頭部和數(shù)據(jù)部分都會被校驗)寨腔。這是TCP可靠傳輸?shù)闹匾U稀?/li>
  • urgent pointer:緊急指針速侈,用于發(fā)送端向接收端發(fā)送緊急數(shù)據(jù)。

Options: TCP頭部最后一個選項字段options是一個變長的可選字段迫卢,最多包含40byte倚搬,這也是TCP頭部最長為60字節(jié)的原因。
options包含的字段非常多乾蛤,這里僅選取比較重要的幾個字段講一下每界。
options的第一個字段kind表示選項的類型。其中幻捏,kind=2是最大報文長度選項,簡稱MSS命咐。傳輸層每次傳輸數(shù)據(jù)有個最大限制MTU(Maximum Transmission Unit)篡九。而TCP模塊通常會將MSS設置為(MTU-40)字節(jié),減掉的這40byte = 20 byte的TCP Header + 20 byte的IP Header(一般情況下TCP和IP 頭部都不包含選項字段)醋奠,從而保證攜帶著TCP報文的IP數(shù)據(jù)報不會超過MTU榛臼,避免發(fā)生IP分片。


TCP連接過程

一般而言窜司,TCP連接由客戶端發(fā)起沛善,并通過三次握手建立連接(特殊情況是所謂同時打開)。
TCP關閉連接的時候塞祈,則可能是客戶端發(fā)起金刁,也可能是服務器主動發(fā)起(也可能是同時關閉,和同時打開一樣,比較少見)尤蛮。

tcp連接流程圖

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

    ??對于建鏈接的3次握手,主要是要初始化Sequence Number 的初始值产捞。通信的雙方要互相通知對方自己的 初始化的Sequence Number(縮寫為ISN:Inital Sequence Number)——所以叫SYN醇锚,全稱Synchronize Sequence Numbers。也就上圖中的 x 和 y坯临。這個號要作為以后的數(shù)據(jù)通信的序號焊唬,以保證應用層接收到的數(shù)據(jù)不會因為網(wǎng)絡上的傳輸?shù)膯栴}而亂序(TCP會用這個序號來拼接數(shù)據(jù))。

  • 為什么要四次揮手看靠?

    ??對于4次揮手赶促,其實你仔細看是2次,因為TCP是全雙工的衷笋,所以芳杏,發(fā)送方和接收方都需要Fin和Ack。只不過辟宗,有一方是被動的爵赵,所以看上去就成了所謂的4次揮手。如果兩邊同時斷連接泊脐,那就會就進入到CLOSING狀態(tài)空幻,然后到達TIME_WAIT狀態(tài)。

  • 為什么是三次握手容客?為什么不是兩次秕铛?

    第三次握手是為了防止失效的連接請求到達服務器,讓服務器錯誤打開連接缩挑。
    客戶端發(fā)送的連接請求如果在網(wǎng)絡中滯留但两,那么就會隔很長一段時間才能收到服務器端發(fā)回的連接確認」┲茫客戶端等待一個超時重傳時間之后谨湘,就會重新請求連接。但是這個滯留的連接請求最終還是會到達服務器芥丧,如果不進行三次握手紧阔,那么服務器就會打開兩個連接。如果有第三次握手续担,客戶端會忽略服務器之后發(fā)送的對滯留連接請求的連接確認擅耽,因此就不會再次打開連接。

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

  • 半關閉狀態(tài)

TCP作為全雙工連接物遇,允許雙向的數(shù)據(jù)傳輸各自獨立的被關閉互不影響乖仇。通俗的說就是憾儒,一端可以發(fā)送結束報文段FIN給對方,告訴它本端已經(jīng)發(fā)送完这敬,但是還可以繼續(xù)接收來自對方的數(shù)據(jù)航夺。此時這種單方向關閉的狀態(tài)稱之為半關閉狀態(tài)


TCP狀態(tài)轉移

TCP狀態(tài)轉移圖

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

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

netstat

CLOSED 初始狀態(tài)阳掐,表示TCP連接是“關閉著的”或“未打開的”。
LISTEN 表示服務器端的某個SOCKET處于監(jiān)聽狀態(tài)冷蚂,可以接受客戶端的連接缭保。
SYN_RECVD 表示服務器接收到了來自客戶端請求連接的SYN報文。在正常情況下蝙茶,這個狀態(tài)是服務 器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態(tài)艺骂,很短暫,基本上用netstat很難看到這種狀態(tài)隆夯,除非故意寫一個監(jiān)測程序钳恕,將三次TCP握手過程中最后一個ACK報文不予發(fā)送。當TCP連接處于此狀態(tài)時蹄衷,再收到客戶端的ACK報文忧额,它就會進入到ESTABLISHED 狀態(tài)。
SYN_SENT 這個狀態(tài)與SYN_RCVD 狀態(tài)相呼應愧口,當客戶端SOCKET執(zhí)行connect()進行連接時睦番,它首先發(fā)送SYN報文,然后隨即進入到SYN_SENT 狀態(tài)耍属,并等待服務端的發(fā)送三次握手中的第2個報文托嚣。SYN_SENT 狀態(tài)表示客戶端已發(fā)送SYN報文。
ESTABLISHED 表示TCP連接已成功建立厚骗。

以上為TCP三次握手的狀態(tài)變遷


以下為TCP四次揮手的狀態(tài)變遷

FIN_WAIT_1 其實FIN_WAIT_1 和FIN_WAIT_2 兩種狀態(tài)的真正含義都是表示等待對方的FIN報文示启。而這兩種狀態(tài)的區(qū)別是:FIN_WAIT_1狀態(tài)實際上是當SOCKET在ESTABLISHED狀態(tài)時,它想主動關閉連接领舰,向對方發(fā)送了FIN報文夫嗓,此時該SOCKET進入到FIN_WAIT_1 狀態(tài)。而當對方回應ACK報文后提揍,則進入到FIN_WAIT_2 狀態(tài)啤月。當然在實際的正常情況下煮仇,無論對方處于任何種情況下劳跃,都應該馬上回應ACK報文,所以FIN_WAIT_1 狀態(tài)一般是比較難見到的浙垫,而FIN_WAIT_2 狀態(tài)有時仍可以用netstat看到刨仑。
FIN_WAIT_2 上面已經(jīng)解釋了這種狀態(tài)的由來郑诺,實際上FIN_WAIT_2狀態(tài)下的SOCKET表示半連接,即有一方調用close()主動要求關閉連接杉武。注意:FIN_WAIT_2 是沒有超時的(不像TIME_WAIT 狀態(tài))辙诞,這種狀態(tài)下如果對方不關閉(不配合完成4次揮手過程),那這個 FIN_WAIT_2 狀態(tài)將一直保持到系統(tǒng)重啟轻抱,越來越多的FIN_WAIT_2 狀態(tài)會導致內(nèi)核crash飞涂。
TIME_WAIT 表示收到了對方的FIN報文,并發(fā)送出了ACK報文祈搜。 TIME_WAIT狀態(tài)下的TCP連接會等待2*MSL(Max Segment Lifetime较店,最大分段生存期,指一個TCP報文在Internet上的最長生存時間容燕。每個具體的TCP協(xié)議實現(xiàn)都必須選擇一個確定的MSL值梁呈,RFC 1122建議是2分鐘,但BSD傳統(tǒng)實現(xiàn)采用了30秒蘸秘,Linux可以cat /proc/sys/net/ipv4/tcp_fin_timeout看到本機的這個值)官卡,然后即可回到CLOSED 可用狀態(tài)了。如果FIN_WAIT_1狀態(tài)下醋虏,收到了對方同時帶FIN標志和ACK標志的報文時寻咒,可以直接進入到TIME_WAIT狀態(tài),而無須經(jīng)過FIN_WAIT_2狀態(tài)灰粮。(這種情況應該就是四次揮手變成三次揮手的那種情況)
CLOSING 這種狀態(tài)在實際情況中應該很少見仔涩,屬于一種比較罕見的例外狀態(tài)。正常情況下粘舟,當一方發(fā)送FIN報文后熔脂,按理來說是應該先收到(或同時收到)對方的ACK報文,再收到對方的FIN報文柑肴。但是CLOSING 狀態(tài)表示一方發(fā)送FIN報文后霞揉,并沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文晰骑。什么情況下會出現(xiàn)此種情況呢适秩?那就是當雙方幾乎在同時close()一個SOCKET的話,就出現(xiàn)了雙方同時發(fā)送FIN報文的情況硕舆,這是就會出現(xiàn)CLOSING 狀態(tài)秽荞,表示雙方都正在關閉SOCKET連接。
CLOSE_WAIT 表示正在等待關閉抚官。怎么理解呢扬跋?當對方close()一個SOCKET后發(fā)送FIN報文給自己,你的系統(tǒng)毫無疑問地將會回應一個ACK報文給對方凌节,此時TCP連接則進入到CLOSE_WAIT狀態(tài)钦听。接下來呢洒试,你需要檢查自己是否還有數(shù)據(jù)要發(fā)送給對方,如果沒有的話朴上,那你也就可以close()這個SOCKET并發(fā)送FIN報文給對方垒棋,即關閉自己到對方這個方向的連接。有數(shù)據(jù)的話則看程序的策略痪宰,繼續(xù)發(fā)送或丟棄叼架。簡單地說,當你處于CLOSE_WAIT 狀態(tài)下衣撬,需要完成的事情是等待你去關閉連接碉碉。
LAST_ACK 當被動關閉的一方在發(fā)送FIN報文后,等待對方的ACK報文的時候淮韭,就處于LAST_ACK 狀態(tài)垢粮。當收到對方的ACK報文后,也就可以進入到CLOSED 可用狀態(tài)了靠粪。

 1. 服務端狀態(tài)轉移過程 

服務器通過listen系統(tǒng)調用進入LISTEN狀態(tài)蜡吧,被動等待客戶端連接,也就是所謂的被動打開占键。一旦監(jiān)聽到SYN(同步報文段)請求昔善,就將該連接放入內(nèi)核的等待隊列,并向客戶端發(fā)送帶SYN的ACK(確認報文段)畔乙,此時該連接處于SYN_RECVD狀態(tài)君仆。如果服務器收到客戶端返回的ACK,則轉到ESTABLISHED狀態(tài)牲距。這個狀態(tài)就是連接雙方能進行全雙工數(shù)據(jù)傳輸?shù)臓顟B(tài)返咱。
而當客戶端主動關閉連接時,服務器收到FIN報文牍鞠,通過返回ACK使連接進入CLOSE_WAIT狀態(tài)咖摹。此狀態(tài)表示——等待服務器應用程序關閉連接。通常难述,服務器檢測到客戶端關閉連接之后萤晴,也會立即給客戶端發(fā)送一個FIN來關閉連接,使連接轉移到LAST_ACK狀態(tài)胁后,等待客戶端對最后一個FIN結束報文段的最后一次確認店读,一旦確認完成,連接就徹底關閉了攀芯。

2. 客戶端狀態(tài)轉移過程

客戶端通過connect系統(tǒng)調用主動與服務器建立連接屯断。此系統(tǒng)調用會首先給服務器發(fā)一個SYN,使連接進入SYN_SENT狀態(tài)。
connect調用可能因為兩種原因失敼伞:1. 目標端口不存在(未被任何進程監(jiān)聽)護著該端口被TIME_WAIT狀態(tài)的連接占用(詳見后文)。2. 連接超時紧武,在超時時間內(nèi)未收到服務器的ACK剃氧。
如果connect調用失敗,則連接返回初始的CLOSED狀態(tài)阻星,如果調用成功朋鞍,則轉到ESTABLISHED狀態(tài)。
客戶端執(zhí)行主動關閉時妥箕,它會向服務器發(fā)送一個FIN滥酥,連接進入TIME_WAIT_1狀態(tài),如果收到服務器的ACK畦幢,進入TIME_WAIT_2狀態(tài)坎吻。此時服務器處于CLOSE_WAIT狀態(tài),這一對狀態(tài)是可能發(fā)生辦關閉的狀態(tài)(詳見后文)宇葱。此時如果服務器發(fā)送FIN關閉連接瘦真,則客戶端會發(fā)送ACK進行確認并進入TIME_WAIT狀態(tài)。

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

客戶端收到服務器的FIN報文之后,并不直接進入CLOSED狀態(tài)印颤,而是TIME_WAIT狀態(tài)您机。客戶端會在此狀態(tài)等等2MSL的時長之后年局,才會徹底關閉际看。(MSL是Maximum Segment Life,報文段最大生存時間矢否,一般為2分鐘仿村。

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

  • 可靠的終止TCP連接
    如果 B 沒收到 A 發(fā)送來的確認報文,那么就會重新發(fā)送連接釋放請求報文兴喂,A 等待一段時間就是為了處理這種情況的發(fā)生
  • 保證讓遲到的TCP報文段有足夠的時間被識別并丟棄
    在Linux系統(tǒng)中蔼囊,一個TCP端口不能被同時打開多次。當一個TCP連接處于TIME_WAIT狀態(tài)時衣迷,我們無法使用此接口來建立新連接畏鼓。如果不存在此狀態(tài),則可以建立一個和剛關閉的連接具有相同IP和端口的連接壶谒,也就是原來連接的化身云矫。此化身可以收到屬于原來連接的在網(wǎng)絡中滯留遲到的報文段,這顯然不應該存在汗菜,故需要TIME_WAIT狀態(tài)让禀。
    另外挑社,因為MSL是一個TCP報文的最大生存時間,所以2MSL的時間可以保證雙向的數(shù)據(jù)都發(fā)送完畢巡揍,遲到的報文都已消失(被中轉路由器丟棄)痛阻。所以2MSL時間之后新的連接可以絕對安全的建立,這就是TIME_WAIT狀態(tài)需要持續(xù)2MSL的原因腮敌。
    但是阱当,有時候我們希望避免TIME_WAIT狀態(tài),因為當程序退出后糜工,我們希望能立即重啟它弊添,因為此狀態(tài)的存在,我們是無法立即重啟的捌木。
    對于客戶端而言油坝,我們一般不需要擔心此問題。因為TCP連接中刨裆,客戶端通常使用的是系統(tǒng)自動分配的臨時端口號來建立連接免钻,這個端口號是隨機的,所以一般不會和上次的重復崔拥。
    但是對于服務端而言极舔,如果是服務器主動關閉連接然后異常終止,因為服務器提供服務的總是同一個知名端口號链瓦,則會出現(xiàn)不能立即重啟的情況拆魏。我們可以通過socket選項的SO_REUSEADDR來強制進程立即使用處于TIME_WAIT狀態(tài)的連接占用的端口,這涉及到Linux網(wǎng)絡編程慈俯,這里暫不討論渤刃。

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

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

接收方發(fā)送的確認報文中的窗口字段可以用來控制發(fā)送方窗口大小卖子,從而影響發(fā)送方的發(fā)送速率。將窗口字段設置為 0刑峡,則發(fā)送方不能發(fā)送數(shù)據(jù)洋闽。

窗口是緩存的一部分,用來暫時存放字節(jié)流突梦。發(fā)送方和接收方各有一個窗口诫舅,接收方通過 TCP 報文段中的窗口字段告訴發(fā)送方自己的窗口大小,發(fā)送方根據(jù)這個值和其它信息設置自己的窗口大小宫患。
發(fā)送窗口內(nèi)的字節(jié)都允許被發(fā)送刊懈,接收窗口內(nèi)的字節(jié)都允許被接收。如果發(fā)送窗口左部的字節(jié)已經(jīng)發(fā)送并且收到了確認,那么就將發(fā)送窗口向右滑動一定距離虚汛,直到左部第一個字節(jié)不是已發(fā)送并且已確認的狀態(tài)匾浪;接收窗口的滑動類似,接收窗口左部字節(jié)已經(jīng)發(fā)送確認并交付主機卷哩,就向右滑動接收窗口蛋辈。
接收窗口只會對窗口內(nèi)最后一個按序到達的字節(jié)進行確認,例如接收窗口已經(jīng)收到的字節(jié)為 {31, 34, 35}殉疼,其中 {31} 按序到達,而 {34, 35} 就不是捌年,因此只對字節(jié) 31 進行確認瓢娜。發(fā)送方得到一個字節(jié)的確認之后,就知道這個字節(jié)之前的所有字節(jié)都已經(jīng)被接收礼预。


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

TCP擁塞控制

如果網(wǎng)絡出現(xiàn)擁塞眠砾,分組將會丟失,此時發(fā)送方會繼續(xù)重傳托酸,從而導致網(wǎng)絡擁塞程度更高褒颈。因此當出現(xiàn)擁塞時,應當控制發(fā)送方的速率励堡。這一點和流量控制很像谷丸,但是出發(fā)點不同。流量控制是為了讓接收方能來得及接收应结,而擁塞控制是為了降低整個網(wǎng)絡的擁塞程度刨疼。

擁塞控制所起的作用

TCP 主要通過四種算法來進行擁塞控制:慢開始、擁塞避免鹅龄、快重傳揩慕、快恢復。

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

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


    TCP擁塞狀況cwnd在擁塞控制時的變化情況

1. 慢開始與擁塞避免

發(fā)送的最初執(zhí)行慢開始,令 cwnd=1,發(fā)送方只能發(fā)送 1 個報文段二拐;當收到確認后服鹅,將 cwnd 加倍,因此之后發(fā)送方能夠發(fā)送的報文段數(shù)量為:2百新、4企软、8 ...

注意到慢開始每個輪次都將 cwnd 加倍,這樣會讓 cwnd 增長速度非撤雇快仗哨,從而使得發(fā)送方發(fā)送的速度增長速度過快,網(wǎng)絡擁塞的可能也就更高铅辞。設置一個慢開始門限 ssthresh厌漂,當 cwnd >= ssthresh 時,進入擁塞避免斟珊,每個輪次只將 cwnd 加 1苇倡。

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

2. 快重傳與快恢復

在接收方,要求每次接收到報文段都應該對最后一個已收到的有序報文段進行確認堵漱。例如已經(jīng)接收到 M1 和 M2综慎,此時收到 M4,應當發(fā)送對 M2 的確認勤庐。

在發(fā)送方寥粹,如果收到三個重復確認,那么可以知道下一個報文段丟失,此時執(zhí)行快重傳,立即重傳下一個報文段之景。例如收到三個 M2愿吹,則 M3 丟失,立即重傳 M3。

在這種情況下,只是丟失個別報文段,而不是網(wǎng)絡擁塞糊肠。因此執(zhí)行快恢復,令 ssthresh = cwnd/2 遗锣,cwnd = ssthresh货裹,注意到此時直接進入擁塞避免。

慢開始和快恢復的快慢指的是 cwnd 的設定值精偿,而不是 cwnd 的增長速率弧圆。慢開始 cwnd 設定為 1赋兵,而快恢復 cwnd 設定為 ssthresh。


快重傳示意圖

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

1. 發(fā)送應答

??發(fā)送端的每個TCP報文都必須得到接收方的應答搔预,才算傳輸成功霹期。

2. 超時重傳

??TCP為每個TCP報文段都維護一個重傳定時器。
??發(fā)送端在發(fā)出一個TCP報文段之后就啟動定時器拯田,如果在定時時間類未收到應答历造,它就將重發(fā)該報文段并重置定時器。

3. 報文重排

??因為TCP報文段最終在網(wǎng)絡層是以IP數(shù)據(jù)報的形式發(fā)送船庇,而IP數(shù)據(jù)報到達接收端可能是亂序或者重復的吭产。TCP協(xié)議會對收到的TCP報文進行重排、整理鸭轮,確保順序正確臣淤。


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

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

  • 交互數(shù)據(jù) 用于進行信息交互,僅包含很少的字節(jié)张弛,使用交互數(shù)據(jù)的程序或協(xié)議對實時性要求很高荒典,比如telnet酪劫、ssh等
  • 成塊數(shù)據(jù) 用于大量數(shù)據(jù)傳輸吞鸭,長度通常為TCP報文段所允許的最大數(shù)據(jù)長度,使用成塊數(shù)據(jù)的應用程序對傳輸效率要求高覆糟,比如ftp等刻剥。

TCP一些有趣的問題 —— 粘包/拆包等

什么是粘包拆包?

對于什么是粘包滩字、拆包問題造虏,我想先舉兩個簡單的應用場景:

  1. 客戶端和服務器建立一個連接,客戶端發(fā)送一條消息麦箍,客戶端關閉與服務端的連接漓藕。
  2. 客戶端和服務器簡歷一個連接,客戶端連續(xù)發(fā)送兩條消息挟裂,客戶端關閉與服務端的連接享钞。

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

第一種情況:

服務端一共讀到兩個數(shù)據(jù)包碟联,第一個包包含客戶端發(fā)出的第一條消息的完整信息,第二個包包含客戶端發(fā)出的第二條消息同窘,那這種情況比較好處理玄帕,服務器只需要簡單的從網(wǎng)絡緩沖區(qū)去讀就好了,第一次讀到第一條消息的完整信息想邦,消費完再從網(wǎng)絡緩沖區(qū)將第二條完整消息讀出來消費裤纹。

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

第二種情況:

服務端一共就讀到一個數(shù)據(jù)包丧没,這個數(shù)據(jù)包包含客戶端發(fā)出的兩條消息的完整信息鹰椒,這個時候基于之前邏輯實現(xiàn)的服務端就蒙了,因為服務端不知道第一條消息從哪兒結束和第二條消息從哪兒開始呕童,這種情況其實是發(fā)生了TCP粘包漆际。

TCP粘包示意圖

第三種情況:

服務端一共收到了兩個數(shù)據(jù)包,第一個數(shù)據(jù)包只包含了第一條消息的一部分夺饲,第一條消息的后半部分和第二條消息都在第二個數(shù)據(jù)包中奸汇,或者是第一個數(shù)據(jù)包包含了第一條消息的完整信息和第二條消息的一部分信息,第二個數(shù)據(jù)包包含了第二條消息的剩下部分往声,這種情況其實是發(fā)送了TCP拆擂找,因為發(fā)生了一條消息被拆分在兩個包里面發(fā)送了,同樣上面的服務器邏輯對于這種情況是不好處理的浩销。

TCP拆包示意圖

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

我們知道tcp是以流動的方式傳輸數(shù)據(jù)贯涎,傳輸?shù)淖钚挝粸橐粋€報文段(segment)。tcp Header中有個Options標識位慢洋,常見的標識為mss(Maximum Segment Size)指的是塘雳,連接層每次傳輸?shù)臄?shù)據(jù)有個最大限制MTU(Maximum Transmission Unit),一般是1500比特普筹,超過這個量要分成多個報文段败明,mss則是這個最大限制減去TCP的header,光是要傳輸?shù)臄?shù)據(jù)的大小太防,一般為1460比特妻顶。換算成字節(jié),也就是180多字節(jié)杏头。

tcp為提高性能盈包,發(fā)送端會將需要發(fā)送的數(shù)據(jù)發(fā)送到緩沖區(qū),等待緩沖區(qū)滿了之后醇王,再將緩沖中的數(shù)據(jù)發(fā)送到接收方呢燥。同理,接收方也有緩沖區(qū)這樣的機制寓娩,來接收數(shù)據(jù)叛氨。

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

  • 應用程序寫入的數(shù)據(jù)大于套接字緩沖區(qū)大小,這將會發(fā)生拆包寞埠。
  • 應用程序寫入數(shù)據(jù)小于套接字緩沖區(qū)大小屁置,網(wǎng)卡將應用多次寫入的數(shù)據(jù)發(fā)送到網(wǎng)絡上,這將會發(fā)生粘包仁连。
  • 進行mss(最大報文長度)大小的TCP分段蓝角,當TCP報文長度-TCP頭部長度>mss的時候將發(fā)生拆包。
  • 接收方法不及時讀取套接字緩沖區(qū)數(shù)據(jù)饭冬,這將發(fā)生粘包使鹅。
    ……

如何解決拆包粘包

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

  • 使用帶消息頭的協(xié)議、消息頭存儲消息開始標識及消息長度信息侨艾,服務端獲取消息頭的時候解析出消息長度执虹,然后向后讀取該長度的內(nèi)容。
  • 設置定長消息蒋畜,服務端每次讀取既定長度的內(nèi)容作為一條完整消息声畏。
  • 設置消息邊界撞叽,服務端從網(wǎng)絡流中按消息編輯分離出消息內(nèi)容姻成。

總結

寫了一個簡單的golang版的tcp服務器實例,僅供參考:
例子


參考和推薦閱讀書目:

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

注釋:

eg.

/etc/services


  1. ?
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末愿棋,一起剝皮案震驚了整個濱河市科展,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌糠雨,老刑警劉巖才睹,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異甘邀,居然都是意外死亡琅攘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門松邪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坞琴,“玉大人,你說我怎么就攤上這事逗抑【绶” “怎么了寒亥?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長荧关。 經(jīng)常有香客問我溉奕,道長,這世上最難降的妖魔是什么忍啤? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任加勤,我火速辦了婚禮,結果婚禮上同波,老公的妹妹穿的比我還像新娘胸竞。我一直安慰自己,他們只是感情好参萄,可當我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布卫枝。 她就那樣靜靜地躺著,像睡著了一般讹挎。 火紅的嫁衣襯著肌膚如雪校赤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天筒溃,我揣著相機與錄音马篮,去河邊找鬼。 笑死怜奖,一個胖子當著我的面吹牛浑测,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播歪玲,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼迁央,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了滥崩?” 一聲冷哼從身側響起岖圈,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钙皮,沒想到半個月后蜂科,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡短条,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年导匣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茸时。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡贡定,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屹蚊,到底是詐尸還是另有隱情厕氨,我是刑警寧澤进每,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站命斧,受9級特大地震影響田晚,放射性物質發(fā)生泄漏。R本人自食惡果不足惜国葬,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一贤徒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧汇四,春花似錦接奈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至背苦,卻和暖如春互捌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背行剂。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工秕噪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厚宰。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓腌巾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铲觉。 傳聞我的和親對象是個殘疾皇子澈蝙,可洞房花燭夜當晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容