連接一個 IP 不存在的主機(jī)時,握手過程是怎樣的?
文章持續(xù)更新,可以微信搜一搜「小白debug」第一時間閱讀,回復(fù)【教程】獲golang免費(fèi)視頻教程。本文已經(jīng)收錄在GitHub https://github.com/xiaobaiTech/golangFamily , 有大廠面試完整考點(diǎn)和成長路線,歡迎Star。
鴿了好長時間了,最近很忙。以前工作忙完,就抽空寫文章。
現(xiàn)在忙完工作,還要一三五學(xué)駕照,二四六看家具。有同感的老鐵們不要舉手,拉到右下角點(diǎn)個"在看"就好了。
真的,全怪某音。

扯遠(yuǎn)了,回到今天的主題。
方兄最近寫了篇很贊的文章 寫給想去字節(jié)寫 Go 的你 ,里面提到了兩個問題。
連接一個 IP 不存在的主機(jī)時,握手過程是怎樣的?
連接一個 IP 地址存在但端口號不存在的主機(jī)時,握手過程又是怎樣的呢?
讓我回想起曾經(jīng)也被面試官問過類似的問題,意識到應(yīng)該很多朋友會對這個問題感興趣。
所以來給大家嘮嘮。
這兩個問題可以延伸出非常多的點(diǎn)。
看完了,說不定能加分!

正常情況的握手過程是怎么樣的
上面提到的問題,其實(shí)是指TCP的三次握手流程。這絕對是面試八股文里的老股了。
我們簡單回顧下基礎(chǔ)知識點(diǎn)。

在服務(wù)端啟動好后會調(diào)用 listen() 方法,進(jìn)入到 LISTEN 狀態(tài),然后靜靜等待客戶端的連接請求到來。
而此時客戶端主動調(diào)用 connect(IP地址) ,就會向某個IP地址發(fā)起第一次握手,發(fā)送SYN 到目的服務(wù)器。
服務(wù)器在收到第一次握手后就會響應(yīng)客戶端,這是第二次握手。
客戶端在收到第二次握手的消息后,響應(yīng)服務(wù)的一個ACK,這算第三次握手,此時客戶端 就會進(jìn)入 ESTABLISHED狀態(tài),認(rèn)為連接已經(jīng)建立完成。
通過抓包可以直觀看出三次握手的流程。

連一個 IP 不存在的主機(jī)時,握手過程是怎樣的
那不存在的IP,分兩種,局域網(wǎng)內(nèi)和局域網(wǎng)外的。

我以我家里的情況舉例。
家里有一臺家用路由器。本質(zhì)上它的功能已經(jīng)集成了我們常說的路由器,交換機(jī)和無線接入點(diǎn)的功能了。
其中路由器和交換機(jī)在之前寫過的 《硬核圖解!30張圖帶你搞懂!路由器,集線器,交換機(jī),網(wǎng)橋,光貓有啥區(qū)別?》里已經(jīng)詳細(xì)介紹過了,就不再說一遍了。無線接入點(diǎn)基本可以認(rèn)為就是個放出 wifi 信號的組件。
家用路由器下,連著我的N臺設(shè)備,包括手機(jī)和電腦,他們的IP都有個共同點(diǎn)。都是 192.168.31.xx 形式的。其中,我的電腦的IP是192.168.31.6 ,這個可以通過 ifconfig查到。
符合這個形式的這些個設(shè)備,本質(zhì)上就是通過各種設(shè)備(wifi或交換機(jī)等)接入到上圖路由器的e2端口,他們共同構(gòu)成一個局域網(wǎng)。
因此,在我家,我們可以粗暴點(diǎn)認(rèn)為只要是 192.168.31.xx 形式的IP,就是局域網(wǎng)內(nèi)的IP。否則就是局域網(wǎng)外的IP,比如 192.0.2.2 。
目的IP在局域網(wǎng)內(nèi)
因?yàn)橥ㄟ^ ifconfig 可以查到我的局域網(wǎng)內(nèi)IP是192.168.31.6 ,這里盲猜末尾+1是不存在的 IP 。試了下,192.168.31.7 還真不存在。
$ ping 192.168.31.7
PING 192.168.31.7 (192.168.31.7): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2
Request timeout for icmp_seq 3
^C
--- 192.168.31.7 ping statistics ---
5 packets transmitted, 0 packets received, 100.0% packet loss
于是寫個程序嘗試連這個IP 。下面的代碼是 golang 寫的,大家不看代碼也沒關(guān)系,放出來只是方便大家自己復(fù)現(xiàn)的時候用的。
// tcp客戶端
package main
import (
"fmt"
"io"
"net"
"os"
)
func main() {
client, err := net.Dial("tcp", "192.168.31.7:8081")
if err != nil {
fmt.Println("err:", err)
return
}
defer client.Close()
go func() {
input := make([]byte, 1024)
for {
n, err := os.Stdin.Read(input)
if err != nil {
fmt.Println("input err:", err)
continue
}
client.Write([]byte(input[:n]))
}
}()
buf := make([]byte, 1024)
for {
n, err := client.Read(buf)
if err != nil {
if err == io.EOF {
return
}
fmt.Println("read err:", err)
continue
}
fmt.Println(string(buf[:n]))
}
}
然后嘗試抓包。

