深入剖析TCP/IP三次握手:網路通訊背後的秘密

網路連線狀態

網路連線狀態(11種)非常重要這裡既包含三次握手中的也包括四次斷開中的,所以要熟悉。
  • LISTEN 被動開啟,首先伺服器需要開啟一個socket進行監聽,監聽來自遠方TCP埠的連線請求,等於伺服器端執行socket、bind、listen三個函式之後阻塞在accept處。
  • SYN_SENT 表示主動連線,客戶端能透過應用程式呼叫connect()函式進行active open。於是客戶端TCP傳送一個SYN以請求建立一個連線,之後狀態為SYN_SEND,表示已傳送一個SYN到伺服器端,等待SYN 1+ACK 0響應。
  • SYN_RECV 伺服器端收到客戶端的SYN 1,然後狀態變為SYN_RECV。表示伺服器收到了客戶端發來的SYN,然後自己也響應了給客戶端一個SYN 1+ACK 1,然後等待客戶端確認。這時候客戶端過來的連線(屬於半連線狀態)被放在一個SYN佇列裡面,SYN泛洪***也是這樣的,就是伺服器響應了SYN+ACK之後,客戶端就不在傳送ACK了,然後繼續傳送SYN,直到把伺服器的最大連線數量耗盡。半連線佇列長度是由核心引數tcp_max_syn_backlog來決定的。
  • ESTABLISHED 代表一個開啟的連線,客戶端收到伺服器傳送的SYN 1+ACK 1,就變為這個狀態,然後向伺服器傳送ACK,如果伺服器收到這個ACK,那麼它也變為這個狀態。這個狀態就是表示連線以及建立,正在或即將傳輸資料。伺服器收到ACK以後就會把半連線從上面提到的SYN佇列中刪除,然後放到ACCEPT佇列中,這時這個半連線的狀態就變成了ESTABLISHED。
  • FIN_WAIT1 主動關閉端(可以是伺服器也可以是客戶端)應用程式呼叫了close,於是其TCP發出FIN主動關閉請求,也就是四次斷開的第一次,之後就進入了FIN_WAIT1狀態,等待遠端主機的ACK請求。
  • CLOSE_WAIT 被動關閉端(可以是伺服器有可以是客戶端)收到了對方發來的FIN後,進入該狀態,然後發出ACK+1以回應FIN請求(它的接收也作為檔案結束符傳遞給上層應用程式)。這個狀態實際上是說客戶端告訴伺服器我沒有請求或者資料要傳送了,等待看看伺服器或者說是程序還有沒有資料要傳送,如果有則繼續傳送,如果沒有的話,就傳送反向關閉指令。如果伺服器大量連線是這個狀態就要去檢視程式,很有可能是程式設計的問題。
  • FIN_WAIT2 主動關閉端收到ACK+1後,就進入的FIN_WAIT2狀態,也就等伺服器是否還有資料發來,如果伺服器沒有資料了,那麼伺服器就傳送的反向關閉指令。也就是反向關閉連線指令FIN1+ACK1。實際上是告訴客戶端我的資料傳送完了,可以關閉連線了。
  • LAST_ACK 被動關閉端,傳送反向結束連線請求FIN 1+ACK 1,然後進入LAST_ACK狀態,等待主動關閉端傳送ACK。
  • TIME_WAIT主動關閉端收到FIN 1 +ACK 1後,並進入TIME_WAIT狀態,然後傳送ACK+1,等待一段時間(2MSL)以確保伺服器收到了ACK+1,然後自己進入CLOSED狀態。這個階段主要是客戶端為了再次確認一下伺服器是否可以關閉連線,因為網路畢竟是不可靠的。對於伺服器有大量TIME_WAIT這個問題通常調整sysctl來解決。
  • CLOSING 比較少見,表示等待遠端TCP對連線中斷的確認。
  • CLOSED被動關閉端在收到ACK包以後,就進入closed狀態,連線結束。
