Python常見面試題:TCP 協議中的三次握手與四次揮手相關概念詳解

今天來聊聊Python常見面試題中面試頻率特別高的一個題目:TCP 協議中的三次握手與四次揮手。

涉及到的知識點有:

1、TCP、UDP 協議的區別

2、TCP 頭部結構

3、三次握手與四次揮手過程詳解

4、什麼是 TIME_WATI 狀態

一、TCP、UDP 協議的區別

在介紹這兩者的區別之前,我們要需要了解一個概念:TCP/IP 協議族。定義如下:

目前 Internet(因特網)使用的主流協議族是 TCP/IP 協議族,它是一個分層、多協議的通信體系 《Linux高性能編程》

提取關鍵詞:分層、多協議和通信。也就是說,它有多個層次,每個層次有不同的協議,這些層次之間通過協議相互協作,最終達到網絡通信的目的。

說到分層,應該不會很陌生,TCP/IP 協議族是一個四層協議系統,自底而上分別是:數據鏈接層、網絡層、傳輸層、應用層。我們這裏要說到的 TCP 和 UDP 協議屬於傳輸層。(各層的作用及相關協議這裏暫時先不做介紹)

下面我們回到標題:TCP、UDP 協議的區別,總結起來這個問題的答案要點如下:
1、首頁它們倆都是傳輸層的協議,而所謂“傳輸層”,它是爲兩臺主機提供端到端的通信,即從 A <-> B 。

2、TCP 協議可靠,UDP 協議不可靠。可靠即指數據由 A 發送到 B,是否能確保數據真的有送達到 B。TCP 協議使用 超時重傳、數據確認等方式來確保數據包被正確地發送至目的端,而 UDP 協議無法保證數據從發送端正確傳送到目的端,如果數據在傳輸過程中丟失、或者目的端通過數據檢驗發現數據錯誤,則 UDP 協議只是簡單地通知應用程序發送失敗,對於 TCP 協議擁有的超時重傳、數據確認等需要應用程序自己來處理這些邏輯。

3、TCP 是面向連接的,UDP 是無連接的。這也比較好理解,因爲 TCP 連接才需要“三次握手,四次揮手”。

4、TCP 服務是基於流的,而 UDP 是基於數據報的,基於流的數據沒有邊界(長度)限制,而基於數據報的服務,每個 UDP 數據報都有一個長度,接收端必須以該長度爲最小單位將其所有內容一次性讀出。

5、當發送方多次執行寫操作時,TCP 模塊會先將這些數據放入 TCP 發送緩衝區中,當 TCP 模塊真正開始發送數據時,發送緩衝區中這些等待發送的數據可能被封裝成一個或多個 TCP 報文段發出,因此,TCP 模塊發出的 TCP 報文段的個數與應用程序執行的寫操作次數是沒有固定數量關係的。同樣,當接收端收到一個或多個 TCP 報文段後,TCP 模塊將這些數據按照序號(序號說明見下面 的 TCP 頭部結構)依次放入 TCP 接收緩衝區中,並通知應用程序讀取數據。接收端可選擇一次或者分多次將數據從緩衝區中讀出(這取決於用戶指定的應用程序讀緩衝區的大小)。因此,接收端讀取數據的次數與發送端發出多少個報文段個數也沒有固定的數量關係。總結來說,即對於 TCP 連接,發送端執行的寫操作次數與接收端執行的讀操作次數之間沒有任何數據關係,這也是基於流服務的特點。而對於 UDP 服務,發送端每執行一次寫操作,就會將其封裝成一個 UDP 數據報併發送之,同時接收端必須根據發送的進行讀,否則就會丟包。因此,對於 UDP 連接,發送端寫的次數據與讀的次數是一致的,這也是基於數據報的服務的特點。

6、TCP 的連接是一對一的,所以如果是基於廣播或者多播的的應用程序不能使用 TCP,而 UDP 則非常適合廣播和多播。

總結一句定義:

TCP 協議(Transmission Control Protocal,傳輸控制協議)爲應用層提供可靠的、面向連接的、基於流的服務。而 UDP 協議(User Datagram Protocal,用戶數據報協議)則與 TCP 協議完全相反,它爲應用層提供不可靠、無連接和基於數據報的服務。

二、TCP 頭部結構

TCP 報文結構分爲頭部部分和數據部分,爲什麼需要了解 TCP 頭部結構,因爲在後面“三次握手與四次揮手”裏會用到頭部結構裏的標誌位。簡單瞭解下對理解後面的過程有一定的好處。

