連接正常結束:四次揮手,好好告別
1)序號(sequence number):Seq序號,占32位,用來標識從TCP源端向目的端發送的字節流,發起方發送數據時對此進行標記。
2)確認號(acknowledgement number):Ack序號,占32位,只有ACK標志位為1時,確認序號字段才有效,Ack=Seq+1。
3)標志位(Flags):共6個,即URG、ACK、PSH、RST、SYN、FIN。具體含義如下:
ACK:確認序號有效。
RST:重置連接。
SYN:發起一個新連接。
FIN:釋放一個連接。
為何建立連接時一起傳輸,釋放連接時卻要分開傳輸?
建立連接時,被動方服務器端結束CLOSED階段進入“握手”階段并不需要任何準備,可以直接返回SYN和ACK報文,開始建立連接。釋放連接時,被動方服務器,突然收到主動方客戶端釋放連接的請求時并不能立即釋放連接,因為還有必要的數據需要處理,所以服務器先返回ACK確認收到報文,經過CLOSE-WAIT階段準備好釋放連接之后,才能返回FIN釋放連接報文。
為什么客戶端在TIME-WAIT階段要等2MSL?
為的是確認服務器端是否收到客戶端發出的ACK確認報文
當客戶端發出最后的ACK確認報文時,并不能確定服務器端能夠收到該段報文。所以客戶端在發送完ACK確認報文之后,會設置一個時長為2MSL的計時器。MSL指的是Maximum Segment Lifetime:一段TCP報文在傳輸過程中的最大生命周期。2MSL即是服務器端發出為FIN報文和客戶端發出的ACK確認報文所能保持有效的最大時長。
服務器端在1MSL內沒有收到客戶端發出的ACK確認報文,就會再次向客戶端發出FIN報文;
如果客戶端在2MSL內,再次收到了來自服務器端的FIN報文,說明服務器端由于各種原因沒有接收到客戶端發出的ACK確認報文。客戶端再次向服務器端發出ACK確認報文,計時器重置,重新開始2MSL的計時;否則客戶端在2MSL內沒有再次收到來自服務器端的FIN報文,說明服務器端正常接收了ACK確認報文,客戶端可以進入CLOSED階段,完成“四次揮手”。
所以,客戶端要經歷時長為2SML的TIME-WAIT階段;這也是為什么客戶端比服務器端晚進入CLOSED階段的原因。
這些東西畢竟都是停留在理論層面的,實際的場景可比這要錯綜復雜的多了。
故障模式
網絡中斷
如果網絡發生了中斷,那就不用提什么“主動關閉”,什么“FIN”包了。TCP程序也并不能感應到連接異常,除非路由器發出一條ICMP報文,說明目的網絡或主機不可達;或者說通過read或write調用才會返回UNreachable的錯誤。
可惜大多數時候并不是如此,在沒有 ICMP 報文的情況下,TCP 程序并不能理解感應到連接異常。如果程序是阻塞在 read 調用上,那么很不幸,程序無法從異常中恢復。
如果程序先調用了 write 操作發送了一段數據流,接下來阻塞在 read 調用上,結果會非常不同。Linux 系統的 TCP 協議棧會不斷嘗試將發送緩沖區的數據發送出去,大概在重傳 12 次、合計時間約為 9 分鐘之后,協議棧會標識該連接異常,這時,阻塞的 read 調用會返回一條 TIMEOUT 的錯誤信息。如果此時程序還執著地往這條連接寫數據,寫操作會立即失敗,返回一個 SIGPIPE 信號給應用程序。
而一旦返回了這種信號,進程就會被終止掉了。也就是我們常說的,程序崩了。
對端有 FIN 包發出
這種情況呢,是比較常見的了,至少在我這里是比較常見的,一般不會造成太惡劣的影響,除非在同一時間內有大批量的連接斷開,那會占用很多的資源的。
對端如果有 FIN 包發出,可能的場景是對端調用了 close 或 shutdown 顯式地關閉了連接,也可能是對端應用程序崩潰,操作系統內核代為清理所發出的。從應用程序角度上看,無法區分是哪種情形。
阻塞的 read 操作在完成正常接收的數據讀取之后,FIN 包會通過返回一個 EOF 來完成通知,此時,read 調用返回值為 0。這里強調一點,收到 FIN 包之后 read 操作不會立即返回。你可以這樣理解,收到 FIN 包相當于往接收緩沖區里放置了一個 EOF 符號,之前已經在接收緩沖區的有效數據不會受到影響。
服務器斷開
注意如果我們的速度不夠快,導致服務器端從睡眠中蘇醒,并成功將報文發送出來后,客戶端會正常顯示,此時我們停留,等待標準輸入。如果不繼續通過 read 或 write 操作對套接字進行讀寫,是無法感知服務器端已經關閉套接字這個事實的。