1. 程式人生 > >三次握手、四次握手、backlog

三次握手、四次握手、backlog

acc 前端 接受 close overflow 場景 路徑 inux 隨機

TCP:三次握手、四次握手、backlog及其他

TCP是什麽

首先看一下OSI七層模型:

技術分享

然後數據從應用層發下來,會在每一層都加上頭部信息進行封裝,然後再發送到數據接收端,這個基本的流程中每個數據都會經過數據的封裝和解封的過程,流程如下圖所示:

技術分享

在OSI七層模型中,每一層的作用和對應的協議如下圖所示:

技術分享

說回TCP,簡單說TCP(Transmission Control Protocol)即傳輸控制協議,是一種面向連接的、可靠的、基於ip的傳輸層協議

TCP協議頭部格式

要學習TCP協議,首先得知道TCP協議頭部的格式,我在網上找了一張覺得畫得比較好的TCP協議頭部格式的圖片:

技術分享

這張圖把TCP協議頭部格式的每部分都描述得比較清楚:

  1. Source Port與Destination Port表示源端口與目標端口,各占據2個字節
  2. Sequence Number表示順序號,占4個字節,每一個字節都有一個序號,連接建立時發送方將初始序號填寫到第一個發送的TCP段序號中
  3. Acknowledgment Number表示應答號,占4個字節,是期望收到對方下次發送的數據的第一個字節的序號,也就是期望收到的下一個報文段的首部中的序號
  4. Offset表示數據偏移量,占4位,表示數據開始的地方離TCP段的起始處有多遠,實際上就是TCP段首部的長度
  5. Reserved表示保留位,占4位,全為0,為了將來定義新的用途保留
  6. C表示CWR,占1位,擁塞窗口減少標識,發送方設置,用於表明它收到了ECE標識的TCP包,發送端通過降低發送窗口的大小來降低速率
  7. E表示ECN,占1位,用於TCP3次握手時表示一個TCP端是具備ECN功能的
  8. U表示URG,占1位,該標誌位表示緊急標識有效
  9. A表示ACK,占1位,表示Acknowledgment Number字段有效,這是一個確認的TCP包,0表示不是確認包
  10. P表示PSH,占1位,該標誌位設置時一般表示發送端緩存中已經沒有待發送的數據,接收端不將該數據進行隊列處理
  11. R表示RST,占1位,用於復位相應的TCP鏈接
  12. S表示SYN,占1位,該標誌僅在三次握手建立TCP連接時有效
  13. F表示FIN,占1位,帶有該標誌位的數據包用來結束一個TCP會話,但對應端口仍處於開放狀態,準備接收後續數據
  14. Window表示窗口,占2個字節,表示報文段發送方期望收到的字節數,換句話說用於表示接收端還有多少空間剩余,用於控制TCP流量
  15. Checksum表示校驗和,占2個字節,發送端基於數據內容計算一個數值,接收端要與發送端數值結果完全一樣,才能證明數據的有效性,接收端校驗失敗會直接丟掉這個數據包
  16. Urgent Pointer表示緊急指針,占2個字節,指向後面優先數據的字節,只有在URG標識設置了才有效
  17. TCP Options表示TCP選項,長度不定,但必須是32bits的整數倍,常見的選項包括MSS、SACK、Timestamp等

從圖上我們可以看到,TCP頭部的固定大小為20個字節,不過由於有可選字段,實際上TCP頭部的大小有可能超過20字節。

TCP三次握手

TCP三次握手是TCP一個比較重點的內容,來學習一下。

TCP三次握手其實就是TCP連接建立的過程,三次握手的目的是同步連接雙方的序列號和確認號並交換TCP窗口大小信息。下面是TCP三次握手的流程圖:

技術分享

畫得很清晰,可惜不是我畫的。整個流程為:

  1. 客戶端主動打開,發送連接請求報文段,將SYN標識位置為1,Sequence Number置為x(TCP規定SYN=1時不能攜帶數據,x為隨機產生的一個值),然後進入SYN_SEND狀態
  2. 服務器收到SYN報文段進行確認,將SYN標識位置為1,ACK置為1,Sequence Number置為y,Acknowledgment Number置為x+1,然後進入SYN_RECV狀態,這個狀態被稱為半連接狀態
  3. 客戶端再進行一次確認,將ACK置為1(此時不用SYN),Sequence Number置為x+1,Acknowledgment Number置為y+1發向服務器,最後客戶端與服務器都進入ESTABLISHED狀態