下面一一說明每個的作用:

16位源端口號與目的端口號,這個比較好理解,就不過多解釋。

32位序號:在建立連接(或者關閉)的過程,這個序號是用來做佔位,當 A 發送連接請求到 B,這個時候會帶上一個序號(隨機值,稱爲 ISN),而 B 確認連接後,會把這個序號 +1 返回,同時帶上自己的充號。當建立連接後,該序號爲生成的隨機值 ISN 加上該段報文段所攜帶的數據的第一個字節在整個字節流中的偏移量。比如,某個 TCP 報文段發送的數據是字節流中的第 100 ~ 200 字節,那該序號爲 ISN + 100。所以總結起來說明建立連接(或者關閉)時,序號的作用是爲了佔位,而連接後,是爲了標記當前數據流的第一個字節。

4位頭部長度:標識 TCP 頭部有多少個 32 bit 字,因爲是 4位,即 TCP 頭部最大能表示 15,即最長是 60 字節。即它是用來記錄頭部的最大長度。

6位標誌位,包括:

URG 標誌:表示緊急指針是否有效。

ACK 標誌:確認標誌。通常稱攜帶 ACK 標誌的 TCP 報文段爲確認報文段。

PSH 標誌:提示接收端應該程序應該立即從 TCP 接收緩衝區中讀走數據,爲接收後續數據騰出空間(如果不讀走,數據就會一直在緩衝區內)。

RST 標誌:表示要求對方重新建立連接。通常稱攜帶 RST 標誌的 TCP 報文段爲復位報文段。

SYN 標誌:表示請求建立一個連接。通常稱攜帶 SYN 標誌的 TCP 報文段稱爲同步報文段。

FIN 標誌:關閉標誌,通常稱攜帶 FIN 標誌的 TCP 報文段爲結束報文段。

這些標誌位說明了當前請求的目的,即要幹什麼。

16 位窗口大小:表示當前 TCP 接收緩衝區還能容納多少字節的數據,這樣發送方就可以控制發送數據的速度,它是 TCP 流量控制的一個手段。

16 位校驗和:驗證數據是否損壞,通過 CRC 算法檢驗。這個校驗不僅包括 TCP 頭部,也包括數據部分。

16 位緊急指針:正的偏移量,它和序號字段的值相加表示最後一個緊急數據的下一字節的序號。TCP 的緊急指針是發送端向接收端發送緊急數據的方法。

TCP 頭部選項:可變長的可選信息,這部分最多包含 40 字節,因爲 TCP 頭部最長是 60 字節,所以固定部分佔 20 字節。這裏不做詳細介紹,可以參考《Linux高性能編程》3.2.2

三. 三次握手與四次揮手過程詳解

畫圖

先來解釋三次握手過程:

1、發送端發送連接請求,6位標誌爲 SYN,同時帶上自己的序號(此時由於不傳輸數據,所以不表示字節的偏移量,只是佔位),比如是 223。

2、接收端接到請求,表示同意連接,發送同意響應,帶上 SYN + ACK 標誌位,同時將確認序號爲 224(發送端序號加1),並帶上自己的序號(此時同樣由於不傳輸數據,所以不表示字節的偏移量,只是佔位),比如是 521。

3、發送端接收到確認信息,再發回給接收端,表示我已接受到你的確認信息,此時標誌仍爲 ACK,確認序號爲 522。

涉及到的問題:爲什麼是三次握手,而不是四次或者兩次?

首先解釋爲什麼不是四次。四次的過程是這樣的:

發送方:我要連你了。

接收方:好的。

接收方:我準備好了,你連吧。

發送方:好的。

顯然接收方準備好連接並同意連接是可以合併的,這樣可以提高連接的效率。

再來,我們解釋爲什麼不是兩次。其實也比較好理解,我們知道 TCP 是全雙工通信的,同時也是可靠的,連接和關閉都是兩邊都要執行纔算真正的完成,同時還需要確保兩端都已經執行了連接或者關閉。如果只有兩次,過程是這樣的:

發送方:我要連你了。

接收方:好的。

很明顯,接收方並不知道也不能保證發送方一定接收到 “好的” 這條信息,一旦接收方真的沒有收到這條信息,就會出現接收收“單方面連接”的情況,這個時候發送方就會一直重試發送連接請求,直到真正收到 “好的” 這條信息之後纔算連接完成。而對於三次,如果發送方沒有等待到你回覆確認,它是不會真正處於連接狀態的,它會重試確認請求。

