TCP:三次握手、四次握手、backlog及其他
TCP是什麼
首先看一下OSI七層模型:
然後數據從應用層發下來,會在每一層都加上頭部信息進行封裝,然後再發送到數據接收端,這個基本的流程中每個數據都會經過數據的封裝和解封的過程,流程如下圖所示:
在OSI七層模型中,每一層的作用和對應的協議如下圖所示:
說回TCP,簡單說TCP(Transmission Control Protocol)即傳輸控制協議,是一種面向連接的、可靠的、基於Ip的傳輸層協議。
TCP協議頭部格式
要學習TCP協議,首先得知道TCP協議頭部的格式,我在網上找了一張覺得畫得比較好的TCP協議頭部格式的圖片:
這張圖把TCP協議頭部格式的每部分都描述得比較清楚:
- Source Port與Destination Port表示源埠與目標埠,各佔據2個位元組
- Sequence Number表示順序號,佔4個位元組,每一個位元組都有一個序號,連接建立時發送方將初始序號填寫到第一個發送的TCP段序號中
- Acknowledgment Number表示應答號,佔4個位元組,是期望收到對方下次發送的數據的第一個位元組的序號,也就是期望收到的下一個報文段的首部中的序號
- Offset表示數據偏移量,佔4位,表示數據開始的地方離TCP段的起始處有多遠,實際上就是TCP段首部的長度
- Reserved表示保留位,佔4位,全為0,為了將來定義新的用途保留
- C表示CWR,佔1位,擁塞窗口減少標識,發送方設置,用於表明它收到了ECE標識的TCP包,發送端通過降低發送窗口的大小來降低速率
- E表示ECN,佔1位,用於TCP3次握手時表示一個TCP端是具備ECN功能的
- U表示URG,佔1位,該標誌位表示緊急標識有效
- A表示ACK,佔1位,表示Acknowledgment Number欄位有效,這是一個確認的TCP包,0表示不是確認包
- P表示PSH,佔1位,該標誌位設置時一般表示發送端緩存中已經沒有待發送的數據,接收端不將該數據進行隊列處理
- R表示RST,佔1位,用於複位相應的TCP鏈接
- S表示SYN,佔1位,該標誌僅在三次握手建立TCP連接時有效
- F表示FIN,佔1位,帶有該標誌位的數據包用來結束一個TCP會話,但對應埠仍處於開放狀態,準備接收後續數據
- Window表示窗口,佔2個位元組,表示報文段發送方期望收到的位元組數,換句話說用於表示接收端還有多少空間剩餘,用於控制TCP流量
- Checksum表示校驗和,佔2個位元組,發送端基於數據內容計算一個數值,接收端要與發送端數值結果完全一樣,才能證明數據的有效性,接收端校驗失敗會直接丟掉這個數據包
- Urgent Pointer表示緊急指針,佔2個位元組,指向後面優先數據的位元組,只有在URG標識設置了才有效
- TCP Options表示TCP選項,長度不定,但必須是32bits的整數倍,常見的選項包括MSS、SACK、Timestamp等
從圖上我們可以看到,TCP頭部的固定大小為20個位元組,不過由於有可選欄位,實際上TCP頭部的大小有可能超過20位元組。
TCP三次握手
TCP三次握手是TCP一個比較重點的內容,來學習一下。
TCP三次握手其實就是TCP連接建立的過程,三次握手的目的是同步連接雙方的序列號和確認號並交換TCP窗口大小信息。下面是TCP三次握手的流程圖:
畫得很清晰,可惜不是我畫的。整個流程為:
- 客戶端主動打開,發送連接請求報文段,將SYN標識位置為1,Sequence Number置為x(TCP規定SYN=1時不能攜帶數據,x為隨機產生的一個值),然後進入SYN_SEND狀態
- 伺服器收到SYN報文段進行確認,將SYN標識位置為1,ACK置為1,Sequence Number置為y,Acknowledgment Number置為x+1,然後進入SYN_RECV狀態,這個狀態被稱為半連接狀態
- 客戶端再進行一次確認,將ACK置為1(此時不用SYN),Sequence Number置為x+1,Acknowledgment Number置為y+1發向伺服器,最後客戶端與伺服器都進入ESTABLISHED狀態
為什麼在第3步中客戶端還要再進行一次確認呢?這主要是為了防止已經失效的連接請求報文段突然又傳回到服務端而產生錯誤的場景:
所謂"已失效的連接請求報文段"是這樣產生的。正常來說,客戶端發出連接請求,但因為連接請求報文丟失而未收到確認。於是客戶端再次發出一次連接請求,後來收到了確認,建立了連接。數據傳輸完畢後,釋放了連接,客戶端一共發送了兩個連接請求報文段,其中第一個丟失,第二個到達了服務端,沒有"已失效的連接請求報文段"。
現在假定一種異常情況,即客戶端發出的第一個連接請求報文段並沒有丟失,只是在某些網路節點長時間滯留了,以至於延誤到連接釋放以後的某個時間點才到達服務端。本來這個連接請求已經失效了,但是服務端收到此失效的連接請求報文段後,就誤認為這是客戶端又發出了一次新的連接請求。於是服務端又向客戶端發出請求報文段,同意建立連接。假定不採用三次握手,那麼只要服務端發出確認,連接就建立了。
由於現在客戶端並沒有發出連接建立的請求,因此不會理會服務端的確認,也不會向服務端發送數據,但是服務端卻以為新的傳輸連接已經建立了,並一直等待客戶端發來數據,這樣服務端的許多資源就這樣白白浪費了。
採用三次握手的辦法可以防止上述現象的發生。比如在上述的場景下,客戶端不向服務端的發出確認請求,服務端由於收不到確認,就知道客戶端並沒有要求建立連接。
TCP四次握手
TCP三次握手是TCP連接建立的過程,TCP四次握手則是TCP連接釋放的過程。下面是TCP四次握手的流程圖:
當客戶端沒有數據再需要發送給服務端時,就需要釋放客戶端的連接,這整個過程為:
- 客戶端發送一個報文給服務端(沒有數據),其中FIN設置為1,Sequence Number置為u,客戶端進入FIN_WAIT_1狀態
- 服務端收到來自客戶端的請求,發送一個ACK給客戶端,Acknowledge置為u+1,同時發送Sequence Number為v,服務端年進入CLOSE_WAIT狀態
- 服務端發送一個FIN給客戶端,ACK置為1,Sequence置為w,Acknowledge置為u+1,用來關閉服務端到客戶端的數據傳送,服務端進入LAST_ACK狀態
- 客戶端收到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三次握手的預期:
- 客戶端發送SYN給服務端
- 服務端返回SYN+ACK給客戶端
- 客戶端確認,返回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(全連接隊列)。整個流程總結用文字如下:
- 服務端綁定某個埠並監聽
- 客戶端發送SYN給服務端發起第一次握手,此時服務端將此請求信息放在半連接隊列中並回復SYN+ACK給客戶端
- 客戶端收到SYN+ACK,發起應答,回復一個ACK給服務端,假設此時全連接隊列未滿,那麼從半連接隊列中拿出此請求信息放入全連接隊列中。如果全連接隊列滿了,那麼客戶端繼續向服務端發送ACK,服務端的處理方式和系統參數tcp_abort_on_overflow有關,Linux環境下可以通過執行"
cat /proc/sys/net/ipv4/tcp_abort_on_overflow
"來查看此參數:0表示位元組丟棄該ACK
1表示發送一個RST給客戶端,直接廢掉這個握手過程與連接
- 服務端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 看到Client只發起了五個請求,第六個請求發起被拒絕了,因為三次握手建立後,前五個請求佔據了全連接隊列並沒有被處理,於是第六個請求進來,全連接隊列中沒有它的位置了,因此請求被拒絕。 如果注釋打開,又是不一樣的效果: 1 Client:0 這裡所有的十個客戶端請求全部被接受,因為accept方法從全連接隊列中取出了連接請求進行處理。看得出來,backlog提供了容量限制功能,避免過多的客戶端Socket佔據大量的服務端資源。 全連接隊列大小的問題 接著說說全連接隊列大小的問題。首先上面提到了backlog,不同的應用對backlog的默認值定義不同,比如: Tomcat可以通過server.xml配置文件中 但是,全連接隊列的大小未必是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的較大值。
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.
17 at java.net.Socket.
18 at org.xrq.test.socket.ClientSocketClass.main(ClientSocketClass.java:11)
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
可以通過"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的較小值。
參考文章