可以發(fā)現(xiàn)根本沒有三次握手的包,只有一些 ARP 包,在詢問“誰是 192.168.31.7,告訴一下 192.168.31.6” 。
這里有三個問題
為什么會發(fā)ARP請求?
為什么沒有TCP握手包?
ARP本身是沒有重試機(jī)制的,為什么ARP請求會發(fā)那么多遍?
首先我們看下正常情況下執(zhí)行connect,也就是第一次握手 的流程。

應(yīng)用層執(zhí)行connect過后,會通過socket層,操作系統(tǒng)接口,進(jìn)程會從用戶態(tài)進(jìn)入到內(nèi)核態(tài),此時進(jìn)入 傳輸層,因?yàn)槭?strong style="font-size: inherit;line-height: inherit;color: rgb(41, 98, 255);">TCP第一次握手,會加入TCP頭,且置SYN標(biāo)志。

然后進(jìn)入網(wǎng)絡(luò)層,我想要連的是 192.168.31.7 ,雖然它是我瞎編的,但IP頭還是得老老實(shí)實(shí)把它加進(jìn)去。
此時需要重點(diǎn)介紹的是鄰居子系統(tǒng),它在網(wǎng)絡(luò)層和數(shù)據(jù)鏈路層之間。可以通過ARP協(xié)議將目的IP轉(zhuǎn)為對應(yīng)的MAC地址,然后數(shù)據(jù)鏈路層就可以用這個MAC地址組裝幀頭。
我們看下那么ARP協(xié)議的流程是