接着我們來看看四次揮手過程:

1、發送方發送關閉請求,標誌位爲:FIN,同時也會帶上自己的序號(此時同樣由於不傳輸數據,所以不表示字節的偏移量,只是佔位)。

2、接收方接到請求後,回覆確認:ACK,同時確認序號爲請求序號加1。

3、接收方也決定關閉連接,發送關閉通知,標誌位爲 FIN,同時還會帶上第2步中的確認信息,即 ACK,以及確認序號和自己的序號。

4、發送方回覆確認信息:ACK,接收方序號加1。

涉及到的問題:爲什麼需要四次握手,不是三次?

三次的過程是這樣的:

發送方:我不再給你發送數據了。

接收方:好的,我也不給你發了。

發送方:好的,拜拜。

這是因爲當接收方收到關閉請求後,它能立馬響應的就是確認關閉,它這裏確認的是接收方的關閉,即發送方不再發數據給接收方了,但他還是可以接收接收方發給他的數據。而接收方是否需要關閉“發送數據給發送方”這條通道,取決於操作系統。操作系統也有可能 sleep 個幾秒再關閉,如果合併成三次,就可能造成接收方不能及時收到確認請求,可能造成超時重試等情況。因此需要四次。

四、什麼是 TIME_WAIT 狀態

首先,我們來看一段代碼(說了這麼多理論,終於要看點代碼了)。這裏舉一個 python 簡單的使用 socket 進行 tcp 通信的示例:

服務端:

socket_server_test.py
# -*- coding: utf-8 -*-
"""
@Time : 2019/6/26 下午4:58
@Author : Demon
@File : socket_server_test.py
@Desc : 
"""
import socket
HOST = '127.0.0.1' # 標準的迴環地址 (localhost)
PORT = 9999 # 監聽的端口 (非系統級的端口: 大於 1023)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 第三個參數,如果爲0,也不可複用
# 第三個如果爲1,可以複用
# s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
 print('Connected by', addr)
 while True:
 data = conn.recv(1024)
 print("data:", data)
 if data:
 print("close")
 s.close()
 break
 conn.sendall(data)

客戶端:

# -*- coding: utf-8 -*-
"""
@Time : 2019/6/26 下午4:55
@Author : yrr
@File : socket_client_test.py
@Desc : 測試 self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
"""
import socket
HOST = '127.0.0.1' # 服務器的主機名或者 IP 地址
PORT = 9999 # 服務器使用的端口
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall(b'Hello, world')
data = s.recv(1024)
print('Received', repr(data))

執行效果:

我們會發現,當我們服務端主動關閉時,如果我們再次運行這個程序,會報錯誤說端口仍然被佔用。這就很奇怪了,明明已經關閉了連接,爲什麼還會佔用着端口呢?我們使用 netstat -an|grep 9999 命令查看,發現當前這個連接處於 TIME_WAIT 狀態。

我們來說下 TIME_WAIT 狀態。即當一方斷開連接後,它並沒有直接進入 CLOSED 狀態,而是轉移到 TIME_WAIT 狀態,在這個狀態,需要等待 2MSL(Maximum Segment Life,報文段最大生存時間)的時間,才能完全關閉。

涉及問題:

1、爲什麼需要有 TIME_WAIT 狀態存在?

簡單來說有兩點原因如下:

a. 當最後發送方發出確認信息後,仍然不能保證接收方能收到信息,萬一沒收到,那接收方就會重試,而此時發送方已經真正關閉了,就接受不到請求了。

b. 如果發送方在發出確認信息後就關閉了,在接收方接到確認信息的過程中,發送方是有可能再次發出連接請求的,那這個時候就亂套了。剛連接完,又收到確認關閉的信息。

2、爲什麼時長是 2MSL 呢?

這個其實也比較好理解,所以我發送確認信息,到達最長時間是 MSL,而你如果沒接受到,再重試,時間最長也是 MSL,那我等 2MSL,如果還沒收到請求,證明你真的已經正常收到了。

正因爲我們有這個 TIME_WAIT 狀態,所以通常我們說是客戶端先關閉,一般不會讓服務器端先關閉。那如何避免出現關閉後端口被佔用的情況(即上面代碼示例問題怎麼解決)呢?很簡單,加一行代碼即可實現:

# socket.SO_REUSEADDR 表示 close 後端口可複用
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

Python常見面試題中這個三次握手與四次揮手的出題率還是比較高的,包含的知識點也比較多,大家慢慢消化!有哪裏不清楚的地方,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章