理解了這些異常現象才敢說真正懂了TCP協議(下)

今天我們繼續介紹關於TCP異常情況的內容。本篇文章接著上一篇文章,前面分析了在連接過程中的各種異常,本篇文章重點介紹的是在數據傳輸過程中的各種異常,以及出現異常後的TCP連接的情況。為了便於大家理解本文,這裡我們將上一篇文章的前半部分內容拷貝到這裡,這部分內容主要介紹協議的內容。

下圖是網絡通信中常見的架構,也就是CS架構。其中程序包括兩部分,分別為客戶端(Client)和服務端(Server)。當然,實際的環境還要複雜的多,在客戶端和服務端之間可能有多種不同種類和數量的設備,這些設備都會增加網絡通信的複雜性。自然,也會增加程序開發容錯的複雜性。

理解了這些異常現象才敢說真正懂了TCP協議(下)

圖1 基本架構

TCP的基本流程

在分析異常情況之前,我們先回憶一下TCP協議的基本邏輯。在客戶端和服務端能夠收發數據之前首先必需建立連接。連接的建立在協議層面也是通過收發數據包完成,只不過在用戶層面就是客戶端調用了一個connect函數。連接的過程俗稱“三次握手”,具體流程如圖2所示。

理解了這些異常現象才敢說真正懂了TCP協議(下)

圖2 TCP的三次握手流程

TCP連接的斷開也是比較複雜的,需要經過所謂的“四次揮手”的流程。其原因是因為TCP是雙工通信,分別需要從客戶端和服務端2側斷開連接。

理解了這些異常現象才敢說真正懂了TCP協議(下)

圖3 TCP的四次揮手

另外一個比較重要的內容是TCP協議的狀態轉換,理解了這個內容,我們才能清楚出現各種異常情況下數據包的內容。

理解了這些異常現象才敢說真正懂了TCP協議(下)

圖4 TCP狀態轉換圖

本文只是簡單回憶一下TCP的基本流程,詳細的內容可以參考本號之前的文章《從TCP到Socket,徹底理解網絡編程是怎麼回事

異常情況分析

本文的分析假設連接已經建立,目前正在數據收發過程。這種情況下會出現各種異常,比如服務器宕機、進程crash或者進程被kill等等。下面我們分別介紹上述集中情況在TCP通信中的表現。

服務進程crash

服務進程crash恐怕是我們日常生成環境最長遇到的情況,沒有之一吧。那麼在這種情況下客戶端軟件是什麼反應?客戶端是否可以感知?

我們分別寫客戶端和服務端的程序,客戶端不斷的發送數據,服務端接收數據。異常的模擬很簡單,我們可以在服務端製造一個指針訪問異常。此時服務端的程序就會crash掉。然後我們觀察客戶端的表現。先上結果,客戶端的表現如下圖所示。

理解了這些異常現象才敢說真正懂了TCP協議(下)

可以看到客戶端被reset掉了。我們在結合通過wireshark抓獲的此時的數據報文內容,可以看到是一個RST報文。

理解了這些異常現象才敢說真正懂了TCP協議(下)

回憶一下什麼情況下服務端會發送RST報文。這種場景跟我們前文介紹的服務端沒有監聽的情況是類似的。由於服務端程序crash了,此時在操作系統中的套接字數據結構已經被釋放,因此在協議層收到數據包的時候無法找到對應的套接字進行處理,於是發送了一個RST報文。

手動殺死服務端應用

這也是線上比較常見的操作,當一個模塊上線時,ops同學總是會先把舊的進程殺死,然後再啟動新的進程。那麼在這個過程中TCP連接又會發生了什麼呢?是否會像上一種情況一樣被RST呢?同樣,我們先看一下結果,如下是客戶端的情況。

理解了這些異常現象才敢說真正懂了TCP協議(下)

從上面錯誤碼來看是管道破裂,其實也就是連接被中斷了。我們再看一下通過wireshark的抓包結果可以看出服務端發送了一個FIN報文,這個報文表示服務端發起了關閉的請求。而接下來的一個報文是客戶端對該請求的確認。

理解了這些異常現象才敢說真正懂了TCP協議(下)

所以,從上面客戶端的錯誤碼和報文情況我們可以知道,在kill進程時TCP協議是能夠感知到的,並且發送的FIN報文。

我們再進一步的思考一下,為什麼kill進程會有FIN呢?這個與前面crash的差異在哪?其實kill進程是通過shell想內核發送了SIGKILL或者SIGTERM,內核接收到該信號之後會進行相應的掃尾工作,因此可以看到服務端發送了FIN報文。

Server進程所在的主機關機

主機關機(這裡指手動關機)的情況與進程被kill是類似的。這時因為在系統關閉時,init進程會給所有進程發送SIGTERM信號,等待一段時間(5~20秒),然後再給所有仍在運行的進程發送SIGKILL信號。當服務器進程死掉時,會關閉所有文件描述符。帶來的影響和上面殺死server相同。

Server進程所在的主機宕機

這是我們線上另一種比較常見的狀況。即使宕機是一個小概率事件,線上幾千臺服務器動不動一兩臺掛掉也是常有的事。這裡掛掉其實包括2種情況,一種是內核panic,另外一種情況是出現了掉電。對於內核panic的情況不會像關機那樣會預先殺死上面的進程,而是突然性的。那麼此時我們的客戶端準備給服務器端發送一個請求,它由write寫入內核,由TCP作為一個報文發出,但因為主機已經掛掉,因此客戶端無法收到ACK。於是客戶端TCP持續重傳分節,試圖從服務器上接收一個ACK,然而服務器始終不能應答,重傳數次之後,大約幾分鐘才停止,之後返回一個ETIMEDOUT錯誤。在這種情況下,如果我們調用的是同步發送接口,則在發送緩衝區慢的情況下會阻塞在這裡,導致程序阻塞。

這個時間真的很長,對於某些應用這種長時間的卡頓是不能接受的。因此,需要一種手段處理這種情況,在套接字接口中可以通過SO_SNDTIMEO標記進行設置。但是有利也有弊,如果設置了該參數,可能會出現這的數據發送超時的情況,進而出現向服務端發送重複數據的情況,此時需要服務端做去重處理。

服務器進程所在的主機宕機後重啟

在客戶端發出請求前,服務器端主機經歷了宕機—重啟的過程。當客戶端TCP把分節發送到服務器端所在的主機,服務器端所在主機的TCP丟失了崩潰前所有連接信息,即TCP收到了一個根本不存在連接上(也就是我們前文介紹的查找不到socket數據結構)的報文,所以會響應一個RST分節。

至此,關於TCP協議中各種異常情況介紹完了,詳細瞭解這些內容後對後續線上問題的分析和解決會有很大的幫助。當然,也有可能還有其它本文沒有介紹到的異常情況,也歡迎大家留言交流。

相關推薦

推薦中...