※Django初探——工程創建以及models資料庫相關配置
※js 給文本框增加快捷鍵
※Java-面向對象總結
※python連接sql server資料庫實現增刪改查
※「2017-05-25」WebForm母版頁
TAG:科技優家 |
※Scout超神發條屠殺WE,EDG終結連敗!網友:Meiko不去握手?
※DOTA2:馬桶哥成功殺入Ti9,網友期待握手Kuroky!
※LPL最具「牌面」的選手:Meiko賽後不握手,Uzi比賽沒鏡頭
※EDG贏了比賽,Meiko卻又被噴了:不救iBoy,賽後不和WE隊員握手?
※RW中單輸不起?賽後拒絕和Doinb握手,因為比賽中Doinb做了這動作
※LOL:FNC中單換人了?贏了比賽卻是Perkz去握手
※傳諾基亞黑莓握手言和 Facebook效仿抖音海外版推短視頻應用Lasso
※長達9年商標拉鋸戰之後,Gucci與Guess選擇了握手言和
※蘋果高通握手言和,5G iPhone有著落了?
※Faker全明星贏下Uzi,握手時Faker沒忍住直接對著Uzi笑了出來!
※TI9:無情握手他lei了!EG大勝秘密晉級,下輪將迎戰OG
※奔跑吧:anglebaby和小朋友握手時,誰注意她的手了?細節見人品
※LOL中國隊決勝局陣容耐人尋味,賽後Uzi與Faker握手,一笑破心魔
※LOL:韓國5人被打到集體自閉!有誰注意Faker和Uzi握手那一幕?
※Uber與Waymo握手言和,代價是2.45 億美元
※E3:我們體驗了xCloud 把3A裝進口袋,未來緊握手中
※洲際賽握手環節:Duke一臉笑容,Faker扭頭一幕很真實!
※HTTPS協議詳解(四):TLS/SSL握手過程
※EDG戰勝WE後Meiko不去握手惹爭議,網友:RNG不也去了
※商業機密盜竊案塵埃落定!Uber與Waymo握手言和:後者獲賠2.45億美元股份