日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

TCP全连接和半连接的问题探讨

發布時間:2023/11/27 生活经验 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 TCP全连接和半连接的问题探讨 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

個人博客: https://rebootcat.com/2020/11/14/tcp_accept/

從何說起

說起 tcp 的連接過程,想必 “3次握手4次揮手”是大家廣為熟知的知識,那么關于更細節更底層的連接過程也許就很少人能講清楚了。

所以本文會先簡單回顧一下 tcp 的 3次握手過程,然后重點聊一下 tcp accept 的過程,涉及到 tcp 半連接隊列、全連接隊列等的內容。

回顧一下

3 次握手

要了解 3 次握手的過程,可能需要先熟悉一下 tcp 協議的格式:

  • tcp segment 的頭部有兩個 2字節的字段 source portdest port,分別表示本機端口以及目標端口,在 tcp 傳輸層是沒有 IP 的概念的,那是 IP 層 的概念,IP 層協議會在 IP 協議的頭部加上 src ipdest ip
  • 4 個字節的 seq,表示序列號,tcp 是可靠連接,不會亂序;
  • 4 個字節的 ack,表示確認號,表示對接收到的上一個報文的確認,值為 seq + 1;
  • 幾個標志位:ACK,RST,SYN,FIN 這些是我們常用的,比較熟悉的。其中 ACK 簡寫為 “.”; RST 簡寫為 “R”; SYN 簡寫為 “S”; FIN 簡寫為 “F”;

注意: ack 和 ACK 是不一樣的意思,一個是確認號,一個是標志位

了解了 tcp 協議的頭部格式,那么再來講一下 3 次握手的過程:

  1. 客戶端對服務端發起建立連接的請求,發送一個 SYN 包(也就是 SYN 標志位設置為 1),同時隨機生成一個 seq 值 x,然后客戶端就處于 SYN_SENT 狀態;
  2. 服務端收到客戶端的連接請求,回復一個 SYN+ACK包(也就是設置 SYN 和 ACK 標志位為 1),同時隨機生成一個 seq 值 y,然后確認號 ack = x + 1,也就是 client 的 seq +1,服務端進入 SYN_RECV 階段;
  3. 客戶端收到服務端的 SYN+ACK 包,會回復一個 ACK 包(也就是設置 ACK 標志位為 1),設置 seq = x + 1,ack 等于 服務端的 seq +1,也就是 ack = y+1,然后連接建立成功;

tcpdump 抓包

開一個終端執行以下命令作為服務端:

# 服務端
$ nc -l 10000

然后打開新的終端用 tcpdump 抓包:

# -i 表示監聽所有網卡;# -t 表示不打印 timestamp;# -S 表示打印絕對的 seq 而不是相對的 seq number;# port 10000 表示對 10000 端口進行抓包$ tcpdump  -i any -t -S port 10000

然后再打開一個終端模擬客戶端:

$ nc 127.0.0.1 10000

觀察 tcpdump 的輸出如下:

IP Jia.22921 > 192.168.1.7.ndmp: Flags [S], seq 614247470, win 29200, options [mss 1460,sackOK,TS val 159627770 ecr 0], length 0
IP 192.168.1.7.ndmp > Jia.22921: Flags [S.], seq 1720434034, ack 614247471, win 65160, options [mss 1460,sackOK,TS val 3002840224 ecr 159627770], length 0
IP Jia.22921 > 192.168.1.7.ndmp: Flags [.], ack 1720434035, win 29200, options [nop,nop,TS val 159627770 ecr 3002840224], length 0

分析以下上面的結果可以看到:

  1. 第一個包 Flags [S] 表示 SYN 包,seq 為隨機值 614247470;

  2. 然后服務端回復了一個 Falgs [S.],也就是 SYN+ACK 包,同時設置 seq 為隨機值 1720434034,設置 ack 為 614247470 + 1 = 614247471;

  3. 客戶端收到之后,回復一個 Flags [.],也就是 ACK 包,同時設置 ack 為 1720434034 + 1 = 1720434035;

假如3次握手丟包了?

上面是正常情況的握手情況,假如握手過程中的任何一個包出現丟包呢會怎么樣?比如受到了攻擊,比如服務端宕機,服務端超時,客戶端掉線,網絡波動等。

