當我們在學校學習網絡和網絡傳輸層時,我們可能總是感到枯草乏味、枯燥難懂。但事實上,在生産環境中,這是一個非常常見的問題,如果你不明白,可能會更困惑。
生産環境遇到的問題
談談我今年遇到的TCP層的幾個問題。
- 問題1:長短連接配接的選擇?
- 問題2:連接配接逾時了,為什麼逾時的時間是128s左右
- 問題3:系統不可達,80端口連不通了,可是本地檢視80端口是正常的,這是為什麼?
- 問題4:用戶端連接配接池很多處于CLOSE-WAIT?
傳輸層
要充分解釋這些問題,我們需要對傳輸層的協定有非常深入的了解。網絡層可能離軟體開發人員有點遠,但傳輸層,特别是廣泛使用的TCP,與我們的工作密切相關。
傳輸層的目的
- 從上到下依次包括 應用層、傳輸層、網絡層、鍊路層、實體層。
- 應用層就是對應不同的資料
- 通過網絡層,包已經能夠正确的被路由到對應的主機

我們需要有機制将不同的應用程式映射到同一主機的網絡層,并確定資料的準确到達。
差別不同的應用-UDP
- 每個應用程式對應一個端口,端口資訊也封裝在包中,以确定資料包屬于哪個應用的。
- UDP的封包格式為傳輸層的簡單協定,也很簡單。
保證傳輸的品質-TCP
UDP不保證資料的準确傳輸和品質保證。如果包丢失,網絡層不會重新傳輸。是以,傳輸層還設計了一種新的協定TCP。除了端口映射外,還在應用層和網絡層之間增加了一些處理機制,以確定傳輸品質。
傳輸的品質對應用而言實際上就2個方面:
- 收到了對端發出的所有資料。
- 對端發出的所有資料都按順序收到。
- C1.C2表示兩個用戶端與app1有資料互動。
- 假設app1向C2發送資料,app1的傳輸層應確定所有資料都發送到C2,以確定沒有丢包事件。
-
- 不丢包不是真的不丢包,而是如果丢包還能重傳,保證資料最終收到。
- 假設C1向app1發送資料,則需要按順序正确接收發送的資料。
核心:Tcp的具體運作機制
TCP要幹什麼
依據以上的描叙,TCP主要的要做2件事:
- 防止丢包
a.丢包重傳
一般情況下,收到包後會向發送者發送ack信号。
逾時未收到會使用重傳機制。
b. 減少丢包
告訴我你的視窗:感受對端的處理能力。
丢包原因及優化:感受網絡擁塞,控制傳輸速率。
- 保證包到達的順序。
使用序列号:確定資料包按正确順序傳遞。
基本試探-建立連接配接
正如上面提到的,TCP首先要試探兩個對端的收發能力,試探過程如下:
主要是試探并确認了:
- 确認A發送資料的能力
- 确認B接收資料的能力
- 确認B發送資料的能力
- 确認A接收資料的能力
這個試探過程也叫做三次握手,通過三次握手兩個應用程式之間建立了一條TCP連接配接。
具體的過程和狀态
TCP連接配接是核心抽象給應用程式使用的。
我們可以更具體地看看這個過程,包括建立連接配接之前、中間和之後。
三次握手的狀态随時間變遷圖如下:
三次握手就是三次發包的過程:
-
1、發起端發送SYNC包:SYN,seq=x,自己進入Sync-Sent狀态
注意:seq=x,下面還會有較長的描述
- 2、監聽端收到SYNC包,發送SYN,ACK,seq=y,ack=x+1,進入Sync-RCVD狀态
-
3、發起端收到SYNC,ACK包,發送 ACK,seq=x+1,ack=y+1,進入Established狀态
a、RTT表示發送資料到收到ACK的時間
- 4、監聽端收到ACK包,進入Established狀态
accept和LISTEN
伺服器的核心使用accept系統調用來幫助建立連接配接。伺服器調用accep函數後,處于LISTEN狀态。可以等待用戶端建立連接配接,并使用netstat檢視LISTEN狀态的連接配接。
# netstat -alpnt
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:11110 0.0.0.0:*
- *0.0.0.0:**表示接受網絡上所有端口的連接配接。
- 核心使用socket作為真正的tcp連接配接對象,但accept的socket是特殊的,沒有建立tcp連接配接。
ESTABLISHED
三次握手成功後,這個連接配接将儲存在記憶體中。
# netstat -alpnt
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 9.13.73.14:38604 9.13.39.104:3306 ESTABLISHED 26100/java
tcp 0 0 9.13.73.14:32926 9.21.210.17:8080 ESTABLISHED 26100/java
- ESTABLISHED:已建立連接配接。
-
連接配接的資訊包括本地IP端口位址和遠端IP端口。
a.本地9.13.73.14:38604與遠端9.13.39.104:3306建立連接配接。
b.本地9.13.73.14:32926與遠端9.21.210.17:8080建立連接配接。
對于問題3的回答
有一次,我們的服務無法到達,即80端口無法連接配接,但登入機器并發送80端口仍然是正常的清單狀态,但我們發現許多連接配接處于Sync-RCVD狀态。檢視日志,我們發現系統已經運作,完全有理由懷疑服務建立連接配接的過程是由記憶體引起的。
是以,盡管80端口的LISTEN狀态正常,但外部無法正常連接配接。
TCP封包格式和資訊交換
三次握手中發的資料報都是TCP封包,TCP封包格式如下:
從這個封包格式和上面三次握手的過程可以看出:
- SYN标記位為1說明這個封包是一個SYN類型的包,用于握手
- 發起端發送SYNC包:SYN,seq=x
- 監聽端收到SYNC包後,也發送自己的SYN包:SYN,seq=y
- 發起端和監聽端的起始序号x和y是32位序号,它們是系統随機生成的
- 在SYN封包中交換了初始序列号之後,這個序列号就一直單調遞增
- 初始序列号ISN還用于關閉連接配接的嗎?
- ACK标記位為1說明這個封包是一個ACK類型的包
- 32位确認号
- 監聽端收到SYNC包,還發送ACK,ack=x+1,小于x+1的全部位元組已經收到,**期待下一次收到seq=x+1的包
- 發起端收到SYNC,發送ACK,ack=y+1,小于y+1的全部位元組已經收到,期待下一次收到seq=y+1的包**
- 用于SYN和ACK連接配接的包的資料為空
另外封包格式中還包含下面資訊:
- 接收到的SYN包的視窗大小代表對方的接收視窗的大小
- RST标記位為1: 可以用于強制斷開連接配接
- PSH标記位為1:告知對方這些資料包收到後應該馬上交給上層的應用
- 選項中的MSS:TCP允許的從對方接收的最大封包段。
連接配接建立後,傳輸資料
連接配接建立後,資料可以傳輸。
回顧上述TCP主要保證的兩件事和基本思路:
- 使用序列号seq確定資料包按正确的順序傳遞。
- ack信号和逾時重傳機制防止丢包。
- 用窗戶減少丢包。
建立連接配接的過程為這件事情做好了鋪墊。讓我們來看看具體的運作機制。
逾時重傳和ACK深層含義
- 發送出去的資料如果一直沒收到ack就重傳,需要确定多久時間沒收到就重傳
- 接收端如果多次收到同一個seq的資料就丢棄
- 需要大于RTT,又不能太大
- RTT是在動态變化的,核心采樣,使用RTO來計算
每當遇到一次逾時重傳時,都會将下一次逾時間隔将設定為以前值的兩倍。多次逾時,表明網絡環境差,不宜頻繁重複發送,就會每次将成為原來的兩倍,可設定最大重傳次數。
現在就可以回答問題2了
初始1S逾時,然後因為系統設定的重傳次數是6,重傳了6次,1+2+4+8++16+32+64加起來大約是128s左右。
可以批量送出ack
seq=4的ack沒有正确的收到, 但如果在逾時時間内收到了ack=6 則表示6之前的seq都已經正确接收到了 seq=4的資料也不會重傳
滑動的視窗和右移的指針
發送端的socket緩沖區,示意圖如下:
- 已發送并收到ACK确認的資料
- 已發送但未收到ACK确認的資料
- 未發送但總大小在接收方處理範圍内
- 未發送但總大小超過接收方處理範圍
視窗右移
- 圖示的發送視窗和收到ack封包中的window大小相關
- ack指針右移則可用視窗變大
- 發送seq指針右移則可用視窗變小
接收端的socket緩沖區,示意圖如下:
- ACK包,ack=16,且(window=16)
- 已成功接收并确認的資料
- ack指針右移則可用視窗還是右移
- 接收視窗是未收到資料但可以接收的資料
- 可用視窗和應用程式擷取資料的能力有關,如果應用程式一直不從socket緩沖區擷取資料,則接收視窗也會變得越來越小
- 滑動視窗并不是一成不變的。當接收方的應用讀取資料的速度非常快的話,接收視窗可以很快的空缺出來。
- 通過使用視窗可以起到流控的作用,可以減少不必要的丢包,并減少網絡擁塞。
順序的保證
如下圖:
假設接收端未收到16,17的封包, 兒後面18-27的資料都收到了, 則并不會發ack給發送方 當發送端逾時重傳16,17之後,且接收端收到之後 則接收端傳回ack=28的封包給發送端。
- ack指針隻能右移,不能往回走
- 這樣可以保證順序傳遞
傳輸的總結
TCP的主要功能是防止包丢失,保證包到達的順序。本節通過描述TCP的具體工作機制證明了TCP确實達到了這一目的。
高并發系統和關閉連接配接的設計
最後,我完成了TCP連接配接建立和傳輸的基本原理和過程,認為我可以松一口氣。
關閉連接配接是TCP的一部分,但與TCP相比,這并不重要。
但最近發現,在生産活動中,大家也非常關注TCP四次揮手的過程。主要原因是系統并發量大。
TCP連接配接是衡量系統并發量的重要因素,如果關閉連接配接異常,必然會影響系統的運作。
對于連接配接對并發量的影響,首先可以分析開頭提出的問題。
長連接配接 VS 短連結
- 短連接配接一般隻會在 client/server間傳遞一次請求操作
- 這時候雙方任意都可以發起close操作
- 短連接配接管理起來比較簡單,存在的連接配接都是有用的連接配接,不需要額外的控制手段。
- 通常浏覽器通路伺服器的時候一般就是短連接配接。
- 長連接配接
- Client與server完成一次讀寫之後,它們之間的連接配接并不會主動關閉,後續的讀寫操作會繼續使用這個連接配接。
- 是以一條連接配接保持幾天、幾個月、幾年或者更長時間都有可能,隻要不出現異常情況或由使用者(應用層)主動關閉。
- 長連接配接可以省去較多的TCP建立和關閉的操作,減少網絡阻塞的影響,
- 減少CPU及記憶體的使用,因為不需要經常的建立及關閉連接配接。
- 連接配接數過多時,影響服務端的性能和并發數量。
是以,長短連接配接怎麼選擇呢?
- 是以對于并發量大,請求頻率低的,建議使用短連接配接。
- 對于服務端來說,長連接配接會耗費服務端的資源
- 如果有幾十萬,上百萬的連接配接,服務端的壓力會非常大,甚至會崩潰
- 對于并發量小,性能要求高的,建議選擇長連接配接
- 比如mysql連接配接池
TCP關閉連接配接
長短連接配接的選擇如此重要,如果連接配接不能正确關閉,就會造成很大的麻煩。TCP設計了完善的關閉機制,關閉連接配接過程如下:
- 關閉連接配接發起方 發起第一個FIN,處于FIN-WAIT1
- 關閉連接配接被動方的核心代碼回複ACK,此時還可以發送資料,處于Close-WAIT狀态
- 關閉連接配接被動方等待應用程式發送FIN,如果上層應用一直不發FIN,就還可以繼續發送資料
- 關閉連接配接發起方收到ACK後處于FIN-WAIT2狀态,還可以接收資料自己不再發送資料
- 關閉連接配接被動方直到應用程式發出FIN,處于LAST-ACK狀态
- 關閉連接配接發起方收到FIN,會發送ACK,自己會處于TIME-WAIT狀态,此時
- 若是關閉連接配接被動方收到ack,就close連接配接
- 若是是關閉連接配接被動方沒收到ack,則會重傳FIN
TIME-WAIT等多長時間
MSL是封包最大的生存時間。它是任何封包在網絡上存在的最長時間。超過這個時間,封包将被丢棄,即MSL的兩倍。TCP的TIME_WAIT狀态也稱為2MSL等待狀态。
等待2MSL時間的主要目的是害怕對方沒有收到最後一個ACK包,是以對方會在逾時後重發第三次握手的FIN包。
若之前互動異常,收到重傳的FIN最多使用2MSL,是以結論是等待2MSL的時間。
最後一個問題
有一次,同步資料的應用從一個伺服器并發同步大量資料,這個過程比較緩慢,是以考慮如何加快同步。
- 首先檢視網絡連接配接,發現20個連接配接的連接配接池中有很多處于close_wait狀态的連接配接。
通過以上分析,我們知道Close-WAIT是對方可能認為連接配接空閑時間太長而關閉的連接配接,但我在這裡使用的連接配接池還沒有發送FIN釋放包。
可見看出,連接配接的數量并沒有成為系統的瓶頸,我們可以繼續增加并發線程的數量,以增加并發量。