用戶數據報協議(UDP)
UDP是一個簡單的傳輸層協議(RFC 768)。
進程往一個UDP套接字寫入一個消息,該消息隨後被封裝(encapsulating)到一個UDP數據報,該UDP數據報進而又被封裝到一個IP數據報,然後發送到目的地。
(1) UDP的幾個“不保證”
[1] 不保證UDP數據報會到達其最終目的地;
[2] 不保證各個數據報的先後順序跨網絡後保持不變;
[3] 不保證每個數據報只到達一次;
……
總之,UDP不提供可靠性,其本身不提供確認、序列號、RTT估算、超時、重傳、流量控制等機制。如果想要確保一個數據報達到其目的地,可以往應用程序中添置一大堆的特性:來自對端的確認、本端的超時與重傳等。即,UDP應用必須處理所有這些情況。
(2) UDP幾個特點
[1] 每個UDP數據報都有一個長度,而TCP是一個字節流(byte-stream)協議,沒有任何記錄邊界,這一點不同於UDP;
[2] UDP提供無連接的(connectionless)服務,因爲UDP客戶與服務器之間不必存在任何長期的聯繫。即,一個UDP客戶可以創建一個套接字併發送一個數據報給一個給定的服務器,然後立即用同一個套接字發送另一個數據報給另一個服務器;同樣,一個UDP服務器可以用同一個UDP套接字從若干個不同的客戶接收數據報;
傳輸控制協議(TCP)
由TCP向進程提供的服務不同於由UDP提供的服務。
(1) TCP提供客戶與服務器之間的連接(connection)。TCP客戶先與某個給定服務器建立一個連接,再跨該連接與那個服務器交換數據,然後終止這個連接。
(2) TCP還提供了可靠性(reliability)。當TCP向另一端發送數據時,它要求對端返回一個確認。如果沒有收到確認,TCP就自動重傳數據並等待更長時間,在數次重傳失敗後,TCP才放棄,如此在嘗試發送數據上所花的總時間一般爲4~10分鐘(依賴於具體實現)。即,TCP的可靠性是指“數據的可靠遞送或故障的可靠通知”。
(3) TCP含有用於動態估算客戶和服務器之間的往返時間(round-trip time, RTT)的算法,以便它知道等待一個確認需要多少時間。
(4) TCP通過給其中每個字節關聯一個序列號,對所發送的數據進行排序(sequencing)。例如,假設一個應用寫2048字節到一個TCP套接字,導致TCP發送2個分節:第一個分節所含數據的序列號爲1~1024,第二個分節所含數據的序列號爲1025~2048。(注意:分節是TCP傳遞給IP的數據單元。)如果這些分節非順序到達,接收端TCP將先根據它們的序列號重新排序,再把結果數據傳遞給接收應用。如果接收端TCP接收到來自對端的重複數據(譬如說,對端認爲一個分節已丟失並因此重傳,而這個字節並沒有真正丟失,只是網絡通信過於擁擠),它可以根據序列號判定數據是重複的,從而丟棄重複數據。
(5) TCP提供流量控制(flowcontrol)。TCP總是告知對端在任何時刻它一次能夠從對端接收多少字節的數據,這稱爲通告窗口(advertised window)。即,在任何時刻,該窗口指出接收緩衝區中當前可用的空間量,從而確保發送端發送的數據不會使接收緩衝區溢出。
該窗口時刻動態變化:當接收到來自發送端的數據時,窗口大小就減小;當接收端應用從緩衝區中讀取數據時,窗口大小就增大。
通知窗口大小減小到0是有可能的:當TCP對應某個套接字的接收緩衝區已滿,導致它必須等待應用從該緩衝區讀取數據時,方能從對端再接收數據。
(6) TCP連接是全雙工的(full-duplex)。即,在一個給定的連接上應用可以在任何時刻在進出兩個方向上既發送數據又接收數據。因此,TCP必須爲每個數據流方向跟蹤諸如序列號和通知窗口大小等狀態信息。
TCP連接的建立和終止
(1) TCP連接建立——三次握手
[1] 服務器必須準備好接收外來的連接,這通常通過調用socket、bind和listen這3個函數來完成,稱之爲“被動打開”(passive open);
[2] 客戶調用connect發起“主動打開”(active open)。這導致客戶TCP發送一個SYN(同步)分節,它告訴服務器客戶將在(待建立的)連接中發送的數據的初始序列號。通常SYN分節不攜帶數據,其所在IP數據報只含有一個IP首部、一個TCP首部及可能有的TCP選項;
[3] 服務器必須確認(ACK)客戶的SYN,同時自己也得發送一個SYN分節,它含有服務器將在同一連接中發送的數據的初始序號列。服務器在單個分節中發送SYN和對客戶SYN的ACK(確認);
[4] 客戶必須確認服務器的SYN;
這種交換至少需要3個分組,因此稱之爲TCP的三路握手(three-way handshake)。
(2) TCP選項
每一個SYN可以包含有多個TCP選項,一些常用的選項有:
[1] MSS選項
發送SYN的TCP一端使用本選項通告對端它的最大分節大小(maximum segmentsize, MSS),即,它在本連接的每個TCP分節中願意接收的最大數據量。發送端TCP使用接收端的MSS值作爲所發送分節的最大大小。TCP_MAXSEG
[2] 窗口規模選項
TCP連接任何一端能夠通告對端的最大窗口大小是65535,因爲在TCP首部中相應的字段佔16位。但目前要求有更大的窗口以獲得儘可能大的吞吐量,這個新選項指定TCP首部中的通告窗口必須擴大(即左移)的位數(0~14),因此所提供的最大窗口接近1GB(65535*2^14)。
在一個TCP連接上使用窗口規模的前提是它的兩個端系統必須都支持這個選項。SO_RCVBUF
[3] 時間戳選項
這個選項對於高速網絡連接是必要的,它可以防止由“失而復現的分組”可能造成的數據損壞。
PS: “失而復現的分組”不是超時重傳的分組,而是由暫時的路由原因造成的迷途的分組,當路由穩定後,它們又會正常到達目的地,其前提是它們在此前尚未被路由器丟棄。高速網絡中32位的序列號短時間內就可能循環一輪重新使用,若不用時間戳選項,失而復現的分組所承載的分節可能與再次使用相同序列號的真正分節發生混淆。
(3) TCP連接終止——四次握手(通常)
TCP終止一個連接則需4個分節。
[1] 某個應用進程首先調用close,稱該端執行“主動關閉”(active close)。該端的TCP於是發送一個FIN分節,表示數據發送完畢。
[2] 接收到這個FIN的對端執行 “被動關閉”(passive close),這個FIN由TCP確認。
注意:FIN的接收也作爲一個文件結束符(end-of-file)傳遞給接收端應用進程,放在已排隊等候該應用進程接收的任何其他數據之後,因爲,FIN的接收意味着接收端應用進程在相應連接上再無額外數據可接收。
[3] 一段時間後,接收到這個文件結束符的應用進程將調用close關閉它的套接字。這導致它的TCP也發送一個FIN。
[4] 接收這個最終FIN的原發送端TCP(即執行主動關閉的那一端)確認這個FIN。
既然每個方向都需要一個FIN和一個ACK,因此通常需要4個分節。
注意:
[1]“通常”是指,某些情況下,步驟1的FIN隨數據一起發送,另外,步驟2和步驟3發送的分節都出自執行被動關閉那一端,有可能被合併成一個分節。
[2] 在步驟2與步驟3之間,從執行被動關閉一端到執行主動關閉一端流動數據是可能的,這稱爲“半關閉”(half-close),可參考shutdown。
[3] 當一個Unix進程無論自願地(調用exit或從main函數返回)還是非自願地(收到一個終止本進程的信號)終止時,所有打開的描述符都被關閉,這也導致仍然打開的任何TCP連接上也發出一個FIN。
[4] 無論是客戶還是服務器,任何一端都可以執行主動關閉。通常情況是,客戶執行主動關閉,但是某些協議,例如,HTTP/1.0卻由服務器執行主動關閉。
TCP狀態轉換圖
TCP涉及連接建立和連接終止的操作可以用狀態轉換圖(state transitiondiagram)來說明。這些狀態可使用netstat工具顯示,方便監視狀態的變化。
TCP爲一個連接定義了11種狀態,並且TCP規則規定如何基於當前狀態及在該狀態下所接收的分節從一個狀態轉換到另一個狀態。
11種狀態,含義如下:
狀態 | 描述 |
(1) CLOSED | 關閉狀態,沒有連接活動或正在進行 |
(2) LISTEN | 監聽狀態,服務器正在等待連接進入 |
(3) SYN_RCVD | 收到一個連接請求,尚未確認 |
(4) SYN_SENT | 已經發送連接請求(SYN),等待確認 |
(5) ESTABLISHED | 連接建立,正常數據傳輸狀態 |
(6) FIN_WAIT_1 | (主動關閉)已經發送關閉請求(FIN),等待確認 |
(7) FIN_WAIT_2 | (主動關閉)收到對方關閉確認(ACK),等待對方關閉請求(FIN) |
(8) TIME_WAIT | 完成雙向關閉,等待所有分組死掉(目的是防止主動關閉端最後發出的ACK丟失,接收端處於LAST_ACK超時重發FIN,因此主動關閉端會進入TIME_WAIT狀態,在壓力測試時,這種狀態會大量存在,進程所佔用的端口號不能被釋放,導致後來的連接建立失敗) |
(9) CLOSING | 雙方同時嘗試關閉,等待對方確認 |
(10) CLOSE_WAIT | (被動關閉)收到對方關閉請求,已經確認 |
(11) LAST_ACK | (被動關閉)等待最後一個關閉確認(ACK),等待所有分組死掉 |
(1) 連接建立——三次握手
當某個進程在CLOSED狀態下執行主動打開時,TCP將發送一個SYN,且新的狀態是SYN_SENT,如果這個TCP接着接收到一個帶ACK的SYN,它將發送一個ACK,且新的狀態是ESTABLISHED,這個最終狀態是絕大多數數據傳送發生的狀態。
(2) 連接終止——四次握手
自ESTABLISHED狀態引出的兩個“方向”處理連接的終止。如果某個進程在接收到一個FIN之前調用close(主動關閉),那就轉換到FIN_WAIT_1狀態;但如果某個進程在ESTABLISHED狀態期間接收到一個FIN(被動關閉),那就轉換到CLOSE_WAIT狀態。
注意:
[1] 客戶端和服務器端都可以執行主動關閉和被動關閉。
[2] 關於同時打開(simultaneous open),發生在兩端幾乎同時發送SYN並且這兩個SYN在網絡中交錯的情形下;同時關閉(simultaneousclose),發生在兩端幾乎同時發送FIN的情形下。(在TCPv1的第18章有這兩種情況的討論)
分組交換
討論一個完整的TCP連接所發生的實際分組交換情況,包括連接建立、數據傳送和連接終止3個階段,並且表明每個端點所歷經的TCP狀態。
(1) 客戶通告一個值爲536的MSS(最大分節大小),表明該客戶只實現了最小重組緩衝區大小;服務器通告一個值爲1460的MSS(以太網上IPv4的典型值)。不同方向上MSS值可以不同。
(2) 連接建立後,客戶就構造一個請求併發送給服務器。這裏假設該請求適合於單個TCP分節,即請求的大小小於服務器通告的值爲1460字節的MSS;服務器處理完該請求併發送一個應答,假設該應答也適合於單個分節,即小於536字節。
注意:
[1] 服務器對客戶請求的確認可以是伴隨其應答一起發送的,即P+ACK,這種做法稱爲“捎帶”(piggybacking),它通常在服務器處理請求併產生應答的時間少於200ms時發生。如果服務器耗用更長時間,譬如說1s,那麼將看到先是確認後是應答。(參考TCPv1第19章和第20章的TCP數據流機理)
(3) 最後是終止連接的4個分節。執行主動關閉的一端,將進入TIME_WAIT狀態。
TIME_WAIT狀態
(1) 2MSL
執行主動關閉的那端會經歷TIME_WAIT狀態,該端點停留在這個狀態的持續時間是:最長分節生命期的兩倍(maximumsegment lifetime, MSL),即,2MSL。
任何TCP實現都必須爲MSL選擇一個值。RFC 1122[Braden 1989]的建議值是2分鐘,不過源自Berkeley的實現傳統上改用30秒這個值。這意味着:TIME_WAIT狀態的持續時間在1分鐘到4分鐘之間。
MSL是任何IP數據報能夠在因特網中存活的最長時間,這個時間是有限的,因爲每個數據報含有一個稱爲跳限(hop limit)的8位字段,可見IPv4的TTL字段,它的最大值爲255。注意:儘管這是一個跳數限制而不是真正的時間限制,我們仍然假設:具有最大跳限(255)
的分組在網絡中存在的時間不可能超過MSL秒。
(2) TIME_WAIT狀態存在的理由
有兩個理由:
[1] 可靠地實現TCP全雙工連接的終止。
[2] 允許老的重複分節在網絡中消失。
理由1的解釋:
假設最終的ACK丟失了,服務器將重新發送它的最終那個FIN,因此客戶必須維護狀態信息,以允許它重新發送最終那個ACK。要是客戶不維護狀態信息,它將響應以一個RST(另外一種類型的TCP分節),該分節將被服務器解釋成一個錯誤。
如果TCP打算執行所有必要的工作以徹底終止某個連接上兩個方向的數據流,即全雙工關閉,那麼它必須正確處理連接終止序列4個分節中任何一個分節丟失的情況。
爲什麼執行主動關閉的那一端是處於TIME_WAIT狀態的那一端:因爲可能不得不重傳最終那個ACK的就是主動關閉端。
理由2的解釋:
TCP必須防止來自某個連接的老的重複分組在該連接已終止後再現,從而被誤解成屬於同一連接的某個新的化身。既然TIME_WAIT狀態的持續時間是2MSL,這就足以讓某個方向上的分組最多存活MSL秒即被丟棄,另一個方向上的應答最多存活MSL秒也被丟棄。通過實施這個規則,就能保證每成功建立一個TCP連接時,來自該連接先前化身的老的重複分組都已在網絡中消逝了。
關於TMIE_WAIT狀態的進一步說明
在內核協議層通過設置以下兩個參數來解決TIME_WAIT問題:
(1) TIME_WAIT狀態可以重用,這樣即使TIME_WAIT佔滿了所有端口,也不會拒絕新的請求;
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
(2) 讓TIME_WAIT儘快回收
echo 1 >/proc/sys/net/ipv4/tcp_tw_recycle
PS: According to Linux documentation
tcp_tw_recycle - BOOLEAN 460 Enable fast recycling TIME-WAIT sockets. Default value is 0. 461 It should not be changed without advice/request of technical 462 experts. 463 464tcp_tw_reuse - BOOLEAN 465 Allow to reuse TIME-WAIT sockets for new connections when it is 466 safe from protocol viewpoint. Default value is 0. 467 It should not be changed without advice/request of technical 468 experts.
As described here, The TCP_TW_RECYCLE could cause some problems when using load balancers...
好處:
其實只修改tcp_tw_recycle就可以解決問題,TIME_WAIT重用TCP協議本身是不建議打開的。tcp_tw_recycle打開後,能在很短的時間能將TIME_WAIT的端口回收,但是具體時間並未找到相應的資料,測試觀察在1s左右。
問題:
打開加速回收或允許重用,也存在一定的問題。因爲,TIME_WAIT狀態需要等待2MSL的目的之一就是要保證老的重複分節在網絡中死掉,而如果快速回收的話,例如,發送端在發出最後一個ACK後立即被回收,而此ACK丟失,接收端超時重發FIN,而由於快速回收,恰好此時發送端使用剛纔的端口建立起新的連接,那新的連接將收到一個異常的FIN,從而可能引發新的連接被異常被動關閉。
TMIE_WAIT的解決方法
短連接最大的缺點是將佔用大量的系統資源,例如:本地端口、socket句柄。導致這個問題的原因其實很簡單:tcp協議層並沒有長短連接的概念,因此不管長連接還是短連接,連接建立->數據傳輸->連接關閉的流程和處理都是一樣的。
正常的TCP客戶端連接在關閉後,會進入一個TIME_WAIT的狀態,持續的時間一般在1~4分鐘,對於連接數不高的場景,1~4分鐘其實並不長,對系統也不會有什麼影響,但如果短時間內(例如1s內)進行大量的短連接,則可能出現這樣一種情況:客戶端所在的操作系統的socket端口和句柄被用盡,系統無法再發起新的連接!
舉例來說:假設每秒建立了1000個短連接(Web場景下是很常見的,例如每個請求都去訪問memcached),假設TIME_WAIT的時間是1分鐘,則1分鐘內需要建立6W個短連接,由於TIME_WAIT時間是1分鐘,這些短連接1分鐘內都處於TIME_WAIT狀態,都不會釋放,而Linux默認的本地端口範圍配置是:net.ipv4.ip_local_port_range = 32768 61000
不到3W,因此這種情況下新的請求由於沒有本地端口就不能建立了。
可以通過如下方式來解決這個問題:
1)可以改爲長連接,但代價較大,長連接太多會導致服務器性能問題,而且PHP等腳本語言,需要通過proxy之類的軟件才能實現長連接;
2)修改ipv4.ip_local_port_range,增大可用端口範圍,但只能緩解問題,不能根本解決問題;
3)客戶端程序中設置socket的SO_LINGER選項;
4)客戶端機器打開tcp_tw_recycle和tcp_timestamps選項;
5)客戶端機器打開tcp_tw_reuse和tcp_timestamps選項;
6)客戶端機器設置tcp_max_tw_buckets爲一個很小的值;
附相關流程圖片:
圖1 三次握手建立連接
圖2 四次握手關閉連接
圖3 TCP狀態轉換圖
圖4 TCP連接的分組交換