所以接下來我們分析下 3 次握手過程中涉及到的連接隊列。

tcp 內核參數

backlog 參數

https://linux.die.net/man/3/listen

The backlog argument provides a hint to the implementation which the implementation shall use to limit the number of outstanding connections in the socket’s listen queue. Implementations may impose a limit on backlog and silently reduce the specified value. Normally, a larger backlog argument value shall result in a larger or equal length of the listen queue. Implementations shall support values of backlog up to SOMAXCONN, defined in <sys/socket.h>.

int listen(int socket, int backlog);

backlog 參數是用來限制 tcp listen queue 的大小的,真實的 listen queue 大小其實也是跟內核參數 somaxconn 有關系,somaxconn 是內核用來限制同一個端口上的連接隊列長度。

全連接隊列

完成 3 次握手的連接,也就是服務端收到了客戶端發送的最后一個 ACK 報文后,這個連接會被放到這個端口的全連接隊列里,然后等待應用程序來處理,對于 epoll 來說就是內核觸發 EPOLLIN 事件,然后應用層使用 epoll_wait 來處理 accept 事件,為連接分配創建 socket 結構,分配 file descriptor 等;

那么假如應用層沒有來處理這些就緒的連接呢?那么這個全連接隊列有可能就滿了,導致后續的連接被丟棄,發生全連接隊列溢出,丟棄這個連接,對客戶端來說就無法成功建立連接。

所以為了性能的考慮,我們有必要盡可能的把這個隊列的大小調大一點。

查看全連接隊列大小

可以通過一下命令來查看當前端口的全連接隊列大小:

$ ss -antl
State   Recv-Q   Send-Q     Local Address:Port      Peer Address:Port  Process            
LISTEN  0        5           192.168.1.7:10000          0.0.0.0:*     

在 ss 輸出中:

LISTEN 狀態:Recv-Q 表示當前 listen backlog 隊列中的連接數目(等待用戶調用 accept() 獲取的、已完成 3 次握手的 socket 連接數量),而 Send-Q 表示了 listen socket 最大能容納的 backlog。

非 LISTEN 狀態:Recv-Q 表示了 receive queue 中存在的字節數目;Send-Q 表示 send queue 中存在的字節數;

壓測觀察全連接隊列溢出

接下來我們實際測試一下,使用項目:mux。

我們先修改一下 backlog 參數為 5:

# 把backlog 調小一點listen(listenfd, 5);

根據編譯文檔,編譯后得到兩個二進制:

$ ls
bench_server   bench_client_accept
  • bench_server 用來作為服務端,底層使用 epoll 實現
  • bench_client_accept 作為壓測客戶端,并發創建大量連接,這里只會與服務端建立連接,不會發送其他任何消息(當然可以用其他的壓測工具)

選擇兩臺機器進行測試,192.168.1.7 作為服務端, 192.168.1.4 作為壓測客戶端,開始壓測前,可能需要設置一下:

$ ulimit -n 65535
  1. 啟動服務端
# 192.168.1.7 作為服務端,監聽 10000 端口$ ./bench_server 192.168.1.7 10000

注意到上圖執行 ss -antl 看到 10000 端口的 listen queue size 為 5,這里是故意調小一點,為了驗證全連接隊列溢出的場景

  1. 先觀察一下服務端全連接隊列的情況以及溢出的情況
$ ss -natl |grep 10000
LISTEN  0        5            192.168.1.7:10000          0.0.0.0:*              
$ netstat  -s |grep -i overflowed2283 times the listen queue of a socket overflowed

上述表明 10000 端口的 listen queue size 為 5,并且全連接隊列中沒有等待應用層處理的連接;

netstat -s |grep -i overflowed 表示全連接隊列溢出的情況,2683 是一個累加值。

  1. 啟動 tcpdump 對客戶端行為抓包,分析 3次握手連接情況
# 運行在 client: 192.168.1.4 上$ tcpdump  -i any port 10000 and tcp -nn > tcpdump.log

3)啟動壓測客戶端

