1. 程式人生 > >關於 TCP 併發連線的幾個思考題與試驗GOOD

關於 TCP 併發連線的幾個思考題與試驗GOOD

陳碩 (giantchen AT gmail)

blog.csdn.net/Solstice

第一道初級題目是:

有一臺機器,它有一個 IP,上面運行了一個 TCP 服務程式,程式只偵聽一個埠,問:從理論上講(只考慮 TCP/IP 這一層面,不考慮IPv6)這個服務程式可以支援多少併發 TCP 連線?答 65536(埠總數) 上下的直接刷掉。

具體來說,這個問題等價於:有一個 TCP 服務程式的地址是 1.2.3.4:8765,問它從理論上能接受多少個併發連線?

第二道進階題目是:

一臺被測機器 A,功能同上,同一交換機上還接有一臺機器 B,如果允許 B 的程式直接收發乙太網 frame,問:讓 A 承擔 10 萬個併發 TCP 連線需要用多少 B 的資源?100萬個呢?

從討論的結果看,很多人做出了第一道題,而第二道題幾乎無人問津。

這裡先不公佈答案(第一題答案見文末),讓我們繼續思考一個本質的問題:一個 TCP 連線要佔用多少系統資源。

在現在的 Linux 作業系統上,如果用 socket()/connect() 或 accept() 來建立 TCP 連線,那麼每個連線至少要佔用一個檔案描述符(file descriptor)。為什麼說“至少”?因為檔案描述符可以複製,比如 dup();也可以被繼承,比如 fork();這樣可能出現系統裡邊同一個 TCP 連線有多個檔案描述符與之對應。據此,很多人給出的第一題答案是:併發連線數受限於系統能同時開啟的檔案數目的最大值。這個答案在實踐中是正確的,卻不符合原題意。

如果拋開作業系統層面,只考慮 TCP/IP 層面,建立一個 TCP 連線有哪些開銷?理論上最小的開銷是多少?考慮兩個場景:

1. 假設有一個 TCP 服務程式,向這個程式成功發起連線需要做哪些事情?換句話說,如何才能讓這個 TCP 服務程式認為有客戶連線到了它(讓它的 accept() 呼叫正常返回)?

2. 假設有一個 TCP 客戶端程式,讓這個程式成功建立到伺服器的連線需要做哪些事情?換句話說,如何才能讓這個 TCP 客戶端程式認為它自己已經連線到伺服器了(讓它的 connect() 呼叫正常返回)?

以上這兩個問題問的不是如何程式設計,如何呼叫 Sockets API,而是問如何讓作業系統的 TCP/IP 協議棧認為任務已經成功完成,連線已經成功建立。

學過 TCP/IP 協議,理解三路握手的同學明白,TCP 連線是虛擬的連線,不是電路連線,維持 TCP 連線理論上不佔用網路資源(會佔用兩頭程式的系統資源)。只要連線的雙方認為 TCP 連線存在,並且可以互相傳送 IP packet,那麼 TCP 連線就一直存在。

對於問題 1,向一個 TCP 服務程式發起一個連線,客戶端(為明白起見,以下稱為 faketcp 客戶端)只需要做三件事情(三路握手):

1a. 向 TCP 服務程式發一個 IP packet,包含 SYN 的 TCP segment

1b. 等待對方返回一個包含 SYN 和 ACK 的 TCP segment

1c. 向對方傳送一個包含 ACK 的 segment

在做完這三件事情之後,TCP 伺服器程式會認為連線已建立。而做這三件事情並不佔用客戶端的資源(?),如果faketcp 客戶端程式可以繞開作業系統的 TCP/IP 協議棧,自己直接傳送並接收 IP packet 或 Ethernet frame 的話。換句話說,faketcp 客戶端可以一直重複做這三件事件,每次用一個不同的 IP:PORT,在服務端建立不計其數的 TCP 連線,而 faketcp 客戶端自己毫髮無損。很快我們將看到如何用程式來實現這一點。

對於問題 2,為了讓一個 TCP 客戶端程式認為連線已建立,faketcp 服務端只需要做兩件事情:

2a. 等待客戶端發來的 SYN TCP segment

2b. 傳送一個包含 SYN 和 ACK 的 TCP segment

2c. 忽視對方發來的包含 ACK 的 segment

在做完這兩件事情(收一個 SYN、發一個 SYN+ACK)之後,TCP 客戶端程式會認為連線已建立。而做這三件事情並不佔用 faketcp 服務端的資源(?)換句話說,faketcp 服務端可以一直重複做這兩件事件,接受不計其數的 TCP 連線,而 faketcp 服務端自己毫髮無損。很快我們將看到如何用程式來實現這一點。

基於對以上兩個問題的分析,說明單獨談論“TCP 併發連線數”是沒有意義的,因為連線數基本上是要多少有多少。更有意義的效能指標或許是:“每秒鐘收發多少條訊息”、“每秒鐘收發多少位元組的資料”、“支援多少個活動的併發客戶”等等。

faketcp 的程式實現

為了驗證我上面的說法,我寫了幾個小程式來實現 faketcp,這幾個程式可以發起或接受不計其數的 TCP 併發連線,並且不消耗作業系統資源,連動態記憶體分配都不會用到。

我家裡有一臺執行 Ubuntu Linux 10.04 的 PC 機,hostname 是 atom,所有的試驗都在這上面進行。

