我想統計一下,TCP/IP 尤其是TCP協議,能搜到的常見的問題,整理起來,關鍵詞添加在目錄中,便于以后查閱。
目前預計整理共3篇:
[TCP] TCP/IP 基礎知識問答 :基礎知識
[TCP] TCP/IP 基礎知識問答(2) :TCP協議相關知識
[TCP] TCP/IP 基礎知識問答(3) :UDP協議相關知識
文章目錄
- TCP協議相關知識
- 什么是TCP
- MTU和MSS分別是什么?
- 沾包和拆包
- 現象
- 對策
- TCP頭包含哪些信息
- 常見TCP的連接狀態有哪些?
- TIMEWAIT狀態存在的意義
- TIMEWAIT過多的危害
- 服務器出現大量 CLOSE_WAIT 狀態的原因
- 如何優化TIMEWAIT
- 優化系統參數:
- 短連接改為長連接
- setsockopt()設置SO_REUSEADDR
- setsockopt()設置SO_REUSEPORT
- 為什么需要三次握手,兩次不行嗎?
- 三次握手的過程可以攜帶數據嗎
- 揮手為什么需要四次
- 揮手可以是3次嗎
- 延遲應答
- 在四次揮手中,為什么發起端進入TIME_WAIT狀態要有2MSL等待
- 什么是MSL
- 為什么等待2MSL
- 什么是半連接隊列
- listen的第二個參數
- 沒有 listen,能建立 TCP 連接嗎?
- 服務端如果只 bind 了 IP 地址和端口,而沒有調用 listen 的話,客戶端可以和服務端通信嗎?
- SYN攻擊是什么
- TCP如何確保可靠性的
- 序列號
- ISN(Initial Sequence Number)是固定的嗎
- 序列號回繞了怎么辦
- TCP時間戳
- TCP 重傳機制
- 超時重傳
- RTT
- RTO
- 快速重傳
- 為何快速重傳是選擇3次ACK
- SACK
- D-SACK
- TCP 流量控制
- 滑動窗口
- 窗口的大小
- 累計應答
- 滑動窗口縮放因子
- 發送窗口的控制
- 接收窗口的控制
- 滑動窗口的流量控制
- 死鎖
- 死鎖的解決方法
- 糊涂窗口綜合征
- 對策
- 讓接收方不通告小窗口
- 延遲確認
- Nagle算法
- TCP 擁塞控制
- 擁塞窗口
- 慢啟動
- 慢啟動門限
- 擁塞避免
- 擁塞發生
- 超時重傳- 擁塞發生算法
- 門啟動門限ssthresh初始值
- 快速重傳- 擁塞發生算法
- 快速恢復
- 超時重傳的擁塞算法圖像
- 快速重傳的擁塞算法圖像
TCP協議相關知識
什么是TCP
TCP是面向連接的,可靠的,基于字節流的通信協議。
MTU和MSS分別是什么?
MTU是最大傳輸單元(maximum transmission unit)。
由硬件規定,比如以太網是1500字節.
MSS是最大分節大小(maximum segment size)。
在TCP傳輸中,MTU - IP頭長度 - TCP頭長度 == MSS, 在通信時,發送端會在TCP頭中包含MSS的大小。
沾包和拆包
現象
沾包現象,不是TCP的bug,而是TCP傳輸帶來的特點。
在程序執行的過程中,并不是調用send()函數后,就會直接發送出去,而是把應用層緩沖區中的
數據拷貝到內核緩沖區中,再拷貝到TCP協議棧的緩沖區中,然后由TCP協議棧來控制發送。
協議棧可能會把多個比較短的數據,合并成一個包來發送。也可能匯報超過MSS大小的數據,拆分成多個包來發送。
TCP協議是基于字節流的,協議棧理解的數據是沒有邊界的,沒有包的概念。
沾包和拆包中的包,是用戶態理解的概念,例如我多次調用Send(),就是用戶態發出了多個包到內核態,如果這多個包都緩存在協議棧緩沖區等待發送,協議棧不會區分這是由幾個Send()傳輸過來的數據,可能直接把所有數據都發出去了。
對策
- 發送固定長度的數據
- 用特殊結束符,比如\r\n,標記本條數據結束
- 給數據包添加頭和尾
另外,如果程序設計式樣允許,可以再Send()一次后,Sleep()一下,避免數據被沾包合并發送。
TCP頭包含哪些信息
TCP頭包含以下內容:
源端口和目的端口(IP地址在IP頭中),各16位,共4字節;
序列號和確認號,用于TCP包的順序確認,各4字節,共8字節。
頭部長度,TCP存在選擇字段,所以TCP頭是可以變長的(20-60字節),長度存儲在此字段,4位,和保留字段g共占8位,1個字節。
標志字段標志這個TCP包的作用,最常見的是ACK標志。
常用的標志位是:
ACK:確認序號有效標識
PSH:告訴協議棧,應盡快講此報文交付給應用層
RST:重建連接標識。即TCP連接出現錯誤,連接斷開,需要重新建立連接
SYN:發起連接時的標志
FIN:釋放連接
窗口大小這個值是接收端期望接收的字節數。窗口最大為65535字節。(如果有縮放因子選項,還要另外計算)。因為滑動窗口機制,需要告訴發送端,接收端還有多少窗口大小能接收數據。如果發送端,收到回復包中窗口大小的值小于MSS或者等于0,則說明接收端接收窗口空間不足,發送端協議棧會暫停發送數據。
校驗和發送端協議棧計算和寫入,接收端進行校驗數據
緊急指針只有當URG標志置1時緊急指針才有效。TCP的緊急方式是發送端向另一端發送緊急數據的一種方式。緊急指針指出在本報文段中緊急數據共有多少個字節(緊急數據放在本報文段數據的最前面)。不常用。
以上為固定字段,共20字節
選項 會存儲一些特殊選項,例如:窗口縮放因子,選擇性ACK等
參考:TCP頭部格式和封裝
常見TCP的連接狀態有哪些?
共11個狀態。
LISTEN:socket在執行listen()后,會進入LISTEN狀態,監聽套接字
SYN_SEND:主動端在發出第一次握手的SYN包后會進入這個狀態
SYN_RECV:被動端在收到第一次握手包,發出第二次握手的SYN ACK包后,會進入這個狀態
ESTABLISHED:在雙方完成三次握手后,都會進入這個狀態,表示建立連接
(四次揮手)
FIN_WAIT1:主動斷開端發送完第一次揮手的FIN ACK包后進入這個狀態
CLOSE_WAIT:被動端在發出第二次揮手的ACK 包后,進入這個狀態
FIN_WAIT2:主動端在收到第二次揮手的包以后,等待收到第三次握手的時候的狀態
LAST_ACK:被動端在發出第三次揮手后,進入這個狀態
TIME_WAIT:主動端在發出第四次揮手后,進入這個狀態,會保持2MSL再進入CLOSED
CLOSED:斷開連接的狀態
CLOSING:是應對四次揮手的意外情況,主動方發出FIN ACK包,同時收到了FIN ACK包,就會進入CLOSING狀態,等到收到了ACK,就會進入TIMEWAIT狀態。
TIMEWAIT狀態存在的意義
- 確保被動段開方能順利斷開連接
- 防止收到上一個相同連接的歷史報文
TIMEWAIT過多的危害
-
如果主動斷開連接的是客戶端,TIMEWAIT在客戶端出現過多,可能會導致端口耗盡:
每個TCP連接都是一個四元組,通過一個四元組就可以確定一個連接。
當存在TIMEWAIT時,這個連接就被占用了。客戶端想要再連接服務器就需要建立新的連接,使用新的端口,但是客戶端的端口是有限的,所以TIMEWAIT過多,可能導致端口耗盡。 -
如果出現在服務端,說明服務器主動斷開了大量連接。服務器出現大量的TIMEWAIT可能會占用系統資源。
原因可能如下:
- 出現了大量的HTTP短連接。不論客戶端還是服務端的HTTP頭出現Connection:Close 都會使HTTP變成短連接,服務器會主動斷開連接。這要排查服務器或者客戶端時候哪里設置了Connection:Close ,要改成長連接
- HTTP長連接超時。這要排查,是不是什么問題導致客戶端長時間無法向服務端發包
- HTTP 長連接超出允許的數量。
服務器出現大量 CLOSE_WAIT 狀態的原因
原因是服務器作為被動斷開方,因為某些原因沒能調用close()結束連接。
需要調查為什么沒有執行close()。
如何優化TIMEWAIT
優化系統參數:
修改/etc/sysctl.conf文件,一般涉及下面的幾個參數:
net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉;
net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認為0,表示關閉;
net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。
net.ipv4.tcp_fin_timeout = 修改系統默認的 TIMEOUT 時間
net.ipv4.tcp_max_tw_buckets = 5000 表示系統同時保持TIME_WAIT套接字的最大數量,(默認是18000). 當TIME_WAIT連接數量達到給定的值時,所有的TIME_WAIT連接會被立刻清除,并打印警告信息。但這種粗暴的清理掉所有的連接,意味著有些連接并沒有成功等待2MSL,就會造成通訊異常。一般不建議調整
net.ipv4.tcp_timestamps = 1(默認即為1)60s內同一源ip主機的socket connect請求中的timestamp必須是遞增的。也就是說服務器打開了 tcp_tw_reccycle了,就會檢查時間戳,如果對方發來的包的時間戳是亂跳的或者說時間戳是滯后的,那么服務器就會丟掉不回包,現在很多公司都用LVS做負載均衡,通常是前面一臺LVS,后面多臺后端服務器,這其實就是NAT,當請求到達LVS后,它修改地址數據后便轉發給后端服務器,但不會修改時間戳數據,對于后端服務器來說,請求的源地址就是LVS的地址,加上端口會復用,所以從后端服務器的角度看,原本不同客戶端的請求經過LVS的轉發,就可能會被認為是同一個連接,加之不同客戶端的時間可能不一致,所以就會出現時間戳錯亂的現象,于是后面的數據包就被丟棄了,具體的表現通常是是客戶端明明發送的SYN,但服務端就是不響應ACK,還可以通過下面命令來確認數據包不斷被丟棄的現象,所以根據情況使用
其他優化:
net.ipv4.ip_local_port_range = 1024 65535 增加可用端口范圍,讓系統擁有的更多的端口來建立鏈接,這里有個問題需要注意,對于這個設置系統就會從1025~65535這個范圍內隨機分配端口來用于連接,如果我們服務的使用端口比如8080剛好在這個范圍之內,在升級服務期間,可能會出現8080端口被其他隨機分配的鏈接給占用掉,這個原因也是文章開頭提到的端口被占用的另一個原因
net.ipv4.ip_local_reserved_ports = 7005,8001-8100 針對上面的問題,我們可以設置這個參數來告訴系統給我們預留哪些端口,不可以用于自動分配。
復制
優化完內核參數后,可以執行sysctl -p命令,來激活上面的設置永久生效
來源:https://cloud.tencent.com/developer/article/1589962
短連接改為長連接
setsockopt()設置SO_REUSEADDR
服務器啟動后,有客戶端連接并已建立,如果服務器主動關閉,那么和客戶端的連接會處于TIME_WAIT狀態,此時再次啟動服務器,就會bind不成功,報:Address already in use。
服務器父進程監聽客戶端,當和客戶端建立鏈接后,fork一個子進程專門處理客戶端的請求,如果父進程停止,因為子進程還和客戶端有連接,所以再次啟動父進程,也會報Address already in use。
來源:https://zhuanlan.zhihu.com/p/79999012
setsockopt()設置SO_REUSEPORT
1、允許將多個socket綁定到相同的地址和端口,前提每個socket綁定前都需設置SO_REUSEPORT。如果第一個綁定的socket未設置SO_REUSEPORT,那么其他的socket無論有沒有設置SO_REUSEPORT都無法綁定到該地址和端口直到第一個socket釋放了綁定。
2、attention:SO_REUSEPORT并不表示SO_REUSEADDR,即不具備上述SO_REUSEADDR的第二點作用(對TIME_WAIT狀態的socket處理方式)。因此當有個socketA未設置SO_REUSEPORT綁定后處在TIME_WAIT狀態時,如果socketB僅設置了SO_REUSEPORT在綁定和socketA相同的ip和端口時將會失敗。解決方案
(1)、socketB設置SO_REUSEADDR 或者socketB即設置SO_REUSEADDR也設置SO_REUSEPORT
(2)、兩個socket上都設置SO_REUSEPORT
來源:https://www.jianshu.com/p/141aa1c41f15
為什么需要三次握手,兩次不行嗎?
不可以。
三次握手的目的:
- 確認兩端手法能力正常
第一個握手包,證明發起端,發送數據能力正常。
第二個握手包,證明接收端,接受能力和發送能力正常。
第三個握手包,證明發起端,接收能力正常。
如果只有兩次我收就建立連接,而發起端是無法發出第三次握手包的,那就會導致這個連接一直占用連接隊列。 - 避免歷史報文的影響
如果兩次握手就建立連接。
客戶端發出一個SYN包,然后宕機,立即重啟后,又發出一個新SYN包。
如果服務器先收到舊SYN包,服務器返回一個SYN+ACK包
,此時服務器就認為連接已經建立了。
但是客戶端收到這個SYN+ACK包后,通過ack num 發現不是自己剛剛發出的,不會進入ESTABLISHED狀態,沒有建立連接,返回一個RST報文。
在收到RST報文前,服務器以為建立了連接,有可能給客戶端發送出數據了,浪費服務器資源。
而三次握手就可以避免這個問題。
服務器先收到舊SYN包,回復一個SYN+ACK包。
客戶端收到以后,通過ack num 發現不是自己剛剛發出的,不會進入ESTABLISHED狀態,沒有建立連接,返回一個RST報文,服務器收到以后也不會進入ESTABLISHED狀態。等收到信的SYN包后,再進行三次握手建立連接。
- 同步序列號
客戶端發出SYN包, 以及初始化的syn num。
服務器收到以后,發出SYN+ACK包,發出初始化的syn num 和 ack num,ack num是SYN包的shn num + 1,代表服務器已經同步客戶端的序列號。
客戶端收到服務器的SYN+ACK包后,獲取了服務器的syn num ,同步了服務器的序列號。
服務器收到客戶端發出的ACK包,知曉客戶端已經同步了服務器的序列號。
參考:TCP 三次握手與四次揮手面試題
三次握手的過程可以攜帶數據嗎
第三次握手可以。
參考:TCP第三次握手能攜帶數據嗎?做個實驗就知道!
揮手為什么需要四次
第一次揮手包,是發起端告訴接收端,我數據處理完了,要斷開連接。
第二次揮手包,是接收端告訴發起端,我知道了,等我處理數據。
第三次揮手包,是接收端告訴發起端,我數據處理完了,要斷開連接。
第四次揮手包,是發起端告訴接收端,我知道了,連接斷開。
此時才可以確認雙方都同意要斷開連接了,沒有數據發送了。
揮手可以是3次嗎
可以。
被動揮手端 在收到第一次FIN包后,會進入CLOSE_WAIT狀態,返回ACK包。等待數據處理完成,然后再發送FIN包。
如果滿足以下條件,可以實現ACK包和FIN包一起發送。
1.沒有應用層數據需要處理
2.TCP啟動了延遲確認
延遲應答
延遲應答是默認開啟的。
可以通過setsockopt 的 TCP_QUICKACK 選項關閉延遲確認。
接收方如果每次收到來自發送發的數據包后都立刻回復確認應答的話,可能會返回一個較小的窗口,這個窗口是用來通知發送方下次發送數據的大小。主要是因為接收方會先將數據放到緩沖區,待上層應用層將這些數據取走。而由于剛收到數據,應用層還沒來得及取,此時緩沖區的可用空間變小,就會通知發送方要減少下次發送的數據長度。
當接收端收到這個小窗口的通知以后,會以它為上限發送數據,從而又降低了網絡的利用率。除此之外,如果只是單純的發送一個確認應答,代價又會很高。因為IP頭部有20字節,TCP頭部也有20字節(此處不計選項)。
————————————————
原文鏈接:https://blog.csdn.net/LOOKTOMMER/article/details/121522110
所以TCP的延遲確認,就是:
- 當沒有響應數據要發送時,ACK 將會延遲一段時間,以等待是否有響應數據可以一起發送
- 當有響應數據要發送時,ACK 會隨著響應數據一起立刻發送給對方(捎帶應答)
- 如果在延遲等待發送 ACK 期間,對方的第二個數據報文又到達了,這時就會立刻發送 ACK
延遲確認等待時間大概100-200ms。
在數據包通信頻繁的時候,延遲確認是有好處的。但是如果數據包通信不頻繁,延遲確認可能會導致通信效率降低,可以配置系統參數縮短延遲確認時間,或者setsockopt 關閉延遲確認。
在四次揮手中,為什么發起端進入TIME_WAIT狀態要有2MSL等待
什么是MSL
MSL是報文最大生存時間(maximum segment lifetime),即一個數據包在網絡中最大存在的時間。
為什么等待2MSL
發起揮手方在發出第四次揮手包后,進入TIME_WAIT狀態,但是有可能這個包丟失了。發出第三個揮手包的接收方,遲遲等不到第四個揮手包,他會等Min(1MSL,超時時間)的時間,重發第三個揮手包。
重新發出的第三個揮手包,最長會經過1 * MSL 時間,到達發起方。
這樣就出現了2MSL時間。
等待2 MSL時間,可以避免,因為丟包而導致的對端無法正常斷開連接。
另外,也是為了防止網絡中還有發給發起方的數據包沒有收到,如果沒有2MSL的等待,發起方斷開連接后,迅速重啟了連接,可能會收到上一個連接的包。
什么是半連接隊列
如果服務器接收到了客戶端發來的第一次握手包,會把這個連接放入半連接隊列。
當三次握手完成后,會把這個連接放入全連接隊列,等待Accept()函數調用。
listen的第二個參數
listen函數的第二個參數,是backlog。
這個參數的含義,不同的地方有不同的解釋,比如與有的地方規定他是半連接隊列和全連接隊列的和。
在linux的新版本和Windows中,他表示的是全連接隊列的大小。
而半連接隊列的大小,Linux系統中由系統參數tcp_max_syn_backlog來控制,
全連接隊列的大小,取backlog參數和系統參數somaxconn的較小者。
Linux中,在listen狀態下,netstat或者ss 命令顯示的 RECV-Q表示當前accept隊列大小,SEND-Q表示accept隊列的最大大小。
Windows中也有SOMAXCONN的宏,如果Listen()的第二個參數設置為這個宏,就會采用系統中的最大合理值來設置全連接隊列的大小。
沒有 listen,能建立 TCP 連接嗎?
可以。
TCP Socket 在Bind()后,就可以connect它自己的IP和Port。
服務端如果只 bind 了 IP 地址和端口,而沒有調用 listen 的話,客戶端可以和服務端通信嗎?
不可以。服務端的TCP的半連接隊列和全連接隊列是在Listen()的時候實現的。
由于沒有Listen(),也就沒有隊列,沒有地方存儲客戶端的連接。
如果此時,客戶端對服務端發起了連接建立,服務端會回 RST 報文。
SYN攻擊是什么
服務端在收到第一次握手的SYN包以后,會把連接加入半連接隊列,為連接分配資源,并發處二次握手的包,
等待第三次握手的包,如果第三次握手的包超時沒有收到,就會再次重發二次握手的包,要重發好幾次,才會把連接移出半連接隊列。
SYN攻擊就是攻擊者模擬大量的IP地址,向服務端發送大量的SYN包,來占用服務器資源,使服務器無法響應正常的連接。
在linux中可以用 netstat 命令檢查處于SYN_RECV狀態的TCP連接,如果出現很多隨機地址,可能是SYN攻擊。
通常現在的網關已經可以過濾SYN攻擊了。
也可以在系統這設置減少超時事件、或者增大半連接隊列。
也可以通過SYN Cookie技術防御SYN攻擊。
半連接隊列、全連接隊列、SYN攻擊,參考:
會把這個連接放入半連接隊列。
TCP如何確保可靠性的
- 三次握手、四次揮手,建立可靠連接
- 通過序列號進行應答機制,可以丟棄重復的包,可以發現丟包
- 超時重傳避免丟包
- 擁塞控制,避免網絡擁堵導致的丟包
- 流量控制,避免接收方處理不完數據,接口窗口占滿導致的丟包
- 校驗和,TCP頭中有校驗和數據段,可以用來校驗數據是否損壞
序列號
TCP頭中有序列號(seq num)和確認號(ack num)各占4字節。
TCP協議會為發送的數據中的每一個字節分配序列號,本次發送的包中的第一個字節的序列號就是TCP頭中的seq num。當對端收到了這個包以后,返回的確認包中,ack num 就是 接收到的seq num + 數據字節數 + 1。
ISN(Initial Sequence Number)是固定的嗎
不是,初始的seq num不是固定的,是隨機的。
是為了避免,收到了就得連接中的包的
序列號回繞了怎么辦
序列號只有32位,也就是一個無符號int型的大小,是存在序列號用盡,從頭開始的可能的。
為了避免這種情況,就有了TCP時間戳選項。
TCP時間戳
TimeStamp選項是默認開啟的,它會隨著時間增長,如圖。
如果出現樹序列號繞回,可以通過時間戳比較是不是最近的通信。
時間戳共8個字節,4個字節是發送該包的事件,4個字節是
TCP 重傳機制
通過序列號與確認號,TCP協議可以確保數據的有序傳輸。也可以發現有哪些數據丟包了。當發生丟包時,就會啟動重傳機制。
超時重傳
RTT
RTT是往返時延,Round-Trip Time ,即一個數據包發出,到收到確認包的時間。往返時延是一個動態變化的值。
RTO
RTO是超時重傳時間,Retransmission Timeout ,RTO應該略大于RTT。
當發出的數據包丟失或者確認包丟失,都會引起超時重傳。
TCP協議棧發出包的時候,會啟動一個定時器,當達到RTO時間時,即發生了超時,認為數據包丟失,會再次發出包,然后重啟RTO計時,此時的RTO應該是上一次的2倍。
這樣每一次重新計算的RTO都是上一次的兩倍,一般會進行3輪超時重傳,如果還沒有收到確認應答,會在等待一段時間后(2MSL?)關閉連接。
一旦發生了超時,會被認為網絡擁塞,就會觸發TCP的擁塞控制策略。
快速重傳
為了避免每次都等到超時時間到了,才開始重傳。TCP還有快速重傳機制。
如果發送端發送了多個包,序列號為,1、2、3、4、5。
接收端收到了1,回復 ACK ack num = 2;
沒收到2。
收到了3,回復 ACK ack num = 2;
收到了4,回復 ACK ack num = 2;
此時雖然沒到超時事件,但是發送收到了連續三個確認包,要求發送2號包,就可以判斷2號包丟包了。
此時雖然沒有超時,客戶端會立刻重傳2、3、4、5包。
因為客戶端不知道接收端除了2號包,3、4、5號包有沒有收到,如果只重傳了2號包,如果3號也丟包了,那還要等超時或者3次3號包的確認包才能確定丟包,效率很低,所以會全部重傳。
如果想要避免全部重傳,可以使用SACK(選擇性重傳)。
為何快速重傳是選擇3次ACK
3次是一個經驗值。如果是2次ACK就進行重傳,會收到更大的包亂序的影響。
詳細參考:TCP 快速重傳為什么是三次冗余 ACK,這個三次是怎么定下來的? - 車小胖的回答 - 知乎
SACK
SACK,Selective Acknowledgment,選擇性確認。
啟動了SACK功能后,會在TCP頭部的選項區域中,增加一個SACK字段。
當發生快速重傳的時候,服務器可以在SACK字段中記錄收到了哪些包。
這樣發送端就可以只重傳丟失的包了。
但是SACK不是默認開啟的。
需要通信雙方在三次握手協商是否開啟。如下圖,一個SYN包中有SACK選項。
D-SACK
D-SACK,Duplicate SACK, 用于接收方告訴發送方哪些數據重復接收了。
如果接收端的ACK確認包丟失了,會導致發送端重傳,當接收端接收到重傳包后,發現重復接收,就會發送D-SACK,告訴發送端,重復接收了,我已經收到過這個包。
D-SACK可以方便發送端判斷網絡狀況。
D-SACK使用的是TCP頭中SACK的字段。
在 Linux 下可以通過 net.ipv4.tcp_dsack 參數開啟/關閉這個功能
TCP 流量控制
滑動窗口
為了避免發送一個包,必須等到收到確認包才能繼續發下一個包的低效率通信。
TCP協議采用了窗口機制。
每個TCP協議棧都一個發送窗口,一個接收窗口。窗口就是一個動態的緩沖區,當服務端的接收空口空閑的時候,客戶端可以不必等待確認應答到來,就向服務端發送多個包。窗口動態變化的過程就是滑動窗口機制。
窗口的大小
在TCP的頭中,有一個Windows字段,這就是窗口的大小。是接收方用于告訴發送方自己還有多少的接收窗口大小。發送方不會發送超出接收方窗口大小的數據。強行發出會導致數據丟失。
累計應答
由于接收端有接收窗口,可以接收多個TCP報文 ,所以即使中間有個報文的去人包丟失了也沒關系,只要最終的報文的確認包成功發出去了就好。發送端你收到了最終發送的報文的確認包就知道前面全部數據都收到了。這就是累計應答。
滑動窗口縮放因子
滑動窗口縮放因子,Window Scaling。
在TCP的頭的Windows字段,記錄了窗口的大小,但是它是16位的,最大就是65536,在如今已經不滿足要求了。
TCP頭還有縮放因子選項,可以擴大窗口。
假設window scale為7,也就是要將Window Size的值左移七位,即乘以128。window scale最大為14。
在整個雙方的交互過程中,發送方和接收方Window size scaling factor乘積因子必須保持不變,但是發送方的乘積因子和接收方的乘積因子可以不同,由各自決定。
——————
https://www.cnblogs.com/hongdada/p/11171068.html
例如這個包,TCP的頭的windows字段是64240,縮放因子是左移8位,即放大256倍。此時表示的窗口的大小是:64240 * 256。
發送窗口的控制
發送窗口部分的內存,通過3個指針進行管理,其中兩個絕對指針,一個相對指針。
絕對指針是指向具體序列號數據的指針,相對指針是通過絕對指針地址計算后計算出來的指針。
第一個絕對指針,指向已發送但是沒收到確認數據的第一個字節。
第二個絕對指針,指向還沒發送且可以發送的數據的第一個字節。
第三個相對指針,指向還沒發送且不可發送的數據的第一個字節,通過第一個指針 + 滑動窗口大小(滑動窗口大小是會變化的)得到。
第一個指針指向的數據 和 第二個指針指向的數據,合在一起就是發送方的滑動窗口。
第二個指針指向的數據,就是可用窗口。
接收窗口的控制
發送窗口部分的內存,通過**=2個指針**進行管理,其中一個絕對指針,一個相對指針。
第一個絕對指針,指向可以接收數據但是還沒收到數據的空間的第一個字節。
第二個相對指針,指向還沒收到數據并且不可接收數據的空間的第一個字節,通過第一個指針 + 滑動窗口大小(滑動窗口大小是會變化的)得到。
第一個指針指向的數據 就是發送方的滑動窗口。在第一個指針之前,是已收到并且已確認的數據。(由于ACK直接由TCP協議棧回復,默認無應用延遲,不存在“已接收未回復ACK)
滑動窗口的流量控制
TCP 通過讓接收方指明希望從發送方接收的數據大小(窗口大小)來進行流量控制。
如果窗口大小為 0 時,就會阻止發送方給接收方傳遞數據,直到窗口變為非 0 為止,這就是窗口關閉。
死鎖
在服務器因為某些情況,導致窗口關閉后,客戶端不可以發送數據。
服務器窗口再度開放后,會給客戶端發送一個ACK報文,告訴窗口非0。
如果這個報文丟失了,就會造成死鎖。
死鎖的解決方法
TCP 為每個連接設有一個持續定時器,只要客戶端收到服務端的關閉窗口通知,就啟動持續計時器。
如果持續計時器超時,就會發送窗口探測 ( Window probe ) 報文。服務器在ACK報文中會告知窗口大小。如果還是關閉窗口,客戶端就再次計時。
如果客戶端多次進行窗口探測,服務器窗口都是關閉,可能會斷開連接。
糊涂窗口綜合征
本部分內容引用自:小林Coding - 糊涂窗口綜合征
如果接收方太忙了,來不及取走接收窗口里的數據,那么就會導致發送方的發送窗口越來越小。
到最后,如果接收方騰出幾個字節并告訴發送方現在有幾個字節的窗口,而發送方會義無反顧地發送這幾個字節,這就是糊涂窗口綜合癥。
要知道,我們的 TCP + IP 頭有 40 個字節,為了傳輸那幾個字節的數據,要搭上這么大的開銷,這太不經濟了。
所以,糊涂窗口綜合癥的原因是:
接收方可以通告一個小的窗口
而發送方可以發送小數據
對策方向是:
讓接收方不通告小窗口給發送方
讓發送方避免發送小數據
對策
讓接收方不通告小窗口
當「窗口大小」小于 min( MSS,緩存空間/2 ) ,也就是小于 MSS 與 1/2 緩存大小中的最小值時,就會向發送方通告窗口為 0,也就阻止了發送方再發數據過來。
延遲確認
==延遲確認也有避免發送小窗口的作用==參照上面延確認內容
等到接收方處理了一些數據后,窗口大小 >= MSS,或者接收方緩存空間有一半可以使用,就可以把窗口打開讓發送方發送數據過來。
怎么讓發送方避免發送小數據呢?
使用 Nagle 算法。
Nagle算法
Nagle :/?ne?g?l/
該算法的思路是延時處理,只有滿足下面兩個條件中的任意一個條件,才可以發送數據:
條件一:要等到窗口大小 >= MSS 并且 數據大小 >= MSS;
條件二:收到之前發送數據的 ack 回包;
Nagle算法會避免發送小數據。但是有些場合可能就是需要發送小數據。可以
可以在 SetSockopt中通過TCP_NODELAY 選項來關閉本Socket的Nagle算法。
TCP 擁塞控制
流量控制,是用來控制窗口的。
擁塞控制是根據網絡狀況而進行的控制。
一般來說,計算機網絡都處在一個共享的環境。因此也有可能會因為其他主機之間的通信使得網絡擁堵。
在網絡出現擁堵時,如果繼續發送大量數據包,可能會導致數據包時延、丟失等,這時 TCP 就會重傳數據,但是一重傳就會導致網絡的負擔更重,于是會導致更大的延遲以及更多的丟包,這個情況就會進入惡性循環被不斷地放大…
所以,TCP 不能忽略網絡上發生的事,它被設計成一個無私的協議,當網絡發送擁塞時,TCP 會自我犧牲,降低發送的數據量。
于是,就有了擁塞控制,控制的目的就是避免「發送方」的數據填滿整個網絡。
來源:小林 Coding - 擁塞控制
擁塞窗口
TCP用發送窗口,和擁塞窗口。
擁塞窗口是根據網絡擁塞狀況來動態變化的。
對于發送端來講,需要獲取接收端的接收窗口大小,也要獲取擁塞窗口大小。
發送端的發送窗口 約等于 ming(接收端的接收窗口,擁塞窗口)
只要發生了超時重傳,就會認為網絡出現了擁塞,就會減小擁塞窗口。反之擁塞窗口會增大。
而擁塞窗口的變化,主要是依賴四個算法:
慢啟動
擁塞避免
擁塞發生
快速恢復
慢啟動
慢啟動就是TCP剛建立連接時,在其實擁塞窗口的基礎上,慢慢增大擁塞窗口的意思,其增大規則是:
發送方每收到一個 ACK,擁塞窗口 cwnd 的大小就會加 1。
例如,當前窗口,發送方可以發送10個TCP報文,然后收到了10個確認應答報文。此時發送方的擁塞窗口增大到可以發送20個TCP報文。
慢啟動算法是指數性增長。
慢啟動門限
慢啟動門限, ssthresh (slow start threshold),一般是65535 字節。
當 擁塞窗口 < ssthresh 時,使用慢啟動算法。
當 擁塞窗口 >= ssthresh 時,就會使用「擁塞避免算法」。
擁塞避免
擁塞避免階段,每當收到一個 ACK 時,cwnd 增加 1/cwnd。
此時是線性增長。
雖然擁塞避免階段,擁塞窗口增長的速率放慢了,但是最終可能還是會進入擁堵,開始出現丟包。一旦出現丟包,進入擁塞發生階段。
擁塞發生
在發生丟包后,會進入超時重傳,或者快速重傳。
不同的重傳方式,對應了不同的擁塞發生算法。
超時重傳- 擁塞發生算法
此時,慢啟動門限ssthresh 設為 cwnd/2,同時cwnd 重置為 初始化值,然后再進行慢啟動。
門啟動門限ssthresh初始值
Linux 針對每一個 TCP 連接的 cwnd 初始化值是 10,也就是 10 個 MSS,我們可以用 ss -nli 命令查看每一個 TCP 連接的 cwnd 初始化值。
小林Coding - 擁塞發生
快速重傳- 擁塞發生算法
此時認為網絡情況沒有那么糟糕。
擁塞窗口 =擁塞窗口/2 ,也就是設置為原來的一半,這里和上面一樣。
但是慢啟動門限設置為和擁塞窗口一樣大。然后,進入快速恢復算法。
快速恢復
快速恢復和 擁塞發生-快速重傳 算法搭配使用,此時認為網絡擁塞沒有那么糟。
本部分內容引用自:小林Coding - 快速恢復
在 擁塞發生-快速重傳的基礎上,擁塞窗口 = ssthresh + 3 ( 3 的意思是確認有 3 個數據包被收到了);
重傳丟失的數據包;
如果再收到重復的 ACK,那么 cwnd 增加 1;
如果收到新數據的 ACK 后,把 cwnd 設置為第一步中的 ssthresh 的值,原因是該 ACK 確認了新的數據,說明從 duplicated ACK 時的數據都已收到,該恢復過程已經結束,可以回到恢復之前的狀態了,也即再次進入擁塞避免狀態;
超時重傳的擁塞算法圖像
來源:小林Coding - 擁塞發生
快速重傳的擁塞算法圖像
來源:小林Coding - 擁塞發生
參考:4.2 TCP 重傳、滑動窗口、流量控制、擁塞控制