網絡協議
在計算機誕生以來,從最原始的單機模式到現在多台計算機協同工作,形成計算機網絡,從前很難想象的信息共享、多機合作、大規模計算在今天也早已成了現實。在早期,計算機網絡需要解決的痛點,就是怎樣才能無障礙地發送和接受數據。而發送和接受數據的過程中,需要相關的協議來支撐,下面介紹下平時接觸最多的協議TCP/IP
協議。
TCP和IP
TCP
(Transmission Control Protocol)的中文名稱為傳輸控制協議,IP
(Internet Protocol)的中文名稱為互聯網互聯協議,除此之外,大家常見的還有HTTP
、HTTPS
、FTP
、SMTP
、UDP
等等。因為TCP/IP
是當前最為流行的網絡傳輸協議框架,所以我們也將TCP/IP
稱為協議族。
TCP
TCP
的分層框架圖如下圖所示,為了表示網絡拓補圖在連接層面上的機器對等理念,采用了A機器和B機器的說法。
下面對圖中所涉及到層進行簡單的說明
- 鏈路層:單個0、1是沒有意義的,鏈路層是以字節為單位把0與1進行分組,定義數據幀,寫入源和目標機器的物理地址、數據、校驗位來傳輸數據。鏈表層報文結構如下:
MAC地址長6個字節共48位,通常使用十六進制數表示,我們可以在命令行窗口中輸入ifconfig -a
指令即可看到MAC地址。
-
網絡層:根據
IP
定義網絡地址,區分網段。子網內根據地址解析協議(ARP)進行MAC尋址,子網外進行路由轉發數據包。這個數據包也就是IP數據包
。 -
傳輸層:數據包通過網絡層發送到目標計算機后,應用程序在傳輸層定義邏輯端口,確認身份后,將數據包交給應用程序,實現端口到端口間通信。最典型的傳輸層協議是
UDP
和TCP
。UDP
只是在IP
數據包上增加端口等部分信息,是面向無連接的,是屬於不可靠的傳輸協議,常用於視頻通信、電話會議等等,因為這些應用場景就算少/丟一兩幀數據影響也不會很大。與之相反,TCP
是面向連接
的,所謂的面向連接,就是一種端到端之間通過不斷地失敗重試機制建立的可靠數據傳輸方式
,如同一條固定的信息通道承載着數據的可靠傳輸。 -
應用層:傳輸層的數據到達應用程序時,以某種統一規定的協議格式解讀數據。比如,E-mail在每個公司的程序界面、操作、管理方式都不一樣,但是都能讀取到郵件的信息,這是因為郵件遵守了SMTP協議,該協議如同傳統的書信格式,按規定填寫郵政編碼以及收件人信息。
小結
程序在發送消息時,應用層按既定的協議打包數據,隨后由傳輸層加上雙方的端口號,由網絡層加上雙方的IP
地址,再由鏈路層加上雙方的MAC地址,並將數據拆分成數據幀,經過多個路由器和網關后,到達目標機器。簡而言之,就是按 “ 端口->IP地址->MAC地址 ”這樣的路徑進行數據的封裝和發送,解包的時候反過來操作即可。
IP
IP
是面向無連接、無狀態的,沒有額外的機制保證發送的包是否有序到達。IP
的地址格式,保證了每個計算機分配的詳細地址唯一。那么,既然鏈路層可以通過唯一的MAC
地址找到機器,為什么還需要通過唯一的IP
地址來標識呢?這是因為,在互聯網區域內,我們很難通過廣播的形式,從上千萬的計算機當中找到目標MAC
地址的計算機而不超時,在數據投遞時就需要對地址進行分層管理。
IP
地址屬於網絡層,主要功能在WLAN
內進行路由尋址,從而選擇最佳路由。IP
報文格式如下圖所示,共32位4個字節,通常用十進制數來表示。IP
地址的掩碼0xffffff00
表示255.255.255.0,掩碼相同,則在同一子網內。IP
協議在IP
報頭中記錄源IP
和目標IP
地址。
圖中標橙色的部分就是數據包的生存時間,即TTL
(Time To Live),該字段表示IP
報文被路由器丟棄之前可經過的最多路由總數。TTL
初始值由源主機設置后,數據包在傳輸過程中每經過一個路由器TTL
值則減1,當該字段為0時,數據包就會被丟棄,並發送ICMP
報文通知源主機,以防止源主機無休止地發送報文。擴展下,ICMP
它是檢測傳輸網絡是否順暢、主機是否可達、路由是否可用等網絡運行狀態的協議。我們經常使用的ping
,tracert
命令就是基於ICMP
來檢測網絡健康狀態的。圖中,TTL
右側是掛載協議標識,表示的是IP
數據包中放置的子數據包協議類型。6代表TCP
,17代表UDP
等。
IP
報文在互聯網上傳輸時,會經歷多個物理網絡,最終才能從源主機到達目標主機。舉個栗子,我們在手機上給某個PC客戶端發送一條消息,首先要經過無線網的IEEE 802.1x
認證,轉到光纖通信上,然后進入內部企業網802.3
,並最終到達目標PC。由於不同硬件之間,對於數據幀的最大長度都有着不同的限制,這個最大長度被稱為最大傳輸單元
,即MTU
。在不同的物理網之間,可能會對IP
報文進行分片,這個工作往往是由路由器去完成。
TCP建立連接
傳輸控制協議(Transmission Control Protocol,TCP),是一種面向連接
、確保數據在端到端間可靠傳輸
的協議。面向連接
是指在發送數據前,需要先建立一條虛擬的鏈路,然后讓數據在這條鏈路上“流動”完成傳輸。為了確保數據的可靠傳輸,不僅需要對發出的每一個字節進行編號確認,校驗每一個數據包的有效性,在出現超時
情況時進行重傳
,還需要通過實現滑動窗口
和堵塞控制
等機制,避免網絡狀況惡化而最終影響數據傳輸的極端情形。每個TCP
數據包是封裝IP
包中,每一個IP
頭的后面緊接着的是TCP
頭,TCP報文格式如下圖所示
協議第一行的兩個端口號各站兩個字節,分別表示了源機器和目標機器的端口號。這兩個端口號與IP
報頭中的源IP
地址和目標IP
地址所組成的四元組可唯一標識一條TCP
連接。由於TCP
是面向連接的,因此有服務端和客戶端之分。需要服務端先在相應的端口上進行監聽,准備好接受客戶端發起的建立連接請求。當客戶端發起第一次請求連接的TCP
包時,目標機器端口就是服務端所監聽的端口號。常見的端口號如HTTP
服務的80
端口、HTTPS
服務的443
端口、SSH
服務的22
端口等。可通過輸入netstat
命令,可以得到當前機器活躍的連接信息。
協議第二行和第三行是序列號,各占4個字節。前者
是指所發送數據包
中數據部分第一字節的序號,后者
是指期望收到來自對方的下一個數據包
中數據部分第一個字節的序號。
由於TCP
報頭中存在一些擴展字段,所以需要通過長度為4個bit的頭部長度字段表示TCP
報頭的大小,這樣接收方才能准確地計算出包中數據部分的開始位置。
TCP
的FLAG由6個bit組成,分別有SYN
、ACK
、FIN
、URG、PSH、RST,都以置1表示有效。我們主要介紹下SYN
、ACK
、FIN
。
- SYN:全稱Synchronize Sequence Number,用作建立連接時的同步信號
- ACK:全稱Acknowledgement,用於對收到的數據進行確認,所確認的數據由確認序列號表示
- FIN:全稱Finish,表示后面沒有數據需要發送,通常意味着所建立的連接需要關閉了
接下來重點介紹TCP中連接建立的原理。通過圖文的形式展示正常網絡通信中,通過三次握手建立連接的過程。本次依然還是以A機器
和B機器
舉例,因為發起請求的一端不一定是客戶端
,也有可能是服務端
向另外一台服務器發送TCP
請求。前者需要在后者發起連接建立請求時先打開某個端口等待數據傳輸,否則將無法正常建立連接。
三次握手指的是建立連接的三個步驟:
- A機器發出一個數據包並將
SYN
置1,表示希望建立連接。圖中我們假設A機器發送的數據包(seq)的序列號為x
- B機器收到A機器發過來的數據包后,通過
SYN
得知這是一個建立連接的請求,於是發送一個響應包並將SYN
和ACK
標志都置1。圖中我們假設B機器發送的數據包(seq)的序列號為y
,而確認序列號必須是x+1
,表示收到了A機器發過來的SYN
。在TCP
中,SYN
被當作數據部分的一個字節。 - A機器收到B機器的響應包后需進行確認,確認包中將
ACK
置1並將確認序列號設置為y+1
,表示收到了來自B機器的SYN
。
圖中展示就是TCP連接時候的三次握手,三次握手兩個主要目的:信息對等
,防止超時
。從信息對等角度來看,如下表所示,雙方只有確定4類信息,才能建立連接。在第2次握手后,從B機器視角看還有兩個紅色的NO
信息無法確認。在第3次握手后,B機器才能確認自己的發報能力和對方的收報能力是正常的。
連接三次握手也是防止出現請求超時導致臟連接
。TTL
(Time To Live)網絡報文的生存時間往往都會超過TCP請求超時時間,如果兩次握手就可以創建連接,傳輸完數據並釋放連接后,第一個超時的連接請求才到達B機器,B機器會認為是A機器創建新連接的請求,然后確認同意創建連接。因為A機器的狀態不是SYN_SENT
,所以直接丟棄了B機器的確認數據,以至於最后只是B機器單方面創建連接完畢,具體的流程可參考下圖。
但如果是三次握手,則B機器收到連接請求后,也會向A機器確認同意后才會創建連接,但因為A機器不是SYN_SENT
狀態,所以會直接丟棄,B機器由於長時間沒有收到確認信息,最終超時導致連接創建失敗,因此不會出現臟連接。
擴展
:從編程的角度,TCP連接的建立是通過文件描述符(File Descriptor,fd)完成的。通過創建套接字獲得一個fd,然后服務端和客戶端需要基於所獲得的fd調用不同的函數分別進入監聽狀態和發起連接請求。由於fd的數量將決定服務端進程所能建立連接的數量,對於大規模分布式服務來說,當fd不足時將會出現“open too many files”錯誤而導致無法建立更多的連接。為此,需要注意調整服務端進程和操作系統所支持的最大文件句柄數。通過使用ulimit -n
命令來查看單個進程可以打開文件句柄的數量。如果想要查看當前系統各個進程產生了多少個句柄,可輸入以下命令:
lsof -n | awk '{print $2}' | sort|uniq -c |sort -nr|more
TCP斷開連接
TCP是全雙工通信,雙方都能作為數據的發送方和接受方,但TCP連接也會有斷開的時候。建立連接只有三次,而斷開連接需要四次。接下來,還是通過以下圖文的方式展示連接斷開的步驟。如圖所示,主要分為四個步驟:
- A機器想要關閉連接,則待本方數據發送完畢后,傳遞
FIN
信號給B機器。 - B機器應答給
ACK
信號,告訴A機器可以斷開,但是需要等B機器處理完數據,再主動給A機器發送FIN
信號。這時,A機器處於半關閉狀態(FIN_WAIT_2),無法再發送新的數據。 - B機器做好連接關閉前的准備工作后,發送
FIN
給A機器,此時B機器也進入半關閉狀態(CLOSE_WAIT)。 - A機器發送針對B機器FIN的
ACK
后,進入TIME_WAIT
狀態,經過2MSL
(Maximum Segment Lifetime)后,沒有收到B機器傳來的報文,則確定B機器已經收到A機器最后發送的ACK
指令,此時TCP連接正式釋放。一般來說,MSL
大於TTL
衰減至0的時間。在RFC793中規定MSL
為2分鍾。
圖中的紅色字體TIME_WAIT
,CLOSE_WAIT
分別表示主動關閉和被動關閉產生的階段性狀態,如果在線上服務器大量出現這兩種狀態,就會加重機器負載,也會影響有效連接的創建,因此需要進行有針對性的調優處理。
-
TIME_WAIT
:主動要求關閉的A機器表示收到了對方的FIN
報文,並發送出了ACK
報文,進入TIME_WAIT
狀態,等2MSL
后即可進入到CLOSED
狀態。如果FIN_WAIT_1
狀態下,同時收到帶FIN
標志和ACK
標志的報文時,可以直接進入TIME_WAIT
,無須經過FIN_WAIT_2
狀態。 -
CLOSE_WAIT
:被動要求關閉的機器收到對方請求關閉連接的FIN
報文,在第一次ACK
應答后,馬上進入CLOSE_WAIT
狀態。這種狀態其實表示在等待關閉,並且通知應用程序發送剩余數據,處理現場信息,關閉相關資源。
在TIME_WAIT
等待的2MSL
是報文在網絡上生存的最長時間,超過閾值報文則被丟棄。但是在當前的高速網絡中,2分鍾的等待時間會導致網絡資源的極大浪費,在高並發服務器上通常會使用更小的值。那既然TIME_WAIT
貌似是百害無一利的,為何不直接關閉,進入CLOSED
狀態呢?原因如下兩點:
- 確認被動關閉方能夠順利進入
CLOSED
狀態。如上圖所示,假如最后一個ACK
由於網絡原因導致無法到達B機器,處於LAST_ACK
的B機器通常“自信”地以為對方沒有收到自己的FIN
+ACK
報文,所以會重發。A機器收到第二次的FIN
+ACK
報文,會重發一次ACK
,並且重新計時。如果按上面問題所描述那樣,A機器收到B機器的FIN+ACK
報文后,發送一個ACK
給B機器,就“自私”地立馬進入CLOSED
狀態,最終可能會導致B機器無法確保收到最后的ACK
指令,也無法進入CLOSED狀態。 - 防止失效請求。這樣做是為了防止已失效連接的請求數據包與正常連接的請求數據包混淆而發生異常。
擴展
:TIME_WAIT是四次揮手斷開連接的尾聲,如果此狀態連接過多,則可以通過優化服務器參數得到解決。如果不是對方連接的異常,一般不會出現連接無法關閉的情況。但是CLOSED_WAIT過多很可能是程序自身的問題,比如在對方關閉連接后,程序沒有檢測到,或者忘記自己關閉連接。