網絡編程-從TCP連接的建立說起
前言
網絡編程幾乎是每一門編程語言都會涉及的內容,雖然各種語言調用的方式可能不一樣,但它們背后的原理支持都是一樣的。因此本文將從TCP的連接的建立說起。在此之前,假設你已經對計算機網絡有了最基本的認識。
網絡編程做什么
當下網絡應用數不勝數,如微信,可以讓你通過網絡與遠在異國他鄉的朋友交流溝通;如在線視頻,讓你通過網絡就可以觀看你喜歡的視頻,而這一切的背后,都有網絡編程技術的支持。通俗來講,可以認為網絡編程是兩臺或者多臺主機(應用)之間進行數據交換或傳輸。
TCP:傳輸控制協議
而數據交換需要按照一定的規則,而這種規則就是協議。只有按照約定的規則,雙方之間才能正確地進行數據交換。而TCP就是這些協議的一種,它提供一種面向連接的,可靠的字節流服務。
- 面向連接:兩個使用TCP的應用在交換數據之前必須先建立一個TCP連接
- 可靠的:TCP有很多機制來盡可能的保證數據不丟失
- 字節流: 不區分是ASCII字符還是二進制數據,數據解釋交給應用層
為什么要理解TCP
事實上不理解TCP背后的基本原理,仍然可以寫出代碼,但是當你遇到一些奇奇怪怪的而通過API的說明又無法解決的問題時,你就會慶幸自己花了點時間去學習TCP了。
TCP連接的建立
關于TCP連接的建立,你可能早已耳熟能詳,其流程倒背如流。但我覺得還是有必要再理一理。TCP連接的建立,也就是三次握手的流程如下:
我們再試著描述一下三次握手的過程:
- 服務端啟動,并暫停等待,處于LISTEN狀態
- 客戶端發起連接請求,發送序列號seq=X,處于SYN_SENT狀態
- 服務端收到后,并回應ACK=X+1和seq=Y,處于SYN_RCVD狀態,客戶端發送能力,服務端接收能力正常。
- 客戶端收到服務端的ACK,連接建立,同時向服務端回復ACK,處于ESTABLISHED狀態
- 服務端收到ACK,連接建立,處于ESTABLISHED狀態,客戶端接收能力正常。
至此三次握手完成。需要注意的是,這是正常流程下的三次握手。而前面所說的這些狀態可以通過netstat命令或者ss命令查看到,當然有些狀態的存在時間比較短,可能無法觀察到。
好了,那么問題來了:
- 為什么要三次握手
- 連接到一個不存在的端口會發生什么
- 連接到一個不存在的服務器主機會發生什么
- 初始seq是如何變化的
- 半連接隊列是什么
- SYN攻擊是什么
如果以上所有問題你都能輕而易舉地回答出來,那么本文后面的內容你可以跳過了。
為什么要三次握手
這幾乎是面試中必問的一個問題。一個TCP連接是全雙工的,即數據在兩個方向上能同時傳輸。因此,建立連接的過程也就必須確認雙方的收發能力都是正常的。
四次握手是否可以呢?完全可以!但是沒有必要!在服務端收到SYN之后,它可以先回ACK,再發送SYN,但是這兩個信息可以一起發送出去,因此沒有必要。
兩次握手是否可以呢?想象這樣一種情況,客戶端發起了一個連接請求在網絡中滯留了很長時間,以至于在連接建立好且斷開連接后,它才到達服務端,此時如果采用兩次握手,那么服務端就會認為這個報文是新的連接請求,于是建立連接,等待客戶端發送數據,但是實際上客戶端根本沒有發出建立請求,也不會理睬服務端,因此導致服務端空等而浪費資源。
為什么服務器會認為這個遲到的報文是新的連接請求?因為如果采用兩次握手機制,那么服務端無法通過SYN來判斷這是一個遲到或者重復的報文,還是正常到達的報文,但是對于三次握手,即便出現這樣的情況,也不會在服務端建立起真正的連接。
一個正常的連接三次握手
我們利用tcpdump命令和nc命令來觀察一個正常的tcp連接建立過程。首先在終端1準備抓包:
- 1
- $ tcpdump port 1234 -i any -v -n
在終端2啟動監聽1234端口:
- 1
- $ nc -l 1234
在終端3連接:
- 1
- $ nc 127.0.0.1 1234
在終端1得到以下輸出內容:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
- 21:00:50.794424 IP (tos 0x0, ttl 64, id 50542, offset 0, flags [DF], proto TCP (6), length 60)
- 127.0.0.1.45848 > 127.0.0.1.1234: Flags [S], cksum 0xfe30 (incorrect -> 0x3163), seq 1310563628, win 43690, options [mss 65495,sackOK,TS val 3721786049 ecr 0,nop,wscale 7], length 0
- 21:00:50.794437 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
- 127.0.0.1.1234 > 127.0.0.1.45848: Flags [S.], cksum 0xfe30 (incorrect -> 0xef35), seq 1685196050, ack 1310563629, win 43690, options [mss 65495,sackOK,TS val 3721786049 ecr 3721786049,nop,wscale 7], length 0
- 21:00:50.794449 IP (tos 0x0, ttl 64, id 50543, offset 0, flags [DF], proto TCP (6), length 52)
- 127.0.0.1.45848 > 127.0.0.1.1234: Flags [.], cksum 0xfe28 (incorrect -> 0xc17a), ack 1, win 342, options [nop,nop,TS val 3721786049 ecr 3721786049], length 0
從上面抓包內容可以看到,總共有三個報文,分別是客戶端發送到服務端的SYN,服務端回應給客戶端的SYN和ACK,以及客戶端回應給服務端的ACK。
連接到一個不存在的端口
如果要連接的服務器端口不存在會出現什么情況呢?我們利用nc命令來抓包觀察。
在一個終端窗口使用管理員權限執行下面的命令進行抓包,并打印相關信息:
- 1
- $ tcpdump port 1234 -i any -v -n
在另外一個終端使用nc命令嘗試連接到本地的1234端口
- 1
- 2
- $ nc 127.0.0.1 1234 -v
- nc: connect to 127.0.0.1 port 1234 (tcp) failed: Connection refused
TCP抓包內容如下:
- 1
- 2
- 3
- 4
- 5
- tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
- 21:06:15.295407 IP (tos 0x0, ttl 64, id 29112, offset 0, flags [DF], proto TCP (6), length 60)
- 127.0.0.1.46108 > 127.0.0.1.1234: Flags [S], cksum 0xfe30 (incorrect -> 0x7fef), seq 1175796450, win 43690, options [mss 65495,sackOK,TS val 2076405654 ecr 0,nop,wscale 7], length 0
- 21:06:15.295462 IP (tos 0x0, ttl 64, id 58706, offset 0, flags [DF], proto TCP (6), length 40)
- 127.0.0.1.1234 > 127.0.0.1.46108: Flags [R.], cksum 0x77e7 (correct), seq 0, ack 1175796451, win 0, length 0
從抓包內容中可以看到,首先nc客戶端發送一個SYN(Flags為S),seq為1175796450。而后收到一個RST(Flags為R),seq為1175796451。
也就是說,如果連接到一個不存在的端口,服務端所在的系統會響應一個RST(復位),直接終止連接。
Flags字段含義如下:
- F : FIN - 結束; 結束會話
- S : SYN - 同步; 表示開始會話請求
- R : RST - 復位;中斷一個連接
- P : PUSH - 推送; 數據包立即發送
- A : ACK - 應答
- U : URG - 緊急
- E : ECE - 顯式擁塞提醒回應
- W : CWR - 擁塞窗口減少
連接到一個不存在的服務器
同樣是利用nc和tcpdump命令。
- 1
- $ tcpdump port 1234 -i any -v -n
在另外一個窗口使用nc命令連接到一個不存在的或者無法連接的服務器地址:
- 1
- 2
- $ nc 121.11.12.31 1234 -v
- nc: connect to 121.11.12.31 port 1234 (tcp) failed: Connection timed out
tcpdump輸出內容如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
- 21:13:04.259752 IP (tos 0x0, ttl 64, id 33411, offset 0, flags [DF], proto TCP (6), length 60)
- 192.168.0.103.52402 > 121.11.12.31.1234: Flags [S], cksum 0xcdc0 (correct), seq 2648987704, win 29200, options [mss 1460,sackOK,TS val 75888078 ecr 0,nop,wscale 7], length 0
- 21:13:05.269438 IP (tos 0x0, ttl 64, id 33412, offset 0, flags [DF], proto TCP (6), length 60)
- 192.168.0.103.52402 > 121.11.12.31.1234: Flags [S], cksum 0xc9ce (correct), seq 2648987704, win 29200, options [mss 1460,sackOK,TS val 75889088 ecr 0,nop,wscale 7], length 0
- 21:13:07.285415 IP (tos 0x0, ttl 64, id 33413, offset 0, flags [DF], proto TCP (6), length 60)
- 192.168.0.103.52402 > 121.11.12.31.1234: Flags [S], cksum 0xc1ee (correct), seq 2648987704, win 29200, options [mss 1460,sackOK,TS val 75891104 ecr 0,nop,wscale 7], length 0
- 21:13:11.445491 IP (tos 0x0, ttl 64, id 33414, offset 0, flags [DF], proto TCP (6), length 60)
- 192.168.0.103.52402 > 121.11.12.31.1234: Flags [S], cksum 0xb1ae (correct), seq 2648987704, win 29200, options [mss 1460,sackOK,TS val 75895264 ecr 0,nop,wscale 7], length 0
- 21:13:19.637403 IP (tos 0x0, ttl 64, id 33415, offset 0, flags [DF], proto TCP (6), length 60)
- 192.168.0.103.52402 > 121.11.12.31.1234: Flags [S], cksum 0x91ae (correct), seq 2648987704, win 29200, options [mss 1460,sackOK,TS val 75903456 ecr 0,nop,wscale 7], length 0
- 21:13:35.765417 IP (tos 0x0, ttl 64, id 33416, offset 0, flags [DF], proto TCP (6), length 60)
- 192.168.0.103.52402 > 121.11.12.31.1234: Flags [S], cksum 0x52ae (correct), seq 2648987704, win 29200, options [mss 1460,sackOK,TS val 75919584 ecr 0,nop,wscale 7], length 0
- 21:14:09.045497 IP (tos 0x0, ttl 64, id 33417, offset 0, flags [DF], proto TCP (6), length 60)
- 192.168.0.103.52402 > 121.11.12.31.1234: Flags [S], cksum 0xd0ad (correct), seq 2648987704, win 29200, options [mss 1460,sackOK,TS val 75952864 ecr 0,nop,wscale 7], length 0
通過實際操作可以發現,當發送第一個SYN沒有響應時,客戶端會再次發送;如果還是沒有響應,再隔更長一段時間,繼續發送SYN,最終連接超時。從觀察情況來看,默認會進行5次重發,5次的重試時間間隔分別為1s, 2s, 4s, 8s, 16s。
初始序列號是如何變化的
通過前面的兩次抓包可以看到,發送第一個SYN請求的初始序列號seq并不是固定的。實際上,不同的系統它的生成方法可能不同,但是可以知道的是,它在一定時間內,生成seq值肯定不同,否則服務端無法區分這到底是同一個seq的重發還是這個報文在網絡中滯留一段時間后又重新到達。RFC 793指出初始序列號可以可看成一個32位的計數器,每隔4ms加1(但不同系統實際實現又可能不太一樣,為了安全起見會處理成隨機值),因此當它重新回到開始的時候,已經過了夠長時間,使得網絡中延遲的報文早已消失。
半連接隊列
在服務器收到客戶端的連接請求,并發送ACK之后,服務端處于SYN_RECV狀態,此時的連接成為半連接,服務器會將半連接放到一個名為半連接隊列的地方。
SYN攻擊
正因如此,如果有人惡意地向服務器發送大量的SYN包,并且由于客戶端IP是偽造的,導致服務器收不到ACK,不斷重發ACK,以至于半連接隊列容易占滿,導致無法處理正常的連接請求,并且可能導致服務器資源耗盡。
如何處理SYN攻擊又是另外一個話題。
總結
TCP三次握手的正常場景我們很容易描述出來,但是涉及更多細節以及異常場景的時候,我們可能不是那么熟悉,通過本文可以簡單地了解TCP連接的建立,為后面的網絡編程打下基礎。但是需要說明的是,本文僅僅簡單介紹了TCP連接的建立,并沒有深入介紹。