# 192.168.1.4 作為壓測客戶端
# 30000 表示連接數
# 100 表示 100 個并發線程
# 1 表示執行 1 輪$ ./bench_client_accept  192.168.1.7 10000 30000 100 1

壓測過程中,可以不斷執行命令觀察服務端全連接隊列溢出的情況,壓測完畢之后再觀察一下全連接隊列溢出的情況:

$ ss -natl |grep 10000
LISTEN  0        5            192.168.1.7:10000          0.0.0.0:*              
$ ss -natl |grep 10000
LISTEN  5        5            192.168.1.7:10000          0.0.0.0:*              
$ ss -natl |grep 10000
LISTEN  0        5            192.168.1.7:10000          0.0.0.0:*                          
$ ss -natl |grep 10000
LISTEN  1        5            192.168.1.7:10000          0.0.0.0:*              
$ ss -natl |grep 10000
LISTEN  0        5            192.168.1.7:10000          0.0.0.0:*              
$ ss -natl |grep 10000
LISTEN  0        5            192.168.1.7:10000          0.0.0.0:*              
$ ss -natl |grep 10000
LISTEN  1        5            192.168.1.7:10000          0.0.0.0:*              
$ ss -natl |grep 10000
LISTEN  0        5            192.168.1.7:10000          0.0.0.0:*              
$ netstat  -s |grep -i overflowed2930 times the listen queue of a socket overflowed

可以看到,壓測過程中的 Recv-Q 出現了5,1 的值,表示全連接隊列中等待被處理的連接,而且有 2930 - 2283 = 647 次連接由于全連接隊列溢出而被丟棄

我們再來觀察一下 bench_client_accept 的日志情況:

$ grep -a 'Start OK' log/bench_client_accept.log  |wc -l
29736
$ grep -a 'start failed' log/bench_client_accept.log  |wc -l
264

可以看到最終有 264 個 client 由于服務端丟棄建立連接時 3 次握手的包而造成連接失敗

如果你細心的話會發現,全連接隊列溢出發生了 647 次,但是最終只有 264 個 client 建立失敗,why?其實原因很簡單,因為客戶端有重試機制,具體參數是 net.ipv4.tcp_syn_retries,這個暫且不詳說。

那再來看一下 tcpdump 抓包的結果,這里要用到一個 python 腳本 tcpdump_analyze.py 來處理一下 tcpdump.log 這個日志:

import os# tcpdump  -i any port 10000 and tcp -nn > tcpdump.logserver_ip_port = "192.168.1.7.10000"
client_map = {}with open('./tcpdump.log', 'r') as fin:for line in fin:sp = line.split()if len(sp) < 3:print("invalid line:{0}".format(line))continueclient_ip_port = sp[2]if client_ip_port == server_ip_port:client_ip_port = sp[4].split(':')[0]if client_ip_port not in client_map:client_map[client_ip_port] = [line]else:client_map[client_ip_port].append(line)connect_fail_client = []
connect_succ_client = []
connect_succ_client_normal = []
connect_succ_client_try   = []total_size = len(client_map)for k,v in client_map.items():print("{0}$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$begin".format(k))for ll in v:print(ll)connect_fail = Truefor i in v:# ack 1 is the last packet of tcp handshake from serverif i.find('ack 1,') != -1:connect_fail = Falsebreakif connect_fail:connect_fail_client.append(v)print("fail");else:connect_succ_client.append(v)if len(v) == 3:connect_succ_client_normal.append(v)print("succ no retry");else:connect_succ_client_try.append(v)print("succ with retry")print("{0}$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$end\n\n".format(k))print("\ntotal client:{0} connect success client size:{1}".format(total_size, len(connect_succ_client)))
print("\ntotal client:{0} connect success client normal handshake size:{1}".format(total_size, len(connect_succ_client_normal)))
print("\ntotal client:{0} connect success client after retry handshake size:{1}".format(total_size, len(connect_succ_client_try)))
print("\ntotal client:{0} connect fail client size:{1}".format(total_size, len(connect_fail_client)))

運行后得到結果:

