關注“腳本之家”,與百萬開發者在一起
出處:小白debug(ID:)如若轉載請聯系原公眾號直入主題。方兄最近寫了篇很贊的文章寫給想去字節寫 Go 的你,里面提到了兩個問題。連接一個IP 不存在的主機時,握手過程是怎樣的?連接一個IP 地址存在但端口號不存在的主機時,握手過程又是怎樣的呢?讓我回想起曾經也被面試官問過類似的問題,意識到應該很多朋友會對這個問題感興趣。所以來給大家嘮嘮。這兩個問題可以延伸出非常多的點??赐炅?,說不定能加分!正常情況的握手過程是怎么樣的
上面提到的問題,其實是指TCP的三次握手流程。這絕對是面試八股文里的老股了。
我們簡單回顧下基礎知識點。
正常情況下的TCP三次握手
在服務端啟動好后會調用()方法,進入到狀態,然后靜靜等待客戶端的連接請求到來。
而此時客戶端主動調用(IP地址),就會向某個IP地址發起第一次握手,發送SYN到目的服務器。
服務器在收到第一次握手后就會響應客戶端,這是第二次握手。
客戶端在收到第二次握手的消息后,響應服務的一個ACK,這算第三次握手,此時客戶端 就會進入狀態,認為連接已經建立完成。
通過抓包可以直觀看出三次握手的流程。
正常三次握手抓包連一個 IP 不存在的主機時,握手過程是怎樣的
那不存在的IP,分兩種,局域網內和局域網外的。
家用路由器局域網互聯
我以我家里的情況舉例。
家里有一臺家用路由器。本質上它的功能已經集成了我們常說的路由器主機id找不到,交換機和無線接入點的功能了。
其中路由器和交換機在之前寫過,已經詳細介紹過了,就不再說一遍了。無線接入點基本可以認為就是個放出 wifi 信號的組件。
家用路由器下,連著我的N臺設備,包括手機和電腦,他們的IP都有個共同點。都是192.168.31.xx形式的。其中,我的電腦的IP是192.168.31.6,這個可以通過查到。
符合這個形式的這些個設備,本質上就是通過各種設備(wifi或交換機等)接入到上圖路由器的e2端口,他們共同構成一個局域網。
因此主機id找不到,在我家,我們可以粗暴點認為只要是 192.168.31.xx形式的IP,就是局域網內的IP。否則就是局域網外的IP,比如192.0.2.2。
目的IP在局域網內
因為通過 可以查到我的局域網內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 。下面的代碼是寫的,大家不看代碼也沒關系,放出來只是方便大家自己復現的時候用的。
//?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]))
????}
}
然后嘗試抓包。
連一個不存在的IP(局域網內)抓包
可以發現根本沒有三次握手的包,只有一些 ARP 包,在詢問“誰是192.168.31.7,告訴一下192.168.31.6” 。
這里有三個問題
首先我們看下正常情況下執行,也就是第一次握手 的流程。
正常的流程
應用層執行過后,會通過層,操作系統接口,進程會從用戶態進入到內核態,此時進入傳輸層,因為是TCP第一次握手,會加入TCP頭,且置SYN標志。
tcp報頭的SYN
然后進入網絡層,我想要連的是192.168.31.7,雖然它是我瞎編的,但IP頭還是得老老實實把它加進去。
此時需要重點介紹的是鄰居子系統,它在網絡層和數據鏈路層之間??梢酝ㄟ^ARP協議將目的IP轉為對應的MAC地址,然后數據鏈路層就可以用這個MAC地址組裝幀頭。
我們看下那么ARP協議的流程是
ARP流程
1.先到本地ARP表查一下有沒有192.168.31.7對應的mac地址,有的話就返回,這里顯然是不可能會有的。
可以通過 arp -a 命令查看本機的 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跟本機IP 192.168.31.6在不在一個局域網下。如果在的話,就在局域網內發一個 arp 廣播,內容就是 前面提到的 “誰是192.168.31.7,告訴一下192.168.31.6”。
3.如果目的IP跟本機IP不在同一個局域網下,那么會去獲取默認網關的MAC地址,這里就是指獲取家用路由器的MAC地址。然后把消息發給家用路由器,讓路由器發到互聯網,找到下一跳路由器,一跳一跳的發送數據,直到把消息發到目的IP上,又或者找不到目的地最終被丟棄。
4.第2和第3點都是本地沒有查到 ARP 緩存記錄的情況,這時候會把SYN報文放進一個隊列(叫)里暫存起來,然后發起ARP請求;等ARP層收到ARP回應報文之后,會再從緩存中取出 SYN 報文,組裝 MAC 幀頭,完成剛剛沒完成的發送流程。
如果經過 ARP 流程能正常返回 MAC 地址,那皆大歡喜,直接給數據鏈路層,經過ring 后傳到網卡,發出去。
但因為現在這個IP是瞎編的,因此不可能得到目的地址 MAC ,所以消息也一直沒法到數據鏈路層。整個流程卡在了ARP流程中。
而抓包是在數據鏈路層之后進行的,因此 TCP 第一次握手的包一直沒能抓到,只能抓到為了獲得 192.168.31.7的MAC地址的ARP請求。
發送數據時,是在經過數據鏈路層之后的 方法執行抓包操作的,這是屬于網卡驅動層的方法了。
順帶一提,接收端抓包是在 core 方法里執行的,也屬于網卡驅動層。感興趣的朋友們可以以這個為關鍵詞搜索相關知識點哈
此時 因為 TCP 協議是可靠的協議,對于 TCP 層來說,第一次握手的消息,已經發出去了,但是一直沒有收到 ACK。也不知道消息是出去后是遇到什么事了。為了保證可靠性,它會不斷重發。而每一次重發,都會因為同樣的原因(沒有目的 MAC 地址)而尬在了 ARP 那個流程里。因此,才看到好幾次重復的 ARP 消息。那回到剛剛的三個問題小結
連一個 IP 不存在的主機時,如果目的IP在局域網內,則第一次握手會失敗,接著不斷嘗試重發握手的請求。同時,本機會不斷發出ARP請求,企圖獲得目的機器的 MAC 地址。并且,因為沒能獲得目的 MAC 地址,這些 TCP 握手請求最終都發不出去,
目的IP在局域網外
上面提到的是,目的 IP 在局域網內的情況,下面討論目的IP在局域網外的情況。
瞎編一個不是 192.168.31.xx形式的 IP 作為這次要用的局域網外IP, 比如10.225.31.11。
先抓包看一下。
連一個不存在的IP(局域網外)抓包
這次的現象是能發出 TCP 第一次握手的SYN包。
這里有兩個問題
為什么連局域網外的IP現象跟連局域網內不一致?
這個問題的答案其實在上面ARP 的流程里已經提到過了,如果目的 IP 跟本機 IP 不在同一個局域網下,那么會去獲取默認網關的 MAC 地址,這里就是指獲取家用路由器的MAC地址。
此時ARP流程成功返回家用路由器的 MAC 地址,數據鏈路層加入幀頭,消息通過網卡發到了家用路由器上。
消息會通過互聯網一直傳遞到某個局域網為 10.225.31.xx的路由器上,那個路由器 發出ARP 請求,詢問他們局域網內的機器有沒有叫10.225.31.11的 (結果當然沒有)。
最終沒能發送成功,發送端也就遲遲收不到目的機的第二次握手響應。
因此觸發TCP重傳。
TCP第一次握手的重試規律好像不太對?
在 Linux 中,第一次握手的SYN重傳次數,是通過參數控制的??梢酝ㄟ^下面的方式查看
$cat?/proc/sys/net/ipv4/tcp_syn_retries
6
這里的含義是指syn重傳會發生6次。
而每次重試都會間隔一定的時間,這里的間隔一般是 1s,2s,4s,8s, 16s, 32s .
SYN重傳
而事實上,看我的截圖,是先重試4次,每次都是1s,之后才是 1s,2s,4s,8s, 16s, 32s 的重試。
這跟我們知道的不太一樣。
這個是因為我用的是macOS抓的包,跟linux就不是一個系統,各自的TCP協議棧在sync重傳方面的實現都可能會有一定的差異。
我還聽說oppo和vivo的 syn重傳 是0.5s起步的。而的 syn重傳 還有自己的專利。
這些冷知識大家可以不用在意。面試的時候知道linux的就夠了,剩下的可以用來裝逼。畢竟面試官不在意"茴"字到底有幾種寫法。
連IP 地址存在但端口號不存在的主機的握手過程
前面提到的是IP地址壓根就不存在的情況。假如IP地址存在但端口號是瞎編的呢?
目的IP是回環地址
連回環地址,端口不存在抓包
現象也比較簡單,已經IP地址是存在的,也就是在互聯網中這個機器是存在的。
那么我們可以正常發消息到目的IP,因為對應的MAC地址和IP都是正確的,所以,數據從數據鏈路層到網絡層都很OK。
直到傳輸層,TCP協議在識別到這個端口號對應的進程根本不存在時,就會把數據丟棄,響應一個RST消息給發送端。
連回環地址時端口不存在RST是什么?
我們都是到TCP正常情況下斷開連接是用四次揮手,那是正常時候的優雅做法。
但異常情況下,收發雙方都不一定正常,連揮手這件事本身都可能做不到,所以就需要一個機制去強行關閉連接。
RST就是用于這種情況,一般用來異常地關閉一個連接。它在TCP包頭中,在收到置了這個標志位的數據包后,連接就會被關閉,此時接收到 RST的一方,一般會看到一個 reset或 的報錯。
TCP報頭RST位目的IP在局域網內
剛剛提到我的本機IP是192.168.31.6,局域網內有臺192.168.31.1。同樣嘗試連一個不存在的端口。
連存在的局域網內IP,端口不存在抓包
此時現象跟前者一致。
唯一不同的是,前者是回環地址,RST數據是從本機的傳輸層返回的。而這次的情況,RST數據是從目的機器的傳輸層返回的。
連外網地址時端口不存在目的IP在局域網外
找一個存在的外網ip,這里我拿了最近剛白嫖的阿里云服務器地址47.102.221.141。(炫耀)
進行連接連接,發現與前面兩種情況是一致的,目的機器在收到我的請求后,立馬就通過RST標志位斷開了這次的連接。
連存在的局域網外IP,端口不存在抓包
這一點跟前面兩種情況一致。
熟悉小白的朋友們都知道,每次搞事情做測試,都會用 。
這次也不例外,ping 一下,獲得它的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
發消息到給百度域名背后的 IP,且瞎隨機指定一個端口8080, 抓包。
連baidu,端口不存在抓包
現象卻不一致。沒有RST。而且觸發了第一次握手的重試消息。這是為什么?
這是因為baidu的機器,作為線上生產的機器,會設置一系列安全策略,比如只對外暴露某些端口,除此之外的端口,都一律拒絕。
所以很多發到 8080端口的消息都在防火墻這一層就被拒絕掉了,根本到不了目的主機里,而RST是在目的主機的TCP/IP協議棧里發出的,都還沒到這一層,就更不可能發RST了。因此發送端發現消息沒有回應(因為被防火墻丟了),就會重傳。所以才會出現上述抓包里的現象。
防火墻安全策略總結
連一個 IP 不存在的主機時
連IP 地址存在但端口號不存在的主機時
最后留個問題,連一個不存在的局域網外IP的主機時,我們可以看到TCP的重發規律是:開始時,每隔1s重發五次TCP SYN消息,接著2s,4s,8s,16s,32s都重發一次;
對比連一個不存在的局域網內IP的主機時,卻是每隔1s重發了4次ARP請求,接著過了32s后才再發出一次ARP請求。已知ARP請求是沒有重傳機制的,它的重試就是TCP重試觸發的,但兩者規律不一致,是為什么?
圍觀!不限速的阿里云盤新功能來了!