三次握手過程
客戶端:傳送SYN=J請求建立連線,此時客戶端進入SYN_SENT狀態等待伺服器響應
伺服器:收到客戶端的SYN=J後傳送SYN=K, ACK J+1表示收到建立連線請求,然後自己進入SYN_RECV狀態進行等待客戶端的最後確認
客戶端:收到伺服器發來的SYN=K, ACK J+1然後傳送ACK=K+1表示收到之前確認,然後自己進入ESTABLISHED狀態表示自己處於連線建立狀態
伺服器:收到客戶端的ACK以後則自己進入ESTABLISHED狀態,此時雙方都處於連線建立狀態,之後進行資料傳送。
三次握手的目的:是為了告訴對方SEQ然後伺服器回覆SEQ+1,這樣傳送端就知道包沒有丟;另外握手的目的是交換資訊,比如:
MSS:最大傳輸包(不含TCP/IP頭),MMS+包頭就是MTU,如果MTU過大傳輸就會卡死。
SACK_PERM:是否支援Selective ack(使用者最佳化重傳效率),比如客戶端傳送5個包給伺服器,中途丟了2號包,伺服器回覆的時候只能回覆2,表示2號前面的都收到了,請求重傳2號包,可是客戶端並不知道2後面的345是否收到沒有,如果支援SACK的話,那麼伺服器請求重傳2的時候就可以同時告訴345已經收到,這樣客戶端只需要重傳2,如果沒有SACK機制,那麼客戶端就會重傳2345,這樣效率就低了。

半連線和全連線

未完成連線佇列:客戶端傳送SYN到伺服器,伺服器正在等待完成三次握手,此時就會把客戶端發起的這個連線請求放在該佇列裡,也就是sync佇列。這個佇列由net.ipv4.tcp_max_syn_backlog引數決定, 系統預設2048,伺服器埠狀態為 SYNC_RCVD。
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
已完成連線佇列:已經完成握手的連線從SYN佇列移動到這個佇列,也就是accept佇列,預設128(其實這個佇列最終的大小是由SOMAXCONN和使用listen函式傳入引數的兩者取最小值決定的),伺服器埠狀態為ESTABLISHED,在Linux核心2.4.25之後在/etc/sysctl.conf中
net.core.somaxconn = 128直接修改。
cat /proc/sys/net/core/somaxconn
TCP的三次握手第一步伺服器收到客戶端的SYN後,把該請求放在半連線佇列中,之後回覆SYN+ACK,當客戶端收到這個訊號併發送ACK之後並且伺服器正常收到和處理後就把該請求從半連線佇列移動到ACCEPT佇列,進入這個佇列才能從Listen變成accept。
比如syn泛洪***就是針對syn佇列的,***方不同的建立連線,但是隻做連線的第一步,當***者收到SYN+ACK後直接丟棄,導致受***的伺服器上這個佇列滿了然後其他正常請求就無法進入。
常見問題:客戶端在傳送完最後一個ACK之後伺服器端如果收到正常情況下應該把該連結從SYNC佇列移動到ACCEPT佇列,如果ACCEPT佇列滿了,預設伺服器丟棄不會響應,所以從客戶端角度來看三次握手已經完成,但伺服器沒有響應這個連結,這種情況經常出現在伺服器同時收到很多連結請求的時候。如何確定這個問題?使用如下命令:
netstat -s | egrep "listen|LISTEN"
如果出現:
xxxxx times the listen queue of a socket overflowed(全連線佇列溢位次數) 
xxxxx SYNs to LISTEN sockets ignored (半連線佇列溢位次數)
這兩個值有時你會看到一樣多,但是通常半連線溢位次數會大於等於全連線溢位次數。就說明可能會有這個問題。因為如果這個數值一直在增加那麼就要注意了。如果想再次確認,那麼你需要修改核心引數
echo '1' > /proc/sys/net/ipv4/tcp_abort_on_overflow
該引數預設為0,引數含義看後面。修改之後客戶端再次發起連線就會收到reset訊號,如果抓包收到這個訊號,就證明伺服器端的accept佇列滿了,你需要進行調整。比如JAVA中預設socket的backlog值大小是50.
ss -lnt
Send-Q:表示LISTEN埠上的全連線佇列最大為多少
Recv-Q:為全連線隊列當前使用了多少
全連線佇列大小取決於:min(backlog, somaxconn),前一個是在socket建立時傳入的(listen函式),somaxconn是OS級別的引數,這個somaxconn的含義請檢視後面的內涵引數說明
半連線佇列大小取決於:/proc/sys/net/ipv4/tcp_max_syn_backlog 這個核心引數
Nginx預設的accept佇列是511,而且是多個程序同時監聽一個埠;Tomcat的accept佇列是100,預設短連線。
# 檢視Accept佇列溢位情況,如果當前沒有溢位則沒有任何返回值netstat -s | grep TCPBacklogDrop
思考:
如果客戶端發出ACK之後剛好伺服器ACCEPT佇列滿了,也就是客戶端認為連線成功建立而實際上伺服器端連線沒有準備好,而這時客戶端認為建立好了而強行傳送資料會怎麼辦呢?客戶端傳送之後肯定會得不到響應,因為伺服器丟棄了,然後客戶端認為丟失所以進行重傳,一定次數之後客戶端認為異常,然後一直到超時最後斷開。

關於Backlog