家裡試驗環境的網路配置是:

net

陳碩在《談一談網路程式設計學習經驗》中曾提到“可以用 TUN/TAP 裝置在使用者態實現一個能與本機點對點通訊的 TCP/IP 協議棧”,這次的試驗正好可以用上這個辦法。

試驗的網路配置是:

tun

具體做法是:在 atom 上通過開啟 /dev/net/tun 裝置來建立一個 tun0 虛擬網絡卡,然後把這個網絡卡的地址設為 192.168.0.1/24,這樣 faketcp 程式就扮演了 192.168.0.0/24 這個網段上的所有機器。atom 發給 192.168.0.2~192.168.0.254 的 IP packet 都會發給 faketcp 程式,faketcp 程式可以模擬其中任何一個 IP 給 atom 發 IP packet。

程式分成幾步來實現。

第一步:實現 icmp echo 協議,這樣就能 ping 通 faketcp 了。

執行方法,開啟 3 個命令列視窗:

1. 在第 1 個視窗執行 sudo ./icmpecho ,程式顯示

allocted tunnel interface tun0

2. 在第 2 個視窗執行

$ sudo ifconfig tun0 192.168.0.1/24

$ sudo tcpdump -i tun0

3. 在第 3 個視窗執行

$ ping 192.168.0.2

$ ping 192.168.0.3

$ ping 192.168.0.234

發現每個 192.168.0.X 的 IP 都能 ping 通。

第二步:實現拒絕 TCP 連線的功能,即在收到 SYN TCP segment 的時候傳送 RST segment。

執行方法,開啟 3 個命令列視窗,頭兩個視窗的操作與前面相同,執行的 faketcp 程式是 ./rejectall

3. 在第 3 個視窗執行

$ nc 192.168.0.2 2000

$ nc 192.168.0.2 3333

$ nc 192.168.0.7 5555

發現向其中任意一個 IP 發起的 TCP 連線都被拒接了。

第三步:實現接受 TCP 連線的功能,即在收到SYN TCP segment 的時候發回 SYN+ACK。這個程式同時處理了連線斷開的情況,即在收到 FIN segment 的時候發回 FIN+ACK。

執行方法,開啟 3 個命令列視窗,步驟與前面相同,執行的 faketcp 程式是 ./acceptall。這次會發現 nc 能和 192.168.0.X 中的每一個 IP 每一個 PORT 都能連通。還可以在第 4 個視窗中執行 netstat –tpn ,以確認連線確實建立起來了。如果在 nc 中輸入資料,資料會堆積在作業系統中,表現為 netstat 顯示的傳送佇列(Send-Q)的長度增加。

第四步:在第三步接受 TCP 連線的基礎上,實現接收資料,即在收到包含 payload 資料 的 TCP segment 時發回 ACK。

執行方法,開啟 3 個命令列視窗,步驟與前面相同,執行的 faketcp 程式是 ./acceptall。這次會發現 nc 能和 192.168.0.X 中的每一個 IP 每一個 PORT 都能連通,資料也能發出去。還可以在第 4 個視窗中執行 netstat –tpn ,以確認連線確實建立起來了,並且傳送佇列的長度為 0。

這一步已經解決了前面的問題 2,扮演任意 TCP 服務端。

第五步:解決前面的問題 1,扮演客戶端向 atom 發起任意多的連線。

這一步的執行方法與前面不同,開啟 4 個命令列視窗。

1. 在第 1 個視窗執行 sudo ./connectmany 192.168.0.1 2007 1000 ,表示將向 192.168.0.1:2007 發起 1000 個併發連線。

程式顯示

allocted tunnel interface tun0
press enter key to start connecting 192.168.0.1:2007

2. 在第 2 個視窗執行

$ sudo ifconfig tun0 192.168.0.1/24

$ sudo tcpdump -i tun0

3. 在第 3 個視窗執行一個能接收併發 TCP 連線的服務程式,可以是 httpd,也可以是 muduo 的 echo 或 discard 示例,程式應 listen 2007 埠。

4. 回到第 1 個視窗中敲回車,然後在第 4 個視窗中用 netstat -tpn 來觀察併發連線。

有興趣的話,還可以繼續擴充套件,做更多的有關 TCP 的試驗,以進一步加深理解,驗證作業系統 TCP/IP 協議棧面對不同輸入的行為。甚至可以按我在《談一談網路程式設計學習經驗》中提議的那樣,實現完整的 TCP 狀態機,做出一個簡單的 mini tcp stack。

第一道題的答案:

在只考慮 IPv4 的情況下,併發數的理論上限是 2**48。考慮某些 IP 段被保留了,這個上界可適當縮小,但數量級不變。實際的限制是作業系統全域性檔案描述符的數量,以及記憶體大小。

一個 TCP 連線有兩個 end points,每個 end point 是 {ip, port},題目說其中一個 end point 已經固定,那麼留下一個 end point 的自由度,即 2 ** 48。客戶端 IP 的上限是 2**32 個,每個客戶端IP發起連線的上限是 2**16,乘到一起得理論上限。

即便客戶端使用 NAT,也不影響這個理論上限。(為什麼?)

在真實的 Linux 系統中,可以通過調整核心引數來支援上百萬併發連線,具體做法見:

(.完.)