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

五月的倉頡發表於2017-05-31

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-的一些問題/

相關文章