TCP連線客戶端connect()返回並不代表TCP連線成功,有可能是伺服器接收佇列滿了,系統會丟棄後續的ACK請求,

客戶端以為建立了連線,然後就執行後續操作,然後就等待到超時。伺服器則會等待ACK超時,會重傳SYN。
TCP佇列的一些問題
  1. 客戶端透過connect向伺服器發出SYN包,客戶端會維護一個socket等待佇列,而伺服器則會維護一個SYN佇列
  2. 此時是半連線狀態,如果socket等待佇列滿了,伺服器則會丟棄,而客戶端會返回超時。只要客戶端沒有收到SYN+ACK,3秒後客戶端會再次傳送,然後依然沒有收到,9秒後再繼續傳送。
  3. 半連線SYN佇列長度由tcp_max_syn_backlog決定
  4. 當伺服器收到客戶端SYN後,會返回SYN+ACK包,客戶端的TCP協議棧會喚醒socket等待佇列,發出connect呼叫
  5. 客戶端返回ACK後,伺服器會進入一個新的叫做accept的佇列,這個佇列長度為min(backlog,somaxconn)預設情況下somaxconn是128,表示最多有129的ESTAB的連線等待accept(),而backlog的值由int listen(int sockfd,int backlog)中的第二個引數指定,其含義是設定listen()函式最多允許多個網路連線同時處於掛起狀態,大部分平臺都是511
  6. 當accept佇列滿了之後,即是客戶端繼續向伺服器傳送ACK包,也不會得到響應,此時伺服器透過tcp_abort_on_overflow來決定如何返回,0表示直接丟棄,1表示傳送RST通知;客戶端則會分別返回read timeout或者connection reset by peer。從上面可以看到有2個佇列,一個儲存SYN_SEND以及SYN_RECV,另外一個accept佇列儲存ESTAB的狀態。
比如客戶端通Nginx通訊,Nginx立即返回ACK,但是3秒後才返回響應資料,Nginx同後端通訊,傳送SYN請求等待3秒後端才響應,就可能是backlog值設定過小,導致accept queue溢位,SYN被丟棄導致3s重傳。 

關於ss命令中Recv-Q和Send-Q的含義?

這兩個指標在不同場景含義不同。一個是狀態處於LISTEN狀態、一個是非LISTEN的其他狀態。
LISTEN狀態
這裡的含義就是上面說的Recv-Q是當前全連線佇列使用量;Send-Q是當前對應程序SOCKET套接字最大blacklog的數量,也就是全連線佇列最大長度
非LISTEN狀態
Recv-Q:資料已經接收到本地快取,還有多少沒有被程式取走,單位bytes
Send-Q:要傳送的資料有多少還在本地緩衝區對方未確認,如果不是0可能是本地傳送資料過快或者對方接收資料過慢,單位bytes
上述兩個值在非LISTEN狀態下都應該保持0或者瞬間不為0,如果長期不為0則可能有問題。
# 統計各種狀態的值netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'# 統計特定程序的TCP狀態netstat -ntap | grep '3141'| awk '{++S[$6]} END {for(a in S) print a, S[a]}'

Linux核心中的TCP/IP引數

tcp_abort_on_overflow 預設為0
TCP全連線佇列也就是accept佇列滿了之後如何處理,預設是0,也就是丟棄,可以改為1,表示如果佇列滿了這時候有客戶端建立連線則傳送一個reset包給客戶端,表示廢除這個握手。
net.core.netdev_max_backlog 預設為128
表示當每個網路介面接收資料包的速率比核心處理這些包的速率快時,允許傳送到佇列的資料包最大數目。就是說當介面接收包的速度比核心處理的快時,那麼多出來的資料包要存放到佇列中,那麼這個佇列最大可以放多少個呢?就是這個引數設定的。

net.ipv4.tcp_max_orphans

用於設定系統中最多允許存在多少TCP套接字不被關聯到任何一個使用者檔案控制代碼上,如果超過這個值,那麼沒有與使用者檔案控制代碼關聯的TCP套接字就會被複位,同時給出警告資訊。這個值主要是為了防止DOS***。一般在系統記憶體比較大的情況下可以調大。

net.ipv4.tcp_max_syn_backlog

用於記錄尚未收到客戶端ACK資訊的連線請求最大值。記憶體比較多可以設定大一點。也就是半連線的佇列,表示伺服器收到了客戶端的SYN包同時伺服器也傳送了ACK+SYN,但是還沒有收到客戶端返回的ACK包,此時連線處於SYN_RECV狀態,當伺服器收到客戶端的ACK包時,則刪除該半連線條目,伺服器進入ESTABLISHED狀態,這時候把該連線放入Accept佇列。修改這個值可以增加更多的網路連線,但是過大容易受到SYN泛洪***。