為什麽在第3步中客戶端還要再進行一次確認呢?這主要是為了防止已經失效的連接請求報文段突然又傳回到服務端而產生錯誤的場景:

技術分享
所謂"已失效的連接請求報文段"是這樣產生的。正常來說,客戶端發出連接請求,但因為連接請求報文丟失而未收到確認。於是客戶端再次發出一次連接請求,後來收到了確認,建立了連接。數據傳輸完畢後,釋放了連接,客戶端一共發送了兩個連接請求報文段,其中第一個丟失,第二個到達了服務端,沒有"已失效的連接請求報文段"。

現在假定一種異常情況,即客戶端發出的第一個連接請求報文段並沒有丟失,只是在某些網絡節點長時間滯留了,以至於延誤到連接釋放以後的某個時間點才到達服務端。本來這個連接請求已經失效了,但是服務端收到此失效的連接請求報文段後,就誤認為這是客戶端又發出了一次新的連接請求。於是服務端又向客戶端發出請求報文段,同意建立連接。假定不采用三次握手,那麽只要服務端發出確認,連接就建立了。

由於現在客戶端並沒有發出連接建立的請求,因此不會理會服務端的確認,也不會向服務端發送數據,但是服務端卻以為新的傳輸連接已經建立了,並一直等待客戶端發來數據,這樣服務端的許多資源就這樣白白浪費了。

采用三次握手的辦法可以防止上述現象的發生。比如在上述的場景下,客戶端不向服務端的發出確認請求,服務端由於收不到確認,就知道客戶端並沒有要求建立連接。
技術分享

TCP四次握手

TCP三次握手是TCP連接建立的過程,TCP四次握手則是TCP連接釋放的過程。下面是TCP四次握手的流程圖:

技術分享

當客戶端沒有數據再需要發送給服務端時,就需要釋放客戶端的連接,這整個過程為:

  1. 客戶端發送一個報文給服務端(沒有數據),其中FIN設置為1,Sequence Number置為u,客戶端進入FIN_WAIT_1狀態
  2. 服務端收到來自客戶端的請求,發送一個ACK給客戶端,Acknowledge置為u+1,同時發送Sequence Number為v,服務端年進入CLOSE_WAIT狀態
  3. 服務端發送一個FIN給客戶端,ACK置為1,Sequence置為w,Acknowledge置為u+1,用來關閉服務端到客戶端的數據傳送,服務端進入LAST_ACK狀態
  4. 客戶端收到FIN後,進入TIME_WAIT狀態,接著發送一個ACK給服務端,Acknowledge置為w+1,Sequence Number置為u+1,最後客戶端和服務端都進入CLOSED狀態

這裏的一個問題是,為什麽TCP連接的建立只需要三次握手而TCP連接的釋放需要四次握手呢:

因為服務端在LISTEN狀態下,收到建立請求的SYN報文後,把ACK和SYN放在一個報文裏發送給客戶端。而連接關閉時,當收到對方的FIN報文時,僅僅表示對方沒有需要發送的數據了,但是還能接收數據,己方未必數據已經全部發送給對方了,所以己方可以立即關閉,也可以將應該發送的數據全部發送完畢後再發送FIN報文給客戶端來表示同意現在關閉連接。

從這個角度而言,服務端的ACK和FIN一般都會分開發送。

使用Wireshark抓包驗證TCP三次握手過程

為了加深對TCP三次握手的理解,抓包看一下TCP三次握手的過程。我這裏訪問的是我們公司自己的網站,不打廣告,訪問的具體什麽頁面、哪個ip就不透露了。

抓包下來的內容為:

技術分享