1.先到本地ARP表查一下有沒有 192.168.31.7 對應(yīng)的 mac地址,有的話就返回,這里顯然是不可能會有的。
可以通過 arp -a 命令查看本機(jī)的 arp表都記錄了哪些信息
$ arp -a
? (192.168.31.1) at 88:c1:97:59:d1:c3 on en0 ifscope [ethernet]
? (224.0.0.251) at 1:0:4e:0:1:fb on en0 ifscope permanent [ethernet]
? (239.255.255.250) at 1:0:3e:7f:ff:fb on en0 ifscope permanent [ethernet]
2.看下 192.168.31.7 跟本機(jī)IP 192.168.31.6在不在一個局域網(wǎng)下。如果在的話,就在局域網(wǎng)內(nèi)發(fā)一個 arp 廣播,內(nèi)容就是 前面提到的 “誰是 192.168.31.7,告訴一下 192.168.31.6”。
3.如果目的IP跟本機(jī)IP不在同一個局域網(wǎng)下,那么會去獲取默認(rèn)網(wǎng)關(guān)的MAC地址,這里就是指獲取家用路由器的MAC地址。然后把消息發(fā)給家用路由器,讓路由器發(fā)到互聯(lián)網(wǎng),找到下一跳路由器,一跳一跳的發(fā)送數(shù)據(jù),直到把消息發(fā)到目的IP上,又或者找不到目的地最終被丟棄。
4.第2和第3點(diǎn)都是本地沒有查到 ARP 緩存記錄的情況,這時候會把SYN報文放進(jìn)一個隊(duì)列(叫unresolved_queue)里暫存起來,然后發(fā)起ARP請求;等ARP層收到ARP回應(yīng)報文之后,會再從緩存中取出 SYN 報文,組裝 MAC 幀頭,完成剛剛沒完成的發(fā)送流程。
如果經(jīng)過 ARP 流程能正常返回 MAC 地址,那皆大歡喜,直接給數(shù)據(jù)鏈路層,經(jīng)過 ring buffer 后傳到網(wǎng)卡,發(fā)出去。
但因?yàn)楝F(xiàn)在這個IP是瞎編的,因此不可能得到目的地址 MAC ,所以消息也一直沒法到數(shù)據(jù)鏈路層。整個流程卡在了ARP流程中。
而抓包是在數(shù)據(jù)鏈路層之后進(jìn)行的,因此 TCP 第一次握手的包一直沒能抓到,只能抓到為了獲得 192.168.31.7 的MAC地址的ARP請求。
發(fā)送數(shù)據(jù)時,是在經(jīng)過數(shù)據(jù)鏈路層之后的 dev_queue_xmit_nit 方法執(zhí)行抓包操作的,這是屬于網(wǎng)卡驅(qū)動層的方法了。
順帶一提,接收端抓包是在 __netif_receive_skb_core 方法里執(zhí)行的,也屬于網(wǎng)卡驅(qū)動層。感興趣的朋友們可以以這個為關(guān)鍵詞搜索相關(guān)知識點(diǎn)哈
此時 因?yàn)?TCP 協(xié)議是可靠的協(xié)議,對于 TCP 層來說,第一次握手的消息,已經(jīng)發(fā)出去了,但是一直沒有收到 ACK。也不知道消息是出去后是遇到什么事了。為了保證可靠性,它會不斷重發(fā)。
而每一次重發(fā),都會因?yàn)橥瑯拥脑颍]有目的 MAC 地址)而尬在了 ARP 那個流程里。因此,才看到好幾次重復(fù)的 ARP 消息。
那回到剛剛的三個問題
為什么會發(fā) ARP 請求?
因?yàn)槟康牡刂肥窍咕幍模镜谹RP表沒有目的機(jī)器的MAC地址,因此發(fā)出ARP消息。
為什么沒有 TCP 握手包?
因?yàn)閰f(xié)議棧的數(shù)據(jù)到了網(wǎng)絡(luò)層后,在數(shù)據(jù)鏈路層前,就因?yàn)闆]有目的MAC地址,沒法發(fā)出。因此抓包軟件抓不到相關(guān)數(shù)據(jù)。
為什么 ARP 請求會發(fā)那么多遍?
因?yàn)?TCP 協(xié)議的可靠性,會重發(fā)第一次握手的消息,但每一次都因?yàn)闆]有目的 MAC 地址而失敗,每次都會發(fā)出ARP請求。
小結(jié)
連一個 IP 不存在的主機(jī)時,如果目的IP在局域網(wǎng)內(nèi),則第一次握手會失敗,接著不斷嘗試重發(fā)握手的請求。同時,本機(jī)會不斷發(fā)出ARP請求,企圖獲得目的機(jī)器的 MAC 地址。并且,因?yàn)闆]能獲得目的 MAC 地址,這些 TCP 握手請求最終都發(fā)不出去,
目的IP在局域網(wǎng)外
上面提到的是,目的 IP 在局域網(wǎng)內(nèi)的情況,下面討論目的IP在局域網(wǎng)外的情況。
瞎編一個不是 192.168.31.xx 形式的 IP 作為這次要用的局域網(wǎng)外IP, 比如 10.225.31.11。
先抓包看一下。