net.core.somaxconn

表示用於調節系統同時發起的TCP連線數,一般為128,當高併發的情況下,如果這個值比較小,就會導致連線超時或者重傳現象。Nginx伺服器中定義的NGX_LISTEN_BACKLOG預設是511,所以需要調整這個引數。當伺服器收到ACK包之後,就會進入一個叫accept的佇列這個佇列的最大長度就是由這個引數決定的。表示最多可有多少個ESTAB的連線等待accept()。這個值表示已客戶端和伺服器已完成三次握手的已建立連線的佇列大小。

net.ipv4.tcp_timestamps

該引數用於設定時間戳,可以避免序列號重複,在一個埠速率比較大的網絡卡下,遇到重複的序列號的機率還是比較大的。如果設定為0表示停用對TCP時間戳的支援。預設情況下,系統是允許重複的。但是對於Nginx來說還是建議關閉。

net.ipv4_tcpsynack_retries

用於設定核心放棄TCP連線之前向客戶端傳送ACK+SYN包的數量,也就是重試次數。這個引數主要影響三次握手中的第二次,也就是伺服器向客戶端傳送SYN+前一個SYN的ACK。一般設定為1,表示核心放棄連線之前傳送一次SYN+ACK包。比如客戶端發來SYN,然後伺服器回覆ACK+SYN,這時候客戶端斷線了,之後會怎麼辦呢?伺服器會進行重發ACK+SYNC,Linux中預設重試5次,每次時間間隔為上一次的一倍,1s-2s-4s-8s-16s之後再等一個32s如果還沒有客戶端響應,則伺服器斷開這個連線。

net.ipv4.syn_retries

引數和上一個類似,這是這次是設定核心放棄建立連線之前傳送SYN包的數量。也建議設定為1.  
 net.ipv4.tcp.syncookies
修改此引數可以有效防範syn flood***。原理是在TCP伺服器收到SYN包後,***者就下線,這樣預設伺服器需要等待63秒之後才會斷開這個連線(中間伺服器要重試幾次),這樣伺服器的SYN佇列很快就滿了。這個引數的目的就是為了解決這個問題,當SYN佇列滿了,伺服器根據預源埠、目的IP和時間戳生產一個序列號(可以叫做cookie)傳送出去,如果是***者它是不會響應的,如果是真實請求則會返回這個cookie,然後伺服器根據這個Cookie來建立連線就算你不在SYN佇列中也可以。預設為0,1表示開啟。對於連線請求很大的伺服器不要開啟這個引數,因為它並不嚴謹。你應該設定三個引數來變相解決這個問題:net.ipv4_tcpsynack_retries、net.ipv4.tcp_max_syn_backlog和tcp_abort_on_overflow也就是,也就是減少重試次數、增大SYN佇列長度和如果處理不過來就拒絕。
net.ipv4.tcp_tw_reuse

表示開啟重用。允許將TIME_WAIT狀態的sockets重新用於新的TCP連線,因為大量處於TIME_WAIT狀態很浪費資源,佔用檔案描述符,預設為0,表示關閉,設定為1表示開啟;

net.ipv4.tcp_tw_recycle

表示開啟TCP連線中TIME_WAIT sockets的快速回收,預設為0,表示關閉。設定為1表示開啟。

net.ipv4.tcp_fin_timeout 

表示如果套接字由本端要求關閉,這個引數決定了它保持在FIN_WAIT-2狀態的時間。預設為2MSL。不建議修改,如果要修改可以根據實際情況而定。

net.ipv4.tcp_keepalive_time

TCP keepalive心跳包機制,用於檢測連線是否已經斷開,這個值就是設定檢測頻率的。表示當keepalive起用的時候,TCP傳送keepalive訊息的頻度。預設是2小時,改為20分鐘。

net.ipv4.ip_local_port_range = 1024 65000 

表示用於向外連線的埠範圍。預設情況下很小,改為1024到65000。

net.ipv4.tcp_max_tw_buckets = 5000

表示系統同時保持TIME_WAIT套接字狀態的最大數量,如果超過這個數字,TIME_WAIT套接字將立刻被清除並列印警告資訊。預設為180000,改為5000。對於Apache、Nginx等伺服器,上幾行的引數可以很好地減少TIME_WAIT套接字數量,但是對於Squid,效果卻不大。此項引數可以控制TIME_WAIT套接字的最大數量,避免Squid伺服器被大量的TIME_WAIT套接字拖死。

連結:https://www.cnblogs.com/rexcheny/p/9381433.html
                                                              (版權歸原作者所有,侵刪)


相關文章