這裏多說一句,由於wireshark抓包針對的是網卡,因此只要某張網卡上有網絡訪問,就會有數據包,這會導致Wireshark的抓包結果裏面會有大量數據包,而大多數都不是想要的,這種情況可以使用Wireshark的過濾規則。我這裏由於知道目標ip,因此使用的是"ip.src == xxx.xxx.xxx.xxx or ip.dst == xxx.xxx.xxx.xxx"這條規則只過濾特定的ip。

從抓包結果看來,整個過程符合TCP三次握手的預期:

  1. 客戶端發送SYN給服務端
  2. 服務端返回SYN+ACK給客戶端
  3. 客戶端確認,返回ACK給服務端

至於Sequence Number和Acknowledge Number就不看了,但是註意,前面說了Sequence Number是隨機產生的一個值,但是這裏確是0,不光這裏是0,抓其他的任何包這個值都是0。但其實這裏並不是真的0,而是Wireshark為了顯示更好閱讀,使用了relative sequence number相對序號,Sequence Number具體值我們也是可以看到的:

技術分享

第一個紅框就是上面說的relative sequence number,第二個紅框就是Sequence Number的真實值0xc978aa7e,轉換為十進制為3380128382,就是隨機產生的Sequence Number。

順便能看到,下一個數據包就是HTTP的數據包,因為TCP三次握手已完成,連接建立,正式傳輸應用層數據,傳輸的HTTP內容大小為704字節。

TCP的backlog

在學習TCP的時候發現的一個比較重要的知識點。

在TCP連接建立的過程中有如下的流程和隊列:

技術分享

如圖所示,這裏面有兩個隊列,分別為syns queue(半連接隊列)與accept queue(全連接隊列)。整個流程總結用文字如下:

  1. 服務端綁定某個端口並監聽
  2. 客戶端發送SYN給服務端發起第一次握手,此時服務端將此請求信息放在半連接隊列中並回復SYN+ACK給客戶端
  3. 客戶端收到SYN+ACK,發起應答,回復一個ACK給服務端,假設此時全連接隊列未滿,那麽從半連接隊列中拿出此請求信息放入全連接隊列中。如果全連接隊列滿了,那麽客戶端繼續向服務端發送ACK,服務端的處理方式和系統參數tcp_abort_on_overflow有關,Linux環境下可以通過執行"cat /proc/sys/net/ipv4/tcp_abort_on_overflow"來查看此參數:技術分享
    • 0表示字節丟棄該ACK
    • 1表示發送一個RST給客戶端,直接廢掉這個握手過程與連接
  4. 服務端accept處理此請求,從全連接隊列中將此請求信息拿出

backlog的定義是已連接但未進行accept處理的socket隊列大小,如果這個隊列滿了,將會發送一個ECONNREFUSED錯誤信息給到客戶端,即 linux 頭文件 /usr/include/asm-generic/errno.h中定義的“Connection refused”。

Java支持原生的Socket,我們可以寫一段代碼來驗證一下。首先是一個普通的客戶端Socket,模擬向本地的8888端口發起連接:

技術分享
 1 public class ClientSocketClass {
 2 
 3     private static Socket[] clients = new Socket[30];  
 4     
 5     public static void main(String[] args) throws Exception {
 6         for (int i = 0; i < 10; i++) {
 7             clients[i] = new Socket("127.0.0.1", 8888);
 8             System.out.println("Client:" + i);
 9         }
10     }
11     
12 }
技術分享

接著是服務端Socket,監聽8888端口,ServerSocket構造函數的第二個參數就是backlog的大小,如果backlog小於1或者不傳會給一個默認值50,代碼很簡單:

技術分享
 1 public class ServerSocketClass {
 2 
 3     public static void main(String[] args) throws Exception {
 4         ServerSocket server = new ServerSocket(8888, 5);
 5         
 6         while (true) {
 7             // server.accept();
 8         }
 9     }
10     
11 }
技術分享

先把註釋關閉,運行ServerSocketClass,先發起監聽,再運行ClientSocketClass,運行結果為:

技術分享
 1 Client:0
 2 Client:1
 3 Client:2
 4 Client:3
 5 Client:4
 6 Exception in thread "main" java.net.ConnectException: Connection refused: connect
 7     at java.net.DualStackPlainSocketImpl.connect0(Native Method)
 8     at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79)
 9     at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339)