這次的現(xiàn)象是能發(fā)出 TCP 第一次握手的 SYN包。
這里有兩個問題
為什么連局域網(wǎng)外的 IP 現(xiàn)象跟連局域網(wǎng)內(nèi)不一致?
TCP 第一次握手的重試規(guī)律好像不太對?
為什么連局域網(wǎng)外的IP現(xiàn)象跟連局域網(wǎng)內(nèi)不一致?
這個問題的答案其實(shí)在上面 ARP 的流程里已經(jīng)提到過了,如果目的 IP 跟本機(jī) IP 不在同一個局域網(wǎng)下,那么會去獲取默認(rèn)網(wǎng)關(guān)的 MAC 地址,這里就是指獲取家用路由器的MAC地址。
此時ARP流程成功返回家用路由器的 MAC 地址,數(shù)據(jù)鏈路層加入幀頭,消息通過網(wǎng)卡發(fā)到了家用路由器上。
消息會通過互聯(lián)網(wǎng)一直傳遞到某個局域網(wǎng)為 10.225.31.xx 的路由器上,那個路由器 發(fā)出ARP 請求,詢問他們局域網(wǎng)內(nèi)的機(jī)器有沒有叫 10.225.31.11的 (結(jié)果當(dāng)然沒有)。
最終沒能發(fā)送成功,發(fā)送端也就遲遲收不到目的機(jī)的第二次握手響應(yīng)。
因此觸發(fā)TCP重傳。
TCP第一次握手的重試規(guī)律好像不太對?
在 Linux 中,第一次握手的 SYN 重傳次數(shù),是通過 tcp_syn_retries 參數(shù)控制的。可以通過下面的方式查看
$cat /proc/sys/net/ipv4/tcp_syn_retries
6
這里的含義是指 syn重傳 會發(fā)生6次。
而每次重試都會間隔一定的時間,這里的間隔一般是 1s,2s,4s,8s, 16s, 32s .

而事實(shí)上,看我的截圖,是先重試4次,每次都是1s,之后才是 1s,2s,4s,8s, 16s, 32s 的重試。
這跟我們知道的不太一樣。
這個是因?yàn)?strong style="font-size: inherit;line-height: inherit;color: rgb(41, 98, 255);">我用的是macOS抓的包,跟linux就不是一個系統(tǒng),各自的TCP協(xié)議棧在sync重傳方面的實(shí)現(xiàn)都可能會有一定的差異。
我還聽說 oppo 和 vivo 的 syn重傳 是0.5s起步的。而 windows 的 syn重傳 還有自己的專利。
這些冷知識大家可以不用在意。面試的時候知道linux的就夠了,剩下的可以用來裝逼。畢竟面試官不在意"茴"字到底有幾種寫法。
連IP 地址存在但端口號不存在的主機(jī)的握手過程
前面提到的是IP地址壓根就不存在的情況。假如IP地址存在但端口號是瞎編的呢?
目的IP是回環(huán)地址

現(xiàn)象也比較簡單,已經(jīng)IP地址是存在的,也就是在互聯(lián)網(wǎng)中這個機(jī)器是存在的。
那么我們可以正常發(fā)消息到目的IP,因?yàn)閷?yīng)的MAC地址和IP都是正確的,所以,數(shù)據(jù)從數(shù)據(jù)鏈路層到網(wǎng)絡(luò)層都很OK。
直到傳輸層,TCP協(xié)議在識別到這個端口號對應(yīng)的進(jìn)程根本不存在時,就會把數(shù)據(jù)丟棄,響應(yīng)一個RST消息給發(fā)送端。

RST是什么?
我們都是到TCP正常情況下斷開連接是用四次揮手,那是正常時候的優(yōu)雅做法。
但異常情況下,收發(fā)雙方都不一定正常,連揮手這件事本身都可能做不到,所以就需要一個機(jī)制去強(qiáng)行關(guān)閉連接。
RST 就是用于這種情況,一般用來異常地關(guān)閉一個連接。它在TCP包頭中,在收到置了這個標(biāo)志位的數(shù)據(jù)包后,連接就會被關(guān)閉,此時接收到 RST的一方,一般會看到一個 connection reset 或 connection refused 的報錯。

目的IP在局域網(wǎng)內(nèi)
剛剛提到我的本機(jī)IP是 192.168.31.6 ,局域網(wǎng)內(nèi)有臺 192.168.31.1 。同樣嘗試連一個不存在的端口。

此時現(xiàn)象跟前者一致。
唯一不同的是,前者是回環(huán)地址,RST數(shù)據(jù)是從本機(jī)的傳輸層返回的。而這次的情況,RST數(shù)據(jù)是從目的機(jī)器的傳輸層返回的。

目的IP在局域網(wǎng)外
找一個存在的外網(wǎng)ip,這里我拿了最近剛白嫖的阿里云服務(wù)器地址 47.102.221.141 。(炫耀)
進(jìn)行連接連接,發(fā)現(xiàn)與前面兩種情況是一致的,目的機(jī)器在收到我的請求后,立馬就通過 RST標(biāo)志位 斷開了這次的連接。

