
RFC793文檔里帶有SYN標志的過程包是不可以攜帶數據的,也就是說三次握手的前兩次是不可以攜帶數據的(邏輯上看,連接還沒建立,攜帶數據好像也有點說不過去)。重點就是第三次握手可不可以攜帶數據。
先說結論:TCP協議建立連接的三次握手過程中的第三次握手允許攜帶數據。

對照著上邊的TCP狀態變化圖的連接建立部分,我們看下RFC793文檔的說法。RFC793文檔給出的說法如下(省略不重要的部分):

重點是這句 “Data or controls which were queued for transmission may be included”,也就是說標準表示,第三次握手的ACK包是可以攜帶數據。
首先, 第三次握手的包是由連接發起方(以下簡稱客戶端)發給端口監聽方(以下簡稱服務端)的,所以只需要找到內核協議棧在一個連接處于SYN-RECV(圖中的SYN_RECEIVED)狀態時收到包之后的處理過程即可。經過一番搜索后找到了,位于 net\ipv4目錄下tcp_input.c文件中的tcp_rcv_state_process函數處理這個過程。如圖:

這個函數實際上是個TCP狀態機,用于處理TCP連接處于各個狀態時收到數據包的處理工作。這里有幾個并列的switch語句,因為函數很長,所以比較容易看錯層次關系。下圖是精簡了無需關注的代碼之后SYN-RECV狀態的處理過程:

一定要注意這兩個switch語句是并列的。所以當TCP_SYN_RECV狀態收到合法規范的二次握手包之后,就會立即把socket狀態設置為TCP_ESTABLISHED狀態,執行到下面的TCP_ESTABLISHED狀態的case時,會繼續處理其包含的數據(如果有)。
上面表明了,當客戶端發過來的第三次握手的ACK包含有數據時,服務端是可以正常處理的。那么客戶端那邊呢?那看看客戶端處于SYN-SEND狀態時,怎么發送第三次ACK包吧。如圖:

tcp_rcv_synsent_state_process函數的實現比較長,這里直接貼出最后的關鍵點:

一目了然吧?if 條件不滿足直接回復單獨的ACK包,如果任意條件滿足的話則使用inet_csk_reset_xmit_timer函數設置定時器等待短暫的時間。這段時間如果有數據,隨著數據發送ACK,沒有數據回復ACK。
之前的疑問算是解決了。
條件1:sk->sk_write_pending != 0
這個值默認是0的,那什么情況會導致不為0呢?答案是協議棧發送數據的函數遇到socket狀態不是ESTABLISHED的時候,會對這個變量做++操作,并等待一小會時間嘗試發送數據。看圖:

net/core/stream.c里的sk_stream_wait_connect函數做了如下操作:

sk->sk_write_pending遞增,并且等待socket連接到達ESTABLISHED狀態后發出數據。這就解釋清楚了。
Linux socket的默認工作方式是阻塞的,也就是說,客戶端的connect調用在默認情況下會阻塞,等待三次握手過程結束之后或者遇到錯誤才會返回。那么nc這種完全用阻塞套接字實現的且沒有對默認socket參數進行修改的命令行小程序會乖乖等待connect返回成功或者失敗才會發送數據的,這就是我們抓不到第三次握手的包帶有數據的原因。
那么設置非阻塞套接字,connect后立即send數據,連接過程不是瞬間連接成功的話,也許有機會看到第三次握手包帶數據。不過開源的網絡庫即便是非阻塞socket,也是監聽該套接字的可寫事件,再次確認連接成功才會寫數據。為了節省這點幾乎可以忽略不計的性能,真的不如安全可靠的代碼更有價值。
條件2:icsk->icsk_accept_queue.rskq_defer_accept != 0
這個條件好奇怪,defer_accept是個socket選項,用于推遲accept,實際上是當接收到第一個數據之后,才會創建連接。tcp_defer_accept這個選項一般是在服務端用的,會影響socket的SYN和ACCEPT隊列。默認不設置的話,三次握手完成,socket就進入accept隊列,應用層就感知到并ACCEPT相關的連接。當tcp_defer_accept設置后,三次握手完成了,socket也不進入ACCEPT隊列,而是直接留在SYN隊列(有長度限制,超過內核就拒絕新連接),直到數據真的發過來再放到ACCEPT隊列。設置了這個參數的服務端可以accept之后直接read,必然有數據,也節省一次系統調用。
SYN隊列保存SYN_RECV狀態的socket,長度由net.ipv4.tcp_max_syn_backlog參數控制,accept隊列在listen調用時,backlog參數設置,內核硬限制由 net.core.somaxconn 限制,即實際的值由min(backlog,somaxconn) 來決定。
有意思的是如果客戶端先bind到一個端口和IP,然后setsockopt(TCP_DEFER_ACCEPT),然后connect服務器,這個時候就會出現rskq_defer_accept=1的情況,這時候內核會設置定時器等待數據一起在回復ACK包。我個人從未這么做過,難道只是為了減少一次ACK的空包發送來提高性能?哪位同學知道煩請告知,謝謝。
條件3:icsk->icsk_ack.pingpong != 0
pingpong這個屬性實際上也是一個套接字選項,用來表明當前鏈接是否為交互數據流,如其值為1,則表明為交互數據流,會使用延遲確認機制。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。