$ python tcpdump_analyze.py(省略部分輸出)192.168.1.4.20409$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$begin
15:43:12.030247 IP 192.168.1.4.20409 > 192.168.1.7.10000: Flags [S], seq 2040611302, win 29200, options [mss 1460,sackOK,TS val 166083457 ecr 0], length 015:43:13.033419 IP 192.168.1.4.20409 > 192.168.1.7.10000: Flags [S], seq 2040611302, win 29200, options [mss 1460,sackOK,TS val 166084460 ecr 0], length 015:43:13.033661 IP 192.168.1.7.10000 > 192.168.1.4.20409: Flags [S.], seq 3015149333, ack 2040611303, win 65160, options [mss 1460,sackOK,TS val 3009296915 ecr 166084460], length 015:43:13.033667 IP 192.168.1.4.20409 > 192.168.1.7.10000: Flags [.], ack 1, win 29200, options [nop,nop,TS val 166084460 ecr 3009296915], length 0succ with retry
192.168.1.4.20409$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$end192.168.1.4.54379$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$begin
15:43:21.047376 IP 192.168.1.4.54379 > 192.168.1.7.10000: Flags [S], seq 2685382859, win 29200, options [mss 1460,sackOK,TS val 166092474 ecr 0], length 015:43:21.047514 IP 192.168.1.7.10000 > 192.168.1.4.54379: Flags [S.], seq 1736229374, ack 2685382860, win 65160, options [mss 1460,sackOK,TS val 3009304929 ecr 166092474], length 015:43:21.047528 IP 192.168.1.4.54379 > 192.168.1.7.10000: Flags [.], ack 1, win 29200, options [nop,nop,TS val 166092474 ecr 3009304929], length 0succ no retry
192.168.1.4.54379$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$endtotal client:30000 connect success client size:29736total client:30000 connect success client normal handshake size:29195total client:30000 connect success client after retry handshake size:541total client:30000 connect fail client size:264

上面的意思是總共有 29736 個 client 成功建立連接,而有 264 個 client 建立失敗;連接成功的 client 里有 29195 個是通過了正常的 3 次握手成功建立,沒有發生重試;而有 541 個 client 是發生了重試的情況下才建立連接成功

可以看到上面的輸出,發生重試 “succ with retry” 的部分,client 發送一個 SYN 之后,由于 server 全連接隊列溢出導致連接被丟棄,client 超時后重新發送 SYN 包,然后建立連接;

而上面連接失敗的客戶端,錯誤原因都是: errno = 110,也就是 “Connection timed out”。

Ok,到現在應該明白全連接隊列大小對于 tcp 3 次握手的影響,如果全連接隊列過小,一旦發生溢出,就會影響后續的連接。

調整內核參數,避免全連接隊列溢出

那我們修改一下 backlog 的大小,改大一些:

listen(listenfd,  100000);

然后我們修改內核參數:

net.core.netdev_max_backlog = 400000
net.core.somaxconn = 100000

可以通過打開 /etc/sysctl.conf 直接修改,或是通過命令修改:

$ sysctl -w net.core.netdev_max_backlog=400000

重新編譯運行,執行上述的壓測,觀察結果。

壓測前:

$ ss -natl |grep 10000
LISTEN  0        100000       192.168.1.7:10000          0.0.0.0:*              
$ netstat  -s |grep -i overflowed3118 times the listen queue of a socket overflowed

壓測后:

$ netstat  -s |grep -i overflowed3118 times the listen queue of a socket overflowed$ python tcpdump_analyze.py
(省略部分輸出)
total client:30000 connect success client size:30000total client:30000 connect success client normal handshake size:30000total client:30000 connect success client after retry handshake size:0total client:30000 connect fail client size:0

可以看到,當我們把內核參數以及 backlog 調大之后,30000 個 client 全部建立連接成功且沒有發生重試,服務端的 listen queue 沒有發生溢出

半連接隊列

全連接隊列存放的是已經完成 3次握手,等待應用層調用 accept() 處理這些連接;其實還有一個半連接隊列,當服務端收到客戶端的 SYN 包后,并且回復 SYN+ACK包后,服務端進入 SYN_RECV 狀態,此時這種連接稱為半連接,會被存放到半連接隊列,當完成 3 次握手之后,tcp 會把這個連接從半連接隊列中移到全連接隊列,然后等待應用層處理