這一點(diǎn)跟前面兩種情況一致。
熟悉小白的朋友們都知道,每次搞事情做測試,都會用 baidu.com 。
這次也不例外,ping 一下 baidu.com ,獲得它的 IP: 220.181.38.148 。
$ ping baidu.com
PING baidu.com (220.181.38.148): 56 data bytes
64 bytes from 220.181.38.148: icmp_seq=0 ttl=48 time=35.728 ms
64 bytes from 220.181.38.148: icmp_seq=1 ttl=48 time=38.052 ms
64 bytes from 220.181.38.148: icmp_seq=2 ttl=48 time=37.845 ms
64 bytes from 220.181.38.148: icmp_seq=3 ttl=48 time=37.210 ms
64 bytes from 220.181.38.148: icmp_seq=4 ttl=48 time=38.402 ms
64 bytes from 220.181.38.148: icmp_seq=5 ttl=48 time=37.692 ms
^C
--- baidu.com ping statistics ---
6 packets transmitted, 6 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 35.728/37.488/38.402/0.866 ms
發(fā)消息到給百度域名背后的 IP,且瞎隨機(jī)指定一個端口 8080, 抓包。

現(xiàn)象卻不一致。沒有 RST 。而且觸發(fā)了第一次握手的重試消息。這是為什么?
這是因?yàn)閎aidu的機(jī)器,作為線上生產(chǎn)的機(jī)器,會設(shè)置一系列安全策略,比如只對外暴露某些端口,除此之外的端口,都一律拒絕。
所以很多發(fā)到 8080端口的消息都在防火墻這一層就被拒絕掉了,根本到不了目的主機(jī)里,而RST是在目的主機(jī)的TCP/IP協(xié)議棧里發(fā)出的,都還沒到這一層,就更不可能發(fā)RST了。因此發(fā)送端發(fā)現(xiàn)消息沒有回應(yīng)(因?yàn)楸环阑饓G了),就會重傳。所以才會出現(xiàn)上述抓包里的現(xiàn)象。

總結(jié)
連一個 IP 不存在的主機(jī)時
如果IP在局域網(wǎng)內(nèi),會發(fā)送N次ARP請求獲得目的主機(jī)的MAC地址,同時不能發(fā)出TCP握手消息。
如果IP在局域網(wǎng)外,會將消息通過路由器發(fā)出,但因?yàn)樽罱K找不到目的地,觸發(fā)TCP重試流程。
連IP 地址存在但端口號不存在的主機(jī)時
不管目的IP是回環(huán)地址還是局域網(wǎng)內(nèi)外的IP地址,目的主機(jī)的傳輸層都會在收到握手消息后,發(fā)現(xiàn)端口不正確,發(fā)出RST消息斷開連接。
當(dāng)然如果目的機(jī)器設(shè)置了防火墻策略,限制他人將消息發(fā)到不對外暴露的端口,那么這種情況,發(fā)送端就會不斷重試第一次握手。
最后留個問題,連一個 不存在的局域網(wǎng)外IP的主機(jī)時,我們可以看到TCP的重發(fā)規(guī)律是:開始時,每隔1s重發(fā)五次 TCP SYN消息,接著2s,4s,8s,16s,32s都重發(fā)一次;
對比連一個 不存在的局域網(wǎng)內(nèi)IP的主機(jī)時,卻是每隔1s重發(fā)了4次ARP請求,接著過了32s后才再發(fā)出一次ARP請求。已知ARP請求是沒有重傳機(jī)制的,它的重試就是TCP重試觸發(fā)的,但兩者規(guī)律不一致,是為什么?
最后
歡迎大家加我微信(公眾號里右下角“聯(lián)系我”),互相圍觀朋友圈砍一刀啥的哈哈。
如果文章對你有幫助,看下文章底部右下角,做點(diǎn)正能量的事情(點(diǎn)兩下)支持一下。(卑微瘋狂暗示,拜托拜托,這對我真的很重要!)

我是小白,我們下期見。
別說了,一起在知識的海洋里嗆水吧
關(guān)注公眾號:【小白debug】
