網(wǎng)絡(luò)編程-從TCP連接的建立說起
前言
網(wǎng)絡(luò)編程幾乎是每一門編程語言都會涉及的內(nèi)容,雖然各種語言調(diào)用的方式可能不一樣,但它們背后的原理支持都是一樣的。因此本文將從TCP的連接的建立說起。在此之前,假設(shè)你已經(jīng)對計算機網(wǎng)絡(luò)有了最基本的認識。
網(wǎng)絡(luò)編程做什么
當下網(wǎng)絡(luò)應(yīng)用數(shù)不勝數(shù),如微信,可以讓你通過網(wǎng)絡(luò)與遠在異國他鄉(xiāng)的朋友交流溝通;如在線視頻,讓你通過網(wǎng)絡(luò)就可以觀看你喜歡的視頻,而這一切的背后,都有網(wǎng)絡(luò)編程技術(shù)的支持。通俗來講,可以認為網(wǎng)絡(luò)編程是兩臺或者多臺主機(應(yīng)用)之間進行數(shù)據(jù)交換或傳輸。
TCP:傳輸控制協(xié)議
而數(shù)據(jù)交換需要按照一定的規(guī)則,而這種規(guī)則就是協(xié)議。只有按照約定的規(guī)則,雙方之間才能正確地進行數(shù)據(jù)交換。而TCP就是這些協(xié)議的一種,它提供一種面向連接的,可靠的字節(jié)流服務(wù)。
- 面向連接:兩個使用TCP的應(yīng)用在交換數(shù)據(jù)之前必須先建立一個TCP連接
- 可靠的:TCP有很多機制來盡可能的保證數(shù)據(jù)不丟失
- 字節(jié)流: 不區(qū)分是ASCII字符還是二進制數(shù)據(jù),數(shù)據(jù)解釋交給應(yīng)用層
為什么要理解TCP
事實上不理解TCP背后的基本原理,仍然可以寫出代碼,但是當你遇到一些奇奇怪怪的而通過API的說明又無法解決的問題時,你就會慶幸自己花了點時間去學習TCP了。
TCP連接的建立
關(guān)于TCP連接的建立,你可能早已耳熟能詳,其流程倒背如流。但我覺得還是有必要再理一理。TCP連接的建立,也就是三次握手的流程如下:
我們再試著描述一下三次握手的過程:
- 服務(wù)端啟動,并暫停等待,處于LISTEN狀態(tài)
- 客戶端發(fā)起連接請求,發(fā)送序列號seq=X,處于SYN_SENT狀態(tài)
- 服務(wù)端收到后,并回應(yīng)ACK=X+1和seq=Y,處于SYN_RCVD狀態(tài),客戶端發(fā)送能力,服務(wù)端接收能力正常。
- 客戶端收到服務(wù)端的ACK,連接建立,同時向服務(wù)端回復(fù)ACK,處于ESTABLISHED狀態(tài)
- 服務(wù)端收到ACK,連接建立,處于ESTABLISHED狀態(tài),客戶端接收能力正常。
至此三次握手完成。需要注意的是,這是正常流程下的三次握手。而前面所說的這些狀態(tài)可以通過netstat命令或者ss命令查看到,當然有些狀態(tài)的存在時間比較短,可能無法觀察到。
好了,那么問題來了:
- 為什么要三次握手
- 連接到一個不存在的端口會發(fā)生什么
- 連接到一個不存在的服務(wù)器主機會發(fā)生什么
- 初始seq是如何變化的
- 半連接隊列是什么
- SYN攻擊是什么
如果以上所有問題你都能輕而易舉地回答出來,那么本文后面的內(nèi)容你可以跳過了。
為什么要三次握手
這幾乎是面試中必問的一個問題。一個TCP連接是全雙工的,即數(shù)據(jù)在兩個方向上能同時傳輸。因此,建立連接的過程也就必須確認雙方的收發(fā)能力都是正常的。
四次握手是否可以呢?完全可以!但是沒有必要!在服務(wù)端收到SYN之后,它可以先回ACK,再發(fā)送SYN,但是這兩個信息可以一起發(fā)送出去,因此沒有必要。
兩次握手是否可以呢?想象這樣一種情況,客戶端發(fā)起了一個連接請求在網(wǎng)絡(luò)中滯留了很長時間,以至于在連接建立好且斷開連接后,它才到達服務(wù)端,此時如果采用兩次握手,那么服務(wù)端就會認為這個報文是新的連接請求,于是建立連接,等待客戶端發(fā)送數(shù)據(jù),但是實際上客戶端根本沒有發(fā)出建立請求,也不會理睬服務(wù)端,因此導(dǎo)致服務(wù)端空等而浪費資源。
為什么服務(wù)器會認為這個遲到的報文是新的連接請求?因為如果采用兩次握手機制,那么服務(wù)端無法通過SYN來判斷這是一個遲到或者重復(fù)的報文,還是正常到達的報文,但是對于三次握手,即便出現(xiàn)這樣的情況,也不會在服務(wù)端建立起真正的連接。
一個正常的連接三次握手
我們利用tcpdump命令和nc命令來觀察一個正常的tcp連接建立過程。首先在終端1準備抓包:
- 1
- $ tcpdump port 1234 -i any -v -n
在終端2啟動監(jiān)聽1234端口:
- 1
- $ nc -l 1234
在終端3連接:
- 1
- $ nc 127.0.0.1 1234
在終端1得到以下輸出內(nèi)容:
- 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
從上面抓包內(nèi)容可以看到,總共有三個報文,分別是客戶端發(fā)送到服務(wù)端的SYN,服務(wù)端回應(yīng)給客戶端的SYN和ACK,以及客戶端回應(yīng)給服務(wù)端的ACK。
連接到一個不存在的端口
如果要連接的服務(wù)器端口不存在會出現(xiàn)什么情況呢?我們利用nc命令來抓包觀察。
在一個終端窗口使用管理員權(quán)限執(zhí)行下面的命令進行抓包,并打印相關(guān)信息:
- 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抓包內(nèi)容如下:
- 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
從抓包內(nèi)容中可以看到,首先nc客戶端發(fā)送一個SYN(Flags為S),seq為1175796450。而后收到一個RST(Flags為R),seq為1175796451。
也就是說,如果連接到一個不存在的端口,服務(wù)端所在的系統(tǒng)會響應(yīng)一個RST(復(fù)位),直接終止連接。
Flags字段含義如下:
- F : FIN - 結(jié)束; 結(jié)束會話
- S : SYN - 同步; 表示開始會話請求
- R : RST - 復(fù)位;中斷一個連接
- P : PUSH - 推送; 數(shù)據(jù)包立即發(fā)送
- A : ACK - 應(yīng)答
- U : URG - 緊急
- E : ECE - 顯式擁塞提醒回應(yīng)
- W : CWR - 擁塞窗口減少
連接到一個不存在的服務(wù)器
同樣是利用nc和tcpdump命令。
- 1
- $ tcpdump port 1234 -i any -v -n
在另外一個窗口使用nc命令連接到一個不存在的或者無法連接的服務(wù)器地址:
- 1
- 2
- $ nc 121.11.12.31 1234 -v
- nc: connect to 121.11.12.31 port 1234 (tcp) failed: Connection timed out
tcpdump輸出內(nèi)容如下:
- 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
通過實際操作可以發(fā)現(xiàn),當發(fā)送第一個SYN沒有響應(yīng)時,客戶端會再次發(fā)送;如果還是沒有響應(yīng),再隔更長一段時間,繼續(xù)發(fā)送SYN,最終連接超時。從觀察情況來看,默認會進行5次重發(fā),5次的重試時間間隔分別為1s, 2s, 4s, 8s, 16s。
初始序列號是如何變化的
通過前面的兩次抓包可以看到,發(fā)送第一個SYN請求的初始序列號seq并不是固定的。實際上,不同的系統(tǒng)它的生成方法可能不同,但是可以知道的是,它在一定時間內(nèi),生成seq值肯定不同,否則服務(wù)端無法區(qū)分這到底是同一個seq的重發(fā)還是這個報文在網(wǎng)絡(luò)中滯留一段時間后又重新到達。RFC 793指出初始序列號可以可看成一個32位的計數(shù)器,每隔4ms加1(但不同系統(tǒng)實際實現(xiàn)又可能不太一樣,為了安全起見會處理成隨機值),因此當它重新回到開始的時候,已經(jīng)過了夠長時間,使得網(wǎng)絡(luò)中延遲的報文早已消失。
半連接隊列
在服務(wù)器收到客戶端的連接請求,并發(fā)送ACK之后,服務(wù)端處于SYN_RECV狀態(tài),此時的連接成為半連接,服務(wù)器會將半連接放到一個名為半連接隊列的地方。
SYN攻擊
正因如此,如果有人惡意地向服務(wù)器發(fā)送大量的SYN包,并且由于客戶端IP是偽造的,導(dǎo)致服務(wù)器收不到ACK,不斷重發(fā)ACK,以至于半連接隊列容易占滿,導(dǎo)致無法處理正常的連接請求,并且可能導(dǎo)致服務(wù)器資源耗盡。
如何處理SYN攻擊又是另外一個話題。
總結(jié)
TCP三次握手的正常場景我們很容易描述出來,但是涉及更多細節(jié)以及異常場景的時候,我們可能不是那么熟悉,通過本文可以簡單地了解TCP連接的建立,為后面的網(wǎng)絡(luò)編程打下基礎(chǔ)。但是需要說明的是,本文僅僅簡單介紹了TCP連接的建立,并沒有深入介紹。