传输层的七七八八
TCP
TCP提供一種面向連接、可靠的字節(jié)流服務。
面向連接:兩端各自維護一份數(shù)據(jù)結構,傳輸數(shù)據(jù)之前,先進行數(shù)據(jù)結構部分信息的狀態(tài)同步,就是去建立連接,建立好之后才能傳輸數(shù)據(jù),不需要的時候斷開連接,然后釋放相關數(shù)據(jù)結構
可靠性:
- 由TCP將報文段分段為合適的大小后交給IP層
- TCP發(fā)出段后啟動定時器,目的端在定時器到期之前沒有確認應答,TCP會重發(fā)該段
- 接收端收到數(shù)據(jù)段后需要確認應答,告知發(fā)送端數(shù)據(jù)已經接收到哪里了(通過Sequence Number和acknowledgement Number記錄)
- TCP的首部校驗和由發(fā)送端計算和存儲,接收端如果校驗出錯,將包丟棄不發(fā)送確認應答,等待重發(fā)
- TCP分段后委托下層發(fā)送數(shù)據(jù)段,到達目的端如果出現(xiàn)亂序,TCP會重排序后交給應用層
- 如果數(shù)據(jù)重復,則丟棄
- 接收端會告知發(fā)送端能接受的最大數(shù)據(jù)段,實現(xiàn)流量控制
首部格式
各字段含義
- Source Port: 發(fā)送端端口號,例如http:80
- Destionation Port:接收端端口號
- Sequence Number(Seq):初始值由主機隨機生成,TCP流服務會對每個字節(jié)進行編號,對傳輸?shù)腄ata部分進行計數(shù)(不包含數(shù)據(jù)鏈路層、IP首部、TCP首部)。目的端的ACK會將Seq+Tcp Segment Data的值返回到發(fā)送端,這個值就是發(fā)送端下次發(fā)送時的seq值。
- 注:雖然SYN、FIN在首部,但傳輸他們時仍然會計數(shù),單位為1byte,所以三次握手和四次揮手時,雖然Tcp Segment Data的長度為0,回復的ACK值仍然要加1。
- Acknowledgement Number:Seq + Tcp Segment Data計算值后返回給發(fā)送端,表明該值和上次Seq值之間的數(shù)據(jù)我已經收到了,下次發(fā)送時以這個值作為Seq值發(fā)送
- Data offset:TCP首部的長度,如果沒有選項內容,首部長度為固定20 bytes,添加選項后最大首部長度為60 bytes(受限于該字段長度:4 bit,單位為4bytes)
- Reserved:該字段為了以后擴展使用,通常設置為0
- Control Flags:每一位代表一個標志,順序如上圖:
- CWR(Congestion Window Reduced):在網絡層的七七八八聊過,ECN(Explicit Congestion Notificat)的實現(xiàn)依靠IP首部記錄路由器是否遇到擁塞,在返回包的TCP首部中通知發(fā)生擁塞。CWR標志和ECE標志設置為1時,會通知對方網絡擁塞
- ECE(ECN-Echo)
- URG(Urgent Flag):為1時表示該數(shù)據(jù)段中有需要緊急處理的數(shù)據(jù)
- ACK(Acknowledgement Flag):TCP規(guī)定除了SYN包之外,該標志都設置為1,表示應答有效
- PSH(Push Flag):為1時表示將數(shù)據(jù)立即傳給上層協(xié)議,不進行緩存
- RST(Reset Flag):強制斷開連接
- SYN(Synchronize Flag):為1時表示想要建立連接,并設置Sequence Number的初始值(握手)
- FIN(Fin Flag):為1時表示不再發(fā)送數(shù)據(jù),希望斷開連接。主機收到設置FIN標志的包后,兩端主機對對方的FIN標志包進行確認應答。不必立即回復,可以等待緩沖區(qū)中的所有數(shù)據(jù)發(fā)送成功并刪除后再回復(揮手)
- Window Size:從ACK Number的位置開始,最大可以接收的數(shù)據(jù),發(fā)送發(fā)發(fā)送的數(shù)據(jù)不能超過該窗口大小。窗口為0時,對端可以發(fā)送窗口探測包。(1 byte)
- Checksum:校驗數(shù)據(jù)是否正確,覆蓋TCP首部和Data部分
- Urgent Pointer:在URG標志位1時該字段有效,緊急數(shù)據(jù)是從數(shù)據(jù)部分的首位到緊急指針所指的位置為止。Telnet Ctrl + C時會有URG為1的包
- Options:用于提高TCP的傳輸性能,長度最大為40bytes(首部最大60bytes - 固定部分20bytes),padding同IP首部的padding一樣,作為對options的填充,調整為32 bit的整數(shù)倍。options分為多種類型:
- 類型2:MSS(Maximum Segment Size),在建立連接時(發(fā)送SYN標志的報文段)中指定MSS,表示本端能接收的最大長度的報文段,通常來說MSS越大,網絡利用率越高。長度為 (MTU - IP首部 - TCP首部),對于以太網MSS長度可達1460bytes,默認為536bytes
- 類型3:WSOPT-Window Scale,可以提高TCP吞吐量,首部的windows size長度為16位,只能發(fā)送最大64KBytes,使用該選項可以擴展到1GBytes字節(jié),提升了單位RTT的數(shù)據(jù)傳輸量,從而增加吞吐。
- 類型8:上面介紹Sequence Number對傳輸數(shù)據(jù)的字節(jié)進行計數(shù),受32位長度的影響,如果高速傳輸一個很大的數(shù)據(jù)包,Sequence Number超出了內核解決序號回繞問題的范圍(回繞幅度2^31 - 1),那么接收端就無法判斷正確的序號了,加上該選項可以區(qū)分新老序號
數(shù)據(jù)傳輸
在主機網卡抓包繁雜信息太多,在虛擬機起一個基本的tcp server,回顯客戶端發(fā)送的消息后關閉,代碼如下:
import socket
import sys
HOST = "0.0.0.0"
PORT = 8888
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen(10)
connection, addr = server.accept()
data = connection.recv(1024)
print(data.decode())
connection.sendall(data)
connection.close()
server.close()
客戶端輸入消息發(fā)送,并接收服務端的消息打印
import socket
HOST = "10.211.55.3"
PORT = 8888
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST,PORT))
send_info = input("send message: ")
client.send(send_info.encode("utf-8"))
recv_info = client.recv(1024).decode("utf-8")
print(recv_info)
client.close()
三次握手建立連接
建立連接過程
- 客戶端發(fā)包(active open):開啟SYN標志,客戶端的initial seq = x
客戶端向服務端發(fā)起帶SYN標志的段,端口就是我們server.py中定義的8888,TCP Segment Len為0表示沒有傳輸數(shù)據(jù),Acknowledgment Number為0(第一次發(fā)起,沒什么好應答的),客戶端生成的Sequence Number為4176525122,wireshark幫我們分析的relative sequence number為0(下文使用relative sequence number),客戶端seq = 0
- 服務器回包(passive open):開啟SYN標志,服務端的initial seq = y和ack,ack的值為x+1(SYN占用一個序號)
服務端收到請求后,向客戶端發(fā)送SYN標志的段,數(shù)據(jù)段長度為0,服務端的seq為2608063952,同樣relative值為0,因為上一個包的SYN是占用sequence number的,所以ack = 1(客戶端的seq 0 + SYN標志位1,看raw值的話就是客戶端的seq4176525122 + 1 = 41766525123)
- 客戶端回包:開啟ACK標志,ack的值為服務端的y+1,seq為x+1
客戶端收到服務端的回包后,發(fā)送ACK段,數(shù)據(jù)段長度為0,客戶端seq為1,同樣ack = 1(服務端seq 0 + SYN標志位 1,raw值是2608063952 + 1 = 2608063953)
通過三次握手,客戶端與服務端協(xié)商好,客戶端下一次從2608063953處接收,服務端從41766525123處接收。
狀態(tài)變遷
-
server.py啟動后服務端處于LISTEN狀態(tài),客戶端client.py發(fā)送SYN標志包后處于SYN_SENT狀態(tài) - 服務端收到該包后會返回SYN標志的應答包,從LISTEN切換為SYN_RCVD狀態(tài)
- 客戶端發(fā)送ACK標志的應答包,切換為ESTABLISHED狀態(tài)
- 服務端收到ACK標志的包后,切換為ESTABLISHED狀態(tài)
之后就可以進行數(shù)據(jù)傳輸了。
數(shù)據(jù)傳輸
- 使用我們的client.py給server.py發(fā)送hello,數(shù)據(jù)段長度為5,根據(jù)上面的握手協(xié)商,本次發(fā)送數(shù)據(jù)的seq為41766525123,ack不變
- 服務端接收到數(shù)據(jù)后給客戶端發(fā)送ack,ack的值為 客戶端seq 41766525123 + tcp segment len 5 = 41766525128,服務端的seq為握手協(xié)商好的2608063953
- 應答后我們的server.py要將hello發(fā)回客戶端,于是數(shù)據(jù)段長度為5,seq為2608063953,ack跟第2步一樣
- 客戶端收到包后給服務端發(fā)送ack應答,ack值為服務端seq 2608063953 + 數(shù)據(jù)段長度 5 = 2608063958
四次揮手斷開連接
TCP是雙向傳輸?shù)模ㄈp工),兩端都各自維護一份連接狀態(tài),因此每個方向必須單獨的進行關閉,所以終止連接需要四次揮手(每一方都需要給對端發(fā)送FIN標志位的包,并且都需要給對方回復一個應答包):
- 為了實現(xiàn)四次揮手,TCP提供了半關閉的能力,即客戶端發(fā)送FIN包后表示不會再發(fā)送數(shù)據(jù),但是仍然可以接收數(shù)據(jù),服務器會應答該FIN包。
- 期間服務端可以繼續(xù)向客戶端發(fā)送未完成的數(shù)據(jù),服務端也不需要再發(fā)送時,會向客戶端發(fā)送FIN包,客戶端應答該FIN包,雙方連接徹底關閉。
斷開連接過程
斷開的請求可以由任意一端發(fā)起,server.py是讓服務端將客戶端發(fā)來的內容發(fā)送出去后直接關閉連接,所以本次抓包是從服務端發(fā)起斷開的
- 服務端發(fā)起關閉:開啟FIN標志,seq = x,len = 0,上一個包沒有數(shù)據(jù),ACK不變
因為之前應答過客戶端的hello,服務端seq變?yōu)?608063958,之前數(shù)據(jù)段的應答包已經發(fā)送過了(有時候會合并發(fā)送),所以ack不變?yōu)?176525128
- 客戶端回包:對FIN標志段進行應答,數(shù)據(jù)段長度為0,ack = x + 1,seq = y,len = 0
同樣因為應答過服務端的hello,客戶端seq為4176525128,F(xiàn)IN標志占用一個seq,所以ack為服務端seq 2608063958 + FIN標志 1 = 2608063959
- 客戶端發(fā)起關閉:開啟FIN標志,seq = y, ack = x + 1,len = 0
客戶端發(fā)起關閉,開啟FIN標志,seq為4176525128,第2步回包已經應答過服務端的FIN包,ack不變
- 服務端回包:對FIN標志段進行應答,seq = x + 1, ack = y + 1, len = 0
服務端發(fā)起應答,第2步客戶端會FIN包后告訴服務端下次從seq = 2608063959發(fā)送,所以seq = 2608063959,ack為客戶端seq 4176525128 + FIN標志 1 = 4176525129
至此,雙方連接徹底關閉。
狀態(tài)
借用劉超老師的圖,根據(jù)我們抓包的情況把左邊看成服務端,右邊看成客戶端
- 傳輸數(shù)據(jù)過程中客戶端和服務端都是ESTABLISHED狀態(tài),服務端發(fā)出FIN標志數(shù)據(jù)段后進入FIN_WAIT_1,等待客戶端回包。如果服務端收不到ACK回包,會重傳該報文(重傳次數(shù)由
tcp_orphan_retries控制),超時會斷開連接。 - 客戶端收到FIN包后,發(fā)送ACK包并進入CLOSE_WAIT狀態(tài),如果這個ACK丟失,服務端沒有收到,服務端會重傳FIN包再次等待客戶端的ACK。
- 服務端收到ACK包后進入FIN_WAIT_2狀態(tài),等待客戶端的FIN包發(fā)來。
- 如果調用close關閉連接,超過
tcp_fin_timeout規(guī)定的時間,客戶端沒有發(fā)來FIN包,那么服務端會直接關閉 - 如果服務端調用shutdown來關閉連接,仍然可以接收數(shù)據(jù),如果客戶端沒有發(fā)送FIN標志的包,那么服務端會一直處于FIN_WAIT_2狀態(tài)
- 如果調用close關閉連接,超過
- 客戶端發(fā)送FIN包后進入LAST_ACK,等待服務端的ACK,如果等不到會重傳FIN包,超過
tcp_orphan_retries就會斷開連接 - 服務端收到客戶端的FIN包,并發(fā)送ACK回包后進入TIME_WAIT狀態(tài),等待2MSL的時間后關閉連接,如果中間收到了客戶端重發(fā)的FIN包,會重置2MSL的定時器。如果沒有進行2MSL的等待直接退出,可能會出現(xiàn)客戶端的FIN包無法收到ACK的情況,這時客戶端再次發(fā)送FIN包會收到服務端RST的回包(Connection reset by peer)
- 客戶端收到服務端的ACK后徹底關閉
TIME_WAIT的2MSL
MSL(Maximum Segment Lifetime)是報文最大生存時間,MSL>=TTL的時間,確保了超過該時間報文會在網絡傳輸中被丟棄,TIME_WAIT設置為2倍MSL的設值允許報文至少被丟棄1次,linux停留在TIME_WAIT的時間為60秒,所以我們的server.py啟動并運行完之后立馬再次啟動會bind失敗,告知端口占用。
如果TIME_WAIT等待的時間不夠可能會將舊的seq插入新的連接數(shù)據(jù)中(
序列號回繞并延遲到達)
異常斷開連接
TCP是通過內核管理的,應用層需要通過send/recv來發(fā)送和接受數(shù)據(jù),如果連接斷開后應用層繼續(xù)recv,會收到Connection reset by peer(之前的項目中,Prometheus相關的日志會出現(xiàn)大量的Connection reset by peer,原因是后端收集數(shù)據(jù)太慢,而server又使用python的WSGI server,無法支持長連接,導致讀取時對端已經關閉的情況),如果是send則會收到Broken pipe
無數(shù)據(jù)傳輸異常斷開(tcp keepalive)
socket通過設置SO_KEEPALIVE啟動keepalive,可以通過sysctl -a查看系統(tǒng)設置,若開啟了keepalive,兩端沒有數(shù)據(jù)傳輸,一端崩潰,另一端發(fā)送探測包經過 7200 + 75 * 9 = 7875秒后認為對端掛了
# 過了7200秒后無數(shù)據(jù)交互啟動探測
net.ipv4.tcp_keepalive_time = 7200
# 探測間隔時間為75秒
net.ipv4.tcp_keepalive_intvl = 75
# 一共探測9次,一直無響應就認為對端掛了,關閉連接
net.ipv4.tcp_keepalive_probes = 9
如果服務端沒有開啟keepalive,這個tcp連接會一直處于ESTABLISHED狀態(tài),服務端重啟失效
端口失效(RST標志)
一般異常關閉連接的時候會使用RST標志,發(fā)出或收到該標志的內核會清理該連接相應的內存資源、端口。接收到RST的一端會收到Connection reset或者Connection refused。大概情況:
- 端口現(xiàn)在不可用
- socket關閉
- 客戶端消息發(fā)送完之前關閉了socket,會發(fā)一個RST到服務端
- 服務端關閉了socket,客戶端再發(fā)送消息,服務端會回復一個RST包
進程崩潰
上面說到TCP棧是由操作系統(tǒng)管理的,如果一方進程發(fā)生了崩潰并被系統(tǒng)感知,操作系統(tǒng)會與對端進行四次揮手結束連接
數(shù)據(jù)傳輸中主機崩潰
- 客戶端崩潰后重啟,服務端利用超時重傳機制重傳報文,客戶端之前連接的上下文都不存在了,會發(fā)送RST包到服務端關閉連接
- 客戶端永久下線,服務端超時重傳達到最大超時時間或最大重傳次數(shù),服務端會斷開連接并通過socket發(fā)送ETIMEOUT到應用程序
TCP狀態(tài)機
該狀態(tài)圖中A為客戶端,B為服務端。由客戶端發(fā)起連接,也由客戶端發(fā)起關閉。
- (1)(2)(3)(4)(5)表示發(fā)起連接的狀態(tài),可以對照三次握手
- (一)(二)(三)(四)(五)(六)表示斷開連接的狀態(tài),可以對照四次揮手
- 實線為客戶端A,虛線為服務端B
觸發(fā)重傳機制
超時重傳
上面的連接建立、傳輸數(shù)據(jù)、連接關閉都會出現(xiàn)數(shù)據(jù)包未被接收的情況,發(fā)送端觸發(fā)重傳機制,TCP提供可靠傳輸依靠確認接收端已經收到了數(shù)據(jù),也就是說通過數(shù)據(jù)發(fā)出去到收到接收端發(fā)來的ack報文才算是完成這一報文段的傳輸,用收到ack的時間戳減去發(fā)送數(shù)據(jù)的時間戳得到的差值就是這個包的RTT(Round-Trip Time),其中的問題是出去的包可能會丟失,對端收到數(shù)據(jù)后返回的ack也可能丟失。TCP通過在發(fā)送時設定一個定時器,如果超過定時器就重傳數(shù)據(jù),具體的問題就在于如何設置定時器間隔時間和重傳的頻率。
- 如果定時器時間設置過大,會出現(xiàn)網絡利用率低的情況,丟了很久了才重傳
- 如果定時器時間設置過小,可能網絡只是延遲略大,第一個包還沒到,觸發(fā)重傳的第二個包就發(fā)出來了,給鏈路增加了不必要的負載
所以重傳定時器的時間應略大于RTT比較合適,而網絡環(huán)境的速率是經常變化的。所以跟蹤測量RTT并且根據(jù)該值來動態(tài)設置重傳定時器RTO (Retransmission Time Out)
快速重傳
快速重傳的機制擁塞控制會用到,如果發(fā)送端接收到了3個相同的ack后,會在超時重傳的定時器過期之前重傳丟失的seq,比如發(fā)送了seq1~seq5,但是seq2、seq3都丟失了,接收端回復ack時回復的都是seq2的ack,再收到seq3的3個ack后,才能再重傳seq3的ack。網絡利用率嚴重下降。現(xiàn)在Linux中會開啟net.ipv4.tcp_sack=1和net.ipv4.tcp_dsack=1,分別對應SACK( Selective Acknowledgment)重傳機制和Duplicate SACK重傳機制。
SACK
在TCP首部options里設置SACK,接收端將已經收到的數(shù)據(jù)信息發(fā)送給發(fā)送端,這樣發(fā)送端在收到三次重復ACK后啟動快速重傳機制,但是根據(jù)這個字段可以看到丟失的數(shù)據(jù),重發(fā)則發(fā)送丟失的那些seq就可以了
Duplicate SACK
SACK主要是告訴發(fā)送端,哪些數(shù)據(jù)是重復發(fā)送了。可以判斷出是ack應答丟了導致的重發(fā),還是發(fā)送方的數(shù)據(jù)包延時到達導致的重發(fā)。
流量控制
滑動窗口
在我們數(shù)據(jù)傳輸部分的抓包中,本地網絡棧加上數(shù)據(jù)包較小,都是一發(fā)一答的順序來進行的。如果是遠端服務器,RRT時間較長的話傳輸會變得低效,應答包不承載數(shù)據(jù),只是告知發(fā)送端我的數(shù)據(jù)接收到哪里了,你下次從哪個seq開始發(fā)送,如果應答包丟了,發(fā)送端還得等著超時重傳再收到ack后發(fā)送下一段。所以如果要提高傳輸效率,引入了滑動窗口的概念:接收端可以告訴客戶端,我的緩沖區(qū)能放多少數(shù)據(jù),你看著發(fā)。至于接收端發(fā)回的ack,如果中間的某次ack走丟了,比如200299的ack收到了,300399的走丟了,400~499的收到了,那么發(fā)送端就認為,500之前的所有數(shù)據(jù)接收端都收到了,不用重傳,繼續(xù)發(fā)送。或者是接收端先不發(fā)送ack,直接發(fā)送一個500的ack,這種方式叫做累計應答。發(fā)送端和接收端都要維護一個窗口用來限制收發(fā)數(shù)據(jù)的大小。
發(fā)送端窗口1
- seq 1、2、3都發(fā)送并收到了確認
- seq 4~9是已經發(fā)送但是未收到ack確認的
- seq 10~12是發(fā)送端可以接著發(fā)送的
- seq 13~15超過了當前窗口大小,發(fā)送端不能發(fā)送,否則發(fā)出去也會被接收端丟掉
接下來發(fā)送端繼續(xù)發(fā)送10、11、12
服務端接收窗口1
- 藍色部分已經接收并發(fā)送了ack,但是應用層還沒有讀取,不占用窗口大小
- 橙色部分是已經收到了數(shù)據(jù),還沒有確認,此時可以直接確認ack=7,發(fā)送端收到ack后就會知道4、5、6的數(shù)據(jù)包接收端已經接收到了。因為存在7、8、9還沒有收到,所以無法發(fā)送ack=11的確認。
- 紅色部分超出窗口大小,無法接收
接下來服務端發(fā)送ack=7確認4、5、6已經收到,窗口滑動后如下
服務端接收窗口2
- 4、5、6已經ack,窗口向右移動3個
- 之前不能接收的13、14、15現(xiàn)在可以接收
客戶端發(fā)送窗口2
- 4、5、6已經ack,之前不可發(fā)送13、14、15已經發(fā)送等待服務端確認
窗口大小變化
應用程序無法及時讀取緩存內容
- 窗口的大小是通過兩端交互數(shù)據(jù)段中TCP首部的windows指定的,窗口大小即當前系統(tǒng)給TCP分配的緩存區(qū)大小受系統(tǒng)繁忙程度的影響,系統(tǒng)繁忙,應用層無法及時讀取TCP緩沖區(qū)中的內容(圖中藍色部分),那么TCP的窗口大小就要減小,告訴發(fā)送端,那么發(fā)送端下次發(fā)送的數(shù)據(jù)就減少。
- 極端情況下減小到0,發(fā)送端不能再發(fā)送任何數(shù)據(jù),這時會啟動定時器來發(fā)送探測包看窗口何時變大,如果回復的windows大小仍然為0,就重新啟動定時器。
操作系統(tǒng)減小TCP緩存
第一種情況緩沖區(qū)大小不變,只是改變接收窗口的大小。而更糟糕的情況是操作系統(tǒng)減小接收端緩沖區(qū)大小,TCP規(guī)定必須先減小窗口大小,然后才能減小緩沖區(qū)。如果發(fā)送端按照上次窗口的大小發(fā)送了120字節(jié)的數(shù)據(jù),而應用層還沒有處理緩沖區(qū)中的數(shù)據(jù),操作系統(tǒng)將緩沖區(qū)減小60字節(jié),此時接收端的窗口為60字節(jié),發(fā)送窗口通告告訴客戶端,但是消息已經發(fā)出,120字節(jié)的數(shù)據(jù)超過了當前窗口大小,發(fā)生丟包。
糊涂窗口綜合癥(Silly Window Syndrome)
接收端會通告一個小窗口,比方5字節(jié)的窗口,TCP首部的固定長度就有20字節(jié),再加上IP首部等長度,發(fā)送端的一個包實際傳輸了5字節(jié),但是包大小就有幾十字節(jié)。顯然網絡利用率大大降低,這個癥狀就是糊涂窗口綜合癥。
為了避免該現(xiàn)象發(fā)生,根據(jù)TCP首部選項里的MSS大小做控制:
- 發(fā)送端滿足以下條件之一才能發(fā)送
- 可以發(fā)送>=MSS長度的報文段
- 數(shù)據(jù)長度至少為接收端通告窗口大小的一半
- 之前數(shù)據(jù)的ack接收到之后
- 服務端通告窗口大小的方式
- 如果窗口大小小于MSS與1/2緩存大小中最小的一個,關閉窗口
- 窗口大小至少增長到MSS,或者超過1/2緩存大小,打開窗口
擁塞控制
流量控制是根據(jù)主機緩沖區(qū)大小調節(jié)滑動窗口來限制客戶端的發(fā)送,而擁塞控制相當于慢慢試探網絡帶寬有多大,擁塞狀況如何來調整發(fā)送端的發(fā)送速率,這里引入了擁塞窗口,實際的發(fā)送窗口等于滑動窗口和擁塞窗口中最小的一個。
擁塞窗口
擁塞窗口是由發(fā)送端決定的,試探的意思就是慢慢的增大擁塞窗口,如果觸發(fā)了超時重傳,就認為是網絡擁塞,較小擁塞窗口
慢啟動
建立連接后,每收到一個ACK就將擁塞窗口加1(單位為1個MSS):
- 收到第一個ACK時,擁塞窗口為1+1 = 2.
- 發(fā)送兩個報文,收到兩個ACK,擁塞窗口為2+2=4
- 4+4 = 8,指數(shù)型增長
增長到ssthresh(slow start threshold)65535 bytes這個值后,啟用擁塞避免
擁塞避免
- 上面擁塞窗口增長到8,收到8個ACK后,每個增長1/8
- 下次發(fā)送9個,收到9個ACK后,每個增長1/9
- 下次發(fā)送10個,線性增長
增長到觸發(fā)重傳機制后,表示網絡發(fā)生擁塞,啟動快速重傳
快速重傳
比如當前窗口是12,將之前的擁塞窗口減半為6,再將減半的值設置給ssthresh=6,再進入快速重傳:
- 如果收到了3個重復的ACK(類似快速重傳算法),擁塞窗口+3(為了盡快將丟失的包重傳)
- 重傳丟失的包
- 再收到重復的ACK后,把擁塞窗口+1
- 直到收到了新的ACK,再將ssthresh設置為進入快速重傳之前的值6
- 進入避免擁塞算法
UDP
與TCP不同,UDP的首部格式簡單,傳輸時也不需要事先建立連接,即不需要客戶端和服務端維護雙方交互的狀態(tài);因此TCP可以以數(shù)據(jù)流的形式發(fā)送,而進程產生一個UDP數(shù)據(jù)報,組裝成一份待發(fā)送的IP數(shù)據(jù)報,只能發(fā)送一個數(shù)據(jù)報或者接收一個數(shù)據(jù)報,加上UDP并無控制可言,所以很多行為與IP層類似。
首部格式
各字段含義
- Source Port:發(fā)送端端口號,如果不需要回復消息,該字段設置為0
- Destination Port:接收端端口號
- Length:UDP首部長度+Data長度(因為IP數(shù)據(jù)報的最大長度為65535,UDP頭+Data實際長度不大于 65535 - IP頭20 = 65515)
- Checksum:與TCP相同,校驗和覆蓋UDP的首部和Data部分,校驗數(shù)據(jù)包的正確性。與TCP不同的是,UDP可以設置為0表示不校驗(包括IP首部的地址和UDP首部都不校驗)。與IP層相同,如果發(fā)送端沒有計算校驗和而接收端發(fā)現(xiàn)校驗和有差錯,那么數(shù)據(jù)包會被直接丟棄而不產生差錯報文
場景
- UDP適用于對丟包不敏感并要求時延極低的應用,不考慮網絡擁塞,一股腦往出發(fā)。
- 因為UDP并不需要維護端對端連接,可以應用于廣播或者多播協(xié)議。例如基于UDP協(xié)議的TFTP、DHCP、VXLAN等
- 因為自身的簡單,只承載傳輸?shù)娜蝿铡K詰脤涌梢愿`活的按照自己的需求來做定制開發(fā),把需要維護的狀態(tài)放在應用層來做。
傳輸層的端口號
傳輸層的端口號用來分辨交給哪個應用程序處理
端口的使用
- 對于UDP和TCP在內核中是獨立存在的,所以可以綁定相同的地址和端口
- 如果兩個TCP程序要綁定相同的端口,那么需要綁定不同的IP地址
- 對于TCP,如果上文重啟server.py后會提示
Address already in use,開啟多路復用(socket設置SO_REUSEPORT)允許第一個socket處于TIME_WAIT的情況下,第二個socket可以使用該地址和端口
端口范圍
知名端口號(0~1023)
相當于一種約定,例如HTTP服務用80端口,HTTPs用443端口,F(xiàn)TP用21端口,SSH用22端口
登記端口號(1024~49151)
我們自己實現(xiàn)的服務器,從該范圍內申請端口長時間使用
客戶端端口號(49152~65535)
操縱系統(tǒng)動態(tài)分派,客戶端通信臨時使用的,用完之后連接關閉,操作系統(tǒng)回收端口號,分配給其他的進程使用
學習自:
《趣談網絡協(xié)議》劉超
《圖解TCP/IP》
《圖解HTTP》
《網絡是怎樣連接的》
《TCP/IP詳解 卷一》
小林coding
https://www.xiaolincoding.com/network/3_tcp/tcp_down_and_crash.html
https://xiaolincoding.com/network/3_tcp/tcp_feature.html
https://blog.csdn.net/GV7lZB0y87u7C/article/details/121186808
總結
- 上一篇: 整合 Disney + 及 Hulu 两
- 下一篇: Spring 缓存注解这样用,太香了!