本文作為學習筆記,文章內容來自“極客時間”專欄《趣談網絡協(xié)議》藕畔,如有侵權马僻,請告知,必即時刪除注服。
1韭邓、TCP包頭
來看 TCP 頭的格式。從這個圖上可以看出溶弟,它比 UDP 復雜得多女淑。- 源端口號和目標端口號,這一點和 UDP 是一樣的辜御。如果沒有這兩個端口號鸭你。數據就不知道應該發(fā)給哪個應用。
- 包的序號擒权,為了解決亂序的問題袱巨。
- 確認序號,發(fā)出去的包應該有確認碳抄,如果沒有收到就應該重新發(fā)送愉老,直到送達。
- 狀態(tài)位纳鼎,例如 SYN 是發(fā)起一個連接俺夕,ACK 是回復裳凸,RST 是重新連接贱鄙,F(xiàn)IN 是結束連接等劝贸。TCP 是面向連接的,因而雙方要維護連接的狀態(tài)逗宁,這些帶狀態(tài)位的包的發(fā)送映九,會引起雙方的狀態(tài)變更。
- 窗口大小瞎颗,TCP 要做流量控制件甥,通信雙方各聲明一個窗口,標識自己當前能夠的處理能力哼拔,別發(fā)送的太快引有,也別發(fā)的太慢。所謂的流量控制就是讓發(fā)送方的發(fā)送速率不要太快倦逐,讓接收方來得及接受譬正。針對的是建立TCP連接的對端。
除了做流量控制以外檬姥,TCP 還會做擁塞控制曾我,對于真正的通路堵車不堵車,它無能為力健民,唯一能做的就是控制自己抒巢,也即控制發(fā)送的速度。擁塞控制針對的是網絡秉犹。
2蛉谜、三次握手
TCP 的連接建立,我們常常稱為三次握手崇堵。我們也常稱為“請求 -> 應答 -> 應答之應答”的三個回合悦陋。
為什么要三次,而不是兩次筑辨?按說兩個人打招呼俺驶,一來一回就可以了啊棍辕?為了可靠暮现,為什么不是四次?
假設兩次握手的情況楚昭,A要發(fā)起請求栖袋,和B建立連接,請求到達B之后抚太,B返回應答包塘幅,雙方建立連接昔案。如果A發(fā)送請求包之后掛了泣棋,B收到請求包兜看,返回應答包,建立連接狂塘,這個時候A已經掛了匾乓,自然不會再發(fā)送數據給B捞稿,因此B這個連接只能一直保持著。因而兩次握手肯定不行拼缝。
B 發(fā)送的應答到達 A娱局,A 就認為連接已經建立了,因為對于 A 來講咧七,他的消息有去有回衰齐。A 會給 B 發(fā)送應答之應答,而 B 也在等這個消息继阻,才能確認連接的建立耻涛,只有等到了這個消息,對于 B 來講穴翩,才算它的消息有去有回犬第。當然 A 發(fā)給 B 的應答之應答也會丟,也會繞路芒帕,甚至 B 掛了歉嗓。按理來說,還應該有個應答之應答之應答背蟆,這樣下去就沒底了鉴分。所以四次握手是可以的,四十次都可以带膀,關鍵四百次也不能保證就真的可靠了志珍。只要雙方的消息都有去有回,就基本可以了垛叨。
我們在程序設計的時候伦糯,可以要求開啟 keepalive 機制,即使沒有真實的數據包嗽元,也有探活包敛纲。另外,你作為服務端 B 的程序設計者剂癌,對于 A 這種長時間不發(fā)包的客戶端淤翔,可以主動關閉,從而空出資源來給其他客戶端使用佩谷。
三次握手除了雙方建立連接外旁壮,主要還是為了溝通一件事情监嗜,就是 TCP 包的序號的問題。
A 要告訴 B抡谐,我這面發(fā)起的包的序號起始是從哪個號開始的裁奇,B 同樣也要告訴 A,B 發(fā)起的包的序號起始是從哪個號開始的童叠。
每個連接都要有不同的序號框喳。這個序號的起始序號是隨著時間變化的课幕,可以看成一個 32 位的計數器厦坛,每 4 微秒加一,如果計算一下乍惊,如果到重復杜秸,需要 4 個多小時,那個繞路的包早就死翹翹了润绎,因為我們都知道 IP 包頭里面有個 TTL撬碟,也即生存時間。在連接建立的過程中莉撇,雙方的狀態(tài)變化時序圖就像這樣呢蛤。
一開始,客戶端和服務端都處于 CLOSED 狀態(tài)棍郎。先是服務端主動監(jiān)聽某個端口其障,處于 LISTEN 狀態(tài)。然后客戶端主動發(fā)起連接 SYN涂佃,之后處于 SYN-SENT 狀態(tài)励翼。服務端收到發(fā)起的連接,返回 SYN辜荠,并且 ACK 客戶端的 SYN汽抚,之后處于 SYN-RCVD 狀態(tài)〔。客戶端收到服務端發(fā)送的 SYN 和 ACK 之后造烁,發(fā)送 ACK 的 ACK,之后處于 ESTABLISHED 狀態(tài)午笛,因為它一發(fā)一收成功了惭蟋。服務端收到 ACK 的 ACK 之后,處于 ESTABLISHED 狀態(tài)季研,因為它也一發(fā)一收了敞葛。
3、四次揮手
斷開連接的時候的狀態(tài)時序圖与涡。
斷開的時候惹谐,我們可以看到持偏,當 A 說“不玩了”,就進入 FIN_WAIT_1 的狀態(tài)氨肌,B 收到“A 不玩”的消息后鸿秆,發(fā)送知道了,就進入 CLOSE_WAIT 的狀態(tài)怎囚。A 收到“B 說知道了”卿叽,就進入 FIN_WAIT_2 的狀態(tài),A 發(fā)送“知道 B 也不玩了”的 ACK 后恳守,從 FIN_WAIT_2 狀態(tài)結束考婴。
按說 A 可以跑路了,但是最后的這個 ACK 萬一 B 收不到呢催烘?則 B 會重新發(fā)一個“B 不玩了”沥阱,這個時候 A 已經跑路了的話,B 就再也收不到 ACK 了伊群,因而 TCP 協(xié)議要求 A 最后等待一段時間 TIME_WAIT考杉,這個時間要足夠長,長到如果 B 沒收到 ACK 的話舰始,“B 說不玩了”會重發(fā)的崇棠,A 會重新發(fā)一個 ACK 并且足夠時間到達 B。
等待的時間設為 2MSL丸卷,MSL 是 Maximum Segment Lifetime枕稀,報文最大生存時間,它是任何報文在網絡上存在的最長時間及老,超過這個時間報文將被丟棄抽莱。
4、TCP的滑動窗口
為了記錄所有發(fā)送的包和接收的包骄恶,發(fā)送端的緩存里是按照包的 ID 一個個排列食铐,根據處理的情況分成四個部分。
- 第一部分:發(fā)送了并且已經確認的僧鲁。
- 第二部分:發(fā)送了并且尚未確認的虐呻。
- 第三部分:沒有發(fā)送,但是已經等待發(fā)送的寞秃。
- 第四部分:沒有發(fā)送斟叼,并且暫時還不會發(fā)送的。
在 TCP 里春寿,接收端會給發(fā)送端報一個窗口的大小朗涩,叫 Advertised window。這個窗口的大小應該等于上面的第二部分加上第三部分绑改,就是已經交代了沒做完的加上馬上要交代的谢床。超過這個窗口的兄一,接收端做不過來,就不能發(fā)送了识腿。于是出革,發(fā)送端需要保持下面的數據結構。
- LastByteAcked:第一部分和第二部分的分界線
- LastByteSent:第二部分和第三部分的分界線
- LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界線
對于接收端來講渡讼,它的緩存里記錄的內容要簡單一些骂束。
- 第一部分:接受并且確認過的。
- 第二部分:還沒接收成箫,但是馬上就能接收的展箱。
- 第三部分:還沒接收,也沒法接收的伟众。
- LastByteRead 之后是已經接收了析藕,但是還沒被應用層讀取的召廷;
- NextByteExpected 是第一部分和第二部分的分界線凳厢。
- MaxRcvBuffer:最大緩存的量;
5竞慢、順序問題與丟包問題
還是剛才的圖先紫,在發(fā)送端來看,1筹煮、2遮精、3 已經發(fā)送并確認;4败潦、5本冲、6、7劫扒、8檬洞、9 都是發(fā)送了還沒確認;10沟饥、11添怔、12 是還沒發(fā)出的;13贤旷、14广料、15 是接收方沒有空間,不準備發(fā)的幼驶。
在接收端來看艾杏,1、2盅藻、3购桑、4汹族、5 是已經完成 ACK,但是沒讀取的其兴;6顶瞒、7 是等待接收的;8元旬、9 是已經接收榴徐,但是沒有 ACK 的。
發(fā)送端和接收端當前的狀態(tài)如下:
- 1匀归、2坑资、3 沒有問題,雙方達成了一致穆端。
- 4袱贮、5 接收方說 ACK 了,但是發(fā)送方還沒收到体啰,有可能丟了攒巍,有可能在路上。
- 6荒勇、7柒莉、8、9 肯定都發(fā)了沽翔,但是 8兢孝、9 已經到了,但是 6仅偎、7 沒到跨蟹,出現(xiàn)了亂序,緩存著但是沒辦法 ACK橘沥。
根據這個例子,我們可以知道威恼,順序問題和丟包問題都有可能發(fā)生品姓,所以我們先來看確認與重發(fā)的機制箫措。一種方法就是超時重試,也即對每一個發(fā)送了斤蔓,但是沒有 ACK 的包植酥,都有設一個定時器,超過了一定的時間,就重新嘗試友驮。每當遇到一次超時重傳的時候漂羊,都會將下一次超時時間間隔設為先前值的兩倍。兩次超時卸留,就說明網絡環(huán)境差走越,不宜頻繁反復發(fā)送。
有一個可以快速重傳的機制耻瑟,當接收方收到一個序號大于下一個所期望的報文段時旨指,就會檢測到數據流中的一個間隔,于是它就會發(fā)送冗余的 ACK喳整,仍然 ACK 的是期望接收的報文段谆构。還有一種方式稱為 Selective Acknowledgment (SACK)。(TODO:這兩種我沒看太明白框都,后面再補吧)
6搬素、流量控制問題
在對于包的確認中,同時會攜帶一個窗口的大小魏保。
對于發(fā)送端熬尺,我們先假設窗口不變的情況,窗口始終為 9囱淋。4 的確認來的時候猪杭,會右移一個,這個時候第 13 個包也可以發(fā)送了妥衣。
我們假設一個極端情況,接收端的應用一直不讀取緩存中的數據戒傻,當數據包 6 確認后税手,窗口大小就不能再是 9 了,就要縮小一個變?yōu)?8需纳。
這個新的窗口 8 通過 6 的確認消息到達發(fā)送端的時候芦倒,你會發(fā)現(xiàn)窗口沒有平行右移,而是僅僅左面的邊右移了不翩,窗口的大小從 9 改成了 8兵扬。
如果接收端還是一直不處理數據,則隨著確認的包越來越多口蝠,窗口越來越小器钟,直到為 0。當這個窗口通過包 14 的確認到達發(fā)送端的時候妙蔗,發(fā)送端的窗口也調整為 0傲霸,停止發(fā)送。這就是我們常說的流量控制。
7昙啄、擁塞控制問題
擁塞控制的問題穆役,也是通過窗口的大小來控制的,前面的滑動窗口 rwnd是怕發(fā)送方把接收方緩存塞滿梳凛,而擁塞窗口 cwnd耿币,是怕把網絡塞滿。
這里有一個公式 LastByteSent - LastByteAcked <= min {cwnd, rwnd} 韧拒,是擁塞窗口和滑動窗口共同控制發(fā)送的速度掰读。就是發(fā)送方的第二部分(已發(fā)送但未確認的)<= 滑動窗口和擁塞窗口的最小值。
TCP 的擁塞控制主要來避免兩種現(xiàn)象叭莫,包丟失和超時重傳蹈集。一旦出現(xiàn)了這些現(xiàn)象就說明,發(fā)送速度太快了雇初,要慢一點拢肆。但是一開始我怎么知道速度多快呢,我怎么知道應該把窗口調整到多大呢靖诗?如果我們通過漏斗往瓶子里灌水郭怪,我們就知道,不能一桶水一下子倒進去刊橘,肯定會濺出來鄙才,要一開始慢慢的倒,然后發(fā)現(xiàn)總能夠倒進去促绵,就可以越倒越快攒庵。這叫作慢啟動。
一條 TCP 連接開始败晴,cwnd 設置為一個報文段浓冒,一次只能發(fā)送一個;當收到這一個確認的時候尖坤,cwnd 加一稳懒,于是一次能夠發(fā)送兩個;當這兩個的確認到來的時候慢味,每個確認 cwnd 加一场梆,兩個確認 cwnd 加二,于是一次能夠發(fā)送四個纯路;當這四個的確認到來的時候或油,每個確認 cwnd 加一,四個確認 cwnd 加四感昼,于是一次能夠發(fā)送八個装哆。可以看出這是指數性的增長。
漲到什么時候是個頭呢蜕琴?有一個值 ssthresh 為 65535 個字節(jié)萍桌,當超過這個值的時候,就要小心一點了凌简,不能倒這么快了上炎,可能快滿了,再慢下來雏搂。每收到一個確認后藕施,cwnd 增加 1/cwnd,我們接著上面的過程來凸郑,一次發(fā)送八個裳食,當八個確認到來的時候,每個確認增加 1/8芙沥,八個確認一共 cwnd 增加 1诲祸,于是一次能夠發(fā)送九個,變成了線性增長而昨。
后來有了 TCP BBR 擁塞算法救氯。它企圖找到一個平衡點,就是通過不斷地加快發(fā)送速度歌憨,將管道填滿着憨,但是不要填滿中間設備的緩存,因為這樣時延會增加务嫡,在這個平衡點可以很好的達到高帶寬和低時延的平衡甲抖。