10     at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
11     at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
12     at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
13     at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
14     at java.net.Socket.connect(Socket.java:579)
15     at java.net.Socket.connect(Socket.java:528)
16     at java.net.Socket.<init>(Socket.java:425)
17     at java.net.Socket.<init>(Socket.java:208)
18     at org.xrq.test.socket.ClientSocketClass.main(ClientSocketClass.java:11)
技術分享

看到Client只發起了五個請求,第六個請求發起被拒絕了,因為三次握手建立後,前五個請求占據了全連接隊列並沒有被處理,於是第六個請求進來,全連接隊列中沒有它的位置了,因此請求被拒絕。

如果註釋打開,又是不一樣的效果:

技術分享
 1 Client:0
 2 Client:1
 3 Client:2
 4 Client:3
 5 Client:4
 6 Client:5
 7 Client:6
 8 Client:7
 9 Client:8
10 Client:9
技術分享

這裏所有的十個客戶端請求全部被接受,因為accept()方法從全連接隊列中取出了連接請求進行處理。看得出來,backlog提供了容量限制功能,避免過多的客戶端Socket占據大量的服務端資源。

全連接隊列大小的問題

接著說說全連接隊列大小的問題。首先上面提到了backlog,不同的應用對backlog的默認值定義不同,比如:

  • Java的Socket默認backlog為50
  • Tomcat默認的backlog為100
  • 阿裏改造的Ali-Tomcat默認的backlog為200
  • Nginx默認的backlog為511

Tomcat可以通過server.xml配置文件中<Connector />節點中的acceptCount來修改backlog。如果請求量不是很大,使用Tomcat默認的100也可以,但如果訪問量比較大,建議這個值設置得大一些,比如1024或者更大。如果Tomcat前一層對SYC FLOOD攻擊的防禦沒有把握的話,最好將SYN COOKIE防禦也開啟。

但是,全連接隊列的大小未必是backlog的值,它是backlog與somaxconn(一個os級別的系統參數)的較小值。Linux環境下可以通過執行"cat /proc/sys/net/core/somaxconn"來查看:

技術分享

這個值系統默認的是128,假如傳入的backlog是10,取128和10的較小值,那麽最終的全連接隊列大小就是10。同樣,如果要修改Linux系統默認的全連接隊列大小的話,可以通過修改/proc/sys/net/core路徑下的somaxconn。

半連接隊列大小的問題

說完了全連接隊列大小的問題,接著說一下半連接隊列大小的問題,它是64與tcp_max_syn_backlog的較大值。

可以通過"cat /proc/sys/net/ipv4/tcp_max_syn_backlog"命令或者"cat /etc/sysctl.conf"命令來查看半連接隊列的大小。以後者為例,其實就是打開了/ect/sysctl.conf這個文件:

技術分享

標紅的即tcp_max_syn_backlog默認值,默認值為1024,可以通過修改這個值來修改系統默認的半連接隊列大小。

通過ss查看Socket統計狀態

前面說了這麽多全連接隊列,那麽如何查看全連接隊列大小?

在Linux環境下可以通過ss命令查看,ss命令全稱為Socket Statistics,顧名思義它用於統計Socket。netstat命令其實也可以顯示類似內容,但是ss命令相比netstat命令能夠顯示更多更詳細的有關TCP和連接狀態的信息,而且比netstat更快速更高效。

ss命令的參數就不列舉了,可以自己上網查看,這裏使用ss -lnt,即查看處於LISTEN狀態的TCP套接字,且不解析服務名稱:

技術分享

Send-Q表示當前端口的全連接隊列大小,Recv-Q表示全連接隊列當前使用了多少。

從Send-Q可以看到,它的值只有三種:128、50、1。這也印證了我們的結論,全連接隊列的大小為傳入的backlog與somaxconn的較小值。

參考文章

http://blog.csdn.net/oney139/article/details/8103223

http://www.jellythink.com/archives/705

http://jm.taobao.org/2017/05/25/525-1/

https://www.cnxct.com/something-about-phpfpm-s-backlog/

http://jaseywang.me/2014/07/20/tcp-queue-的一些問題/

三次握手、四次握手、backlog