那么怎么查看半連接隊列的大小呢?沒有直接的 linux command 來查詢半連接隊列的長度,但是根據上面的定義,服務端處于 SYN_RECV 狀態的數量就表示半連接的數量。所以采用一定的方式增大半連接的數量,看服務端 SYN_RECV 的數量最大值有多少,那就是半連接隊列的大小

那問題就來了,如何增大半連接的數量呢?這里采用到的就是 SYN-FLOOD 攻擊,通過發送大量的 SYN 包而不進行回應,造成服務端創建了大量的半連接,但是這些半連接不會被確認,最終把 tcp 半連接隊列占滿造成溢出,并影響正常的連接。

半連接隊列溢出

采用的工具是: hping3,一款很強大的工具。

啟動服務端:

# 192.168.1.7 作為服務端,監聽 10000 端口$ ./bench_server 192.168.1.7 10000

開始攻擊:

$ hping3 -S  --flood --rand-source -p 10000 192.168.1.7

觀察半連接數量:

$ netstat -ant |grep SYN
(省略)
tcp        0      0 192.168.1.7:10000       152.66.128.1:48581      SYN_RECV   
tcp        0      0 192.168.1.7:10000       208.220.119.30:57972    SYN_RECV   
tcp        0      0 192.168.1.7:10000       3.104.166.109:25975     SYN_RECV $ netstat -ant |grep SYN |wc -l
256

持續觀察,可以看到處于 SYN_RECV 狀態的連接基本保持在 256,說明半連接隊列的大小是 256。而此時,10000 端口已經比較難連接上了。

查看一下半連接隊列的丟棄情況:

$ netstat  -s |grep dropped26055883 SYNs to LISTEN sockets dropped

注意: 26055883 是一個累加值,可以持續觀察

那怎么增大半連接隊列大小呢?

增大半連接隊列,防止溢出

直接修改內核參數:

# 直接修改文件 /etc/sysctl.confnet.ipv4.tcp_max_syn_backlog = 100000

或者使用命令:

$ sysctl -w net.ipv4.tcp_max_syn_backlog=100000

據說半連接隊列并非只由這個參數決定,不同的系統的計算方式不一致,還會和全連接隊列大小有關

當然這個應對 SYN-Flood 攻擊只是輕微降低影響而已。

還可以設置 net.ipv4.tcp_syncookies = 0 來一定程度防范 SYN 攻擊。

syncookies 的原理就是當服務端收到客戶端 SYN 包后,不會放到半連接隊列里,而是通過 {src_ip, src_port, timestamp} 等計算一個 cookie(也就是一個哈希值),通過 SYN+ACK包返回給客戶端,客戶端返回一個 ACK 包,攜帶上這個 cookie,服務端通過校驗可以直接把這個連接放入全連接隊列。整個過程不需要半連接隊列的參與

SYN 重試

上面壓測驗證全連接隊列溢出的場景下,通過 tcpdump 抓包分析到有些連接是經過了重試才建立成功的,具體表現在:

客戶端發送 SYN 包請求建立連接,但此時由于服務端全連接隊列溢出或者半連接隊列溢出,該 SYN 包就會被丟棄,當客戶端遲遲無法收到服務端的 SYN+ACK 包后,客戶端超時重發 SYN 包,如果再次超時,那么根據內核設置的 SYN 超時重試次數決定是否繼續重發 SYN 包。

假設重試次數為 6 次:

  • 第一次發送 SYN 后等待 1 s (2^0);
  • 第二次發送 SYN 后等待 2 s (2^1);
  • 第三次發送 SYN 后等待 4 s (2^1);

所以當我們發現服務端出現了問題的時候,可以適當提高 SYN 重試的次數;當然過大的值也會影響問題的快速發現;

可以通過設置:

$ sysctl -w net.ipv4.tcp_syn_retries=2

The End

Ok, 到這里基本上把 tcp 3 次握手比較細節的地方講到了。 tcp 真是一個巨復雜的協議,還有不少值得深挖的東西!

Blog:

  • rebootcat.com

  • email: linuxcode2niki@gmail.com

2020-11-14 于杭州
By 史矛革

總結

以上是生活随笔為你收集整理的TCP全连接和半连接的问题探讨的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。