连接一个 IP 不存在的主机时,握手过程是怎样的?
文章持续更新,可以微信搜一搜「小白debug」第一时间阅读,回复【教程】获golang免费视频教程。本文已经收录在GitHub https://github.com/xiaobaiTech/golangFamily , 有大厂面试完整考点和成长路线,欢迎Star。
鸽了好长时间了,最近很忙。以前工作忙完,就抽空写文章。
现在忙完工作,还要一三五学驾照,二四六看家具。有同感的老铁们不要举手,拉到右下角点个"在看"就好了。
真的,全怪某音。
![](https://filescdn.proginn.com/00115d4e92df80fb383980597557b0a3/d18dbb9f49e360de4ca3eff3478b2ae8.webp)
扯远了,回到今天的主题。
方兄最近写了篇很赞的文章 写给想去字节写 Go 的你 ,里面提到了两个问题。
连接一个 IP 不存在的主机时,握手过程是怎样的?
连接一个 IP 地址存在但端口号不存在的主机时,握手过程又是怎样的呢?
让我回想起曾经也被面试官问过类似的问题,意识到应该很多朋友会对这个问题感兴趣。
所以来给大家唠唠。
这两个问题可以延伸出非常多的点。
看完了,说不定能加分!
![](https://filescdn.proginn.com/9d1b860e6ce482f30e40e5fe6a5deb38/0e4e2ec198dd59b7ea00cbe755b642a3.webp)
正常情况的握手过程是怎么样的
上面提到的问题,其实是指TCP的三次握手流程。这绝对是面试八股文里的老股了。
我们简单回顾下基础知识点。
![正常情况下的TCP三次握手](https://filescdn.proginn.com/40876c1e9880ba2ddb6080547a365167/f505e403eb8cfcff6528e370cc59fb80.webp)
在服务端启动好后会调用 listen()
方法,进入到 LISTEN
状态,然后静静等待客户端的连接请求到来。
而此时客户端主动调用 connect(IP地址)
,就会向某个IP地址发起第一次握手,发送SYN
到目的服务器。
服务器在收到第一次握手后就会响应客户端,这是第二次握手。
客户端在收到第二次握手的消息后,响应服务的一个ACK
,这算第三次握手,此时客户端 就会进入 ESTABLISHED
状态,认为连接已经建立完成。
通过抓包可以直观看出三次握手的流程。
![正常三次握手抓包](https://filescdn.proginn.com/0491d8389324ebb3cb9049ae23623743/29723d0da4816eb141e62b5afb5e1d96.webp)
连一个 IP 不存在的主机时,握手过程是怎样的
那不存在的IP,分两种,局域网内和局域网外的。
![家用路由器局域网互联](https://filescdn.proginn.com/ebcfdf995a1239e6063fd8cc65db056e/545cf66ba63902dab108a6613122152a.webp)
我以我家里的情况举例。
家里有一台家用路由器。本质上它的功能已经集成了我们常说的路由器,交换机和无线接入点的功能了。
其中路由器和交换机在之前写过的 《硬核图解!30张图带你搞懂!路由器,集线器,交换机,网桥,光猫有啥区别?》里已经详细介绍过了,就不再说一遍了。无线接入点基本可以认为就是个放出 wifi 信号的组件。
家用路由器下,连着我的N台设备,包括手机和电脑,他们的IP都有个共同点。都是 192.168.31.xx
形式的。其中,我的电脑的IP是192.168.31.6
,这个可以通过 ifconfig
查到。
符合这个形式的这些个设备,本质上就是通过各种设备(wifi或交换机等)接入到上图路由器的e2端口,他们共同构成一个局域网。
因此,在我家,我们可以粗暴点认为只要是 192.168.31.xx
形式的IP,就是局域网内的IP。否则就是局域网外的IP,比如 192.0.2.2
。
目的IP在局域网内
因为通过 ifconfig 可以查到我的局域网内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
写的,大家不看代码也没关系,放出来只是方便大家自己复现的时候用的。
// 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(局域网内)抓包](https://filescdn.proginn.com/06ef5af3f277ca08a68e732245c234f5/8e7b5e47a826258e0a59e0046e89cece.webp)
可以发现根本没有三次握手的包,只有一些 ARP 包,在询问“谁是 192.168.31.7
,告诉一下 192.168.31.6
” 。
这里有三个问题
为什么会发ARP请求?
为什么没有TCP握手包?
ARP本身是没有重试机制的,为什么ARP请求会发那么多遍?
首先我们看下正常情况下执行connect
,也就是第一次握手 的流程。
![正常connect的流程](https://filescdn.proginn.com/13cfa29564dc465eb233450f1a342746/6acb0f4815f9aa8939d497d00ae09e35.webp)
应用层执行connect
过后,会通过socket层,操作系统接口,进程会从用户态进入到内核态,此时进入 传输层,因为是TCP第一次握手,会加入TCP头,且置SYN标志。
![tcp报头的SYN](https://filescdn.proginn.com/ce44f506252e89c158bc0fa535bb97e9/854fbb010c87d530ff5f1bafe74a84f8.webp)
然后进入网络层,我想要连的是 192.168.31.7
,虽然它是我瞎编的,但IP头还是得老老实实把它加进去。
此时需要重点介绍的是邻居子系统,它在网络层和数据链路层之间。可以通过ARP协议将目的IP转为对应的MAC地址,然后数据链路层就可以用这个MAC地址组装帧头。
我们看下那么ARP协议的流程是
![ARP流程](https://filescdn.proginn.com/325903cbc64860f4e7fcb8e7ce7e23a5/4aca4b5b6310a897625bd2e2534f6109.webp)
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报文放进一个队列(叫unresolved_queue)里暂存起来,然后发起ARP请求;等ARP层收到ARP回应报文之后,会再从缓存中取出 SYN 报文,组装 MAC 帧头,完成刚刚没完成的发送流程。
如果经过 ARP 流程能正常返回 MAC 地址,那皆大欢喜,直接给数据链路层,经过 ring buffer
后传到网卡,发出去。
但因为现在这个IP是瞎编的,因此不可能得到目的地址 MAC ,所以消息也一直没法到数据链路层。整个流程卡在了ARP流程中。
而抓包是在数据链路层之后进行的,因此 TCP 第一次握手的包一直没能抓到,只能抓到为了获得 192.168.31.7
的MAC地址的ARP请求。
发送数据时,是在经过数据链路层之后的 dev_queue_xmit_nit 方法执行抓包操作的,这是属于网卡驱动层的方法了。
顺带一提,接收端抓包是在 __netif_receive_skb_core 方法里执行的,也属于网卡驱动层。感兴趣的朋友们可以以这个为关键词搜索相关知识点哈
此时 因为 TCP 协议是可靠的协议,对于 TCP 层来说,第一次握手的消息,已经发出去了,但是一直没有收到 ACK。也不知道消息是出去后是遇到什么事了。为了保证可靠性,它会不断重发。
而每一次重发,都会因为同样的原因(没有目的 MAC 地址)而尬在了 ARP 那个流程里。因此,才看到好几次重复的 ARP 消息。
那回到刚刚的三个问题
为什么会发 ARP 请求?
因为目的地址是瞎编的,本地ARP表没有目的机器的MAC地址,因此发出ARP消息。
为什么没有 TCP 握手包?
因为协议栈的数据到了网络层后,在数据链路层前,就因为没有目的MAC地址,没法发出。因此抓包软件抓不到相关数据。
为什么 ARP 请求会发那么多遍?
因为 TCP 协议的可靠性,会重发第一次握手的消息,但每一次都因为没有目的 MAC 地址而失败,每次都会发出ARP请求。
小结
连一个 IP 不存在的主机时,如果目的IP在局域网内,则第一次握手会失败,接着不断尝试重发握手的请求。同时,本机会不断发出ARP请求,企图获得目的机器的 MAC 地址。并且,因为没能获得目的 MAC 地址,这些 TCP 握手请求最终都发不出去,
目的IP在局域网外
上面提到的是,目的 IP 在局域网内的情况,下面讨论目的IP在局域网外的情况。
瞎编一个不是 192.168.31.xx
形式的 IP 作为这次要用的局域网外IP, 比如 10.225.31.11
。
先抓包看一下。
![连一个不存在的IP(局域网外)抓包](https://filescdn.proginn.com/b1005285f0998b18731f0063a0e0f23a/519d955f466ad8eeefcfde8098b87eec.webp)
这次的现象是能发出 TCP 第一次握手的 SYN包
。
这里有两个问题
为什么连局域网外的 IP 现象跟连局域网内不一致?
TCP 第一次握手的重试规律好像不太对?
为什么连局域网外的IP现象跟连局域网内不一致?
这个问题的答案其实在上面 ARP 的流程里已经提到过了,如果目的 IP 跟本机 IP 不在同一个局域网下,那么会去获取默认网关的 MAC 地址,这里就是指获取家用路由器的MAC地址。
此时ARP流程成功返回家用路由器的 MAC 地址,数据链路层加入帧头,消息通过网卡发到了家用路由器上。
消息会通过互联网一直传递到某个局域网为 10.225.31.xx
的路由器上,那个路由器 发出ARP 请求,询问他们局域网内的机器有没有叫 10.225.31.11
的 (结果当然没有)。
最终没能发送成功,发送端也就迟迟收不到目的机的第二次握手响应。
因此触发TCP重传。
TCP第一次握手的重试规律好像不太对?
在 Linux 中,第一次握手的 SYN
重传次数,是通过 tcp_syn_retries
参数控制的。可以通过下面的方式查看
$cat /proc/sys/net/ipv4/tcp_syn_retries
6
这里的含义是指 syn重传
会发生6次。
而每次重试都会间隔一定的时间,这里的间隔一般是 1s,2s,4s,8s, 16s, 32s .
![SYN重传](https://filescdn.proginn.com/cebb205db6a2a025e78bd140b8ee1731/577c45f1ca4c0efea5d41bb39e9d0885.webp)
而事实上,看我的截图,是先重试4次,每次都是1s,之后才是 1s,2s,4s,8s, 16s, 32s 的重试。
这跟我们知道的不太一样。
这个是因为我用的是macOS抓的包,跟linux就不是一个系统,各自的TCP协议栈在sync重传方面的实现都可能会有一定的差异。
我还听说 oppo
和 vivo
的 syn重传 是0.5s起步的。而 windows
的 syn重传 还有自己的专利。
这些冷知识大家可以不用在意。面试的时候知道linux的就够了,剩下的可以用来装逼。毕竟面试官不在意"茴"字到底有几种写法。
连IP 地址存在但端口号不存在的主机的握手过程
前面提到的是IP地址压根就不存在的情况。假如IP地址存在但端口号是瞎编的呢?
目的IP是回环地址
![连回环地址,端口不存在抓包](https://filescdn.proginn.com/af93236370537e3d49988c2690580f2a/f63ef08f072ed55ebefeefe06f2b7d01.webp)
现象也比较简单,已经IP地址是存在的,也就是在互联网中这个机器是存在的。
那么我们可以正常发消息到目的IP,因为对应的MAC地址和IP都是正确的,所以,数据从数据链路层到网络层都很OK。
直到传输层,TCP协议在识别到这个端口号对应的进程根本不存在时,就会把数据丢弃,响应一个RST消息给发送端。
![连回环地址时端口不存在](https://filescdn.proginn.com/5cfd54e0b0831eafb01ca7300cf4114d/ee98e4d50f12469e489ac815c796db9f.webp)
RST是什么?
我们都是到TCP正常情况下断开连接是用四次挥手,那是正常时候的优雅做法。
但异常情况下,收发双方都不一定正常,连挥手这件事本身都可能做不到,所以就需要一个机制去强行关闭连接。
RST 就是用于这种情况,一般用来异常地关闭一个连接。它在TCP包头中,在收到置了这个标志位的数据包后,连接就会被关闭,此时接收到 RST的一方,一般会看到一个 connection reset
或 connection refused
的报错。
![TCP报头RST位](https://filescdn.proginn.com/7d09fbb12b9a17ab6f5972df2e970a71/7c3eb39fc4096dea2034b561a1c3f0a1.webp)
目的IP在局域网内
刚刚提到我的本机IP是 192.168.31.6
,局域网内有台 192.168.31.1
。同样尝试连一个不存在的端口。
![连存在的局域网内IP,端口不存在抓包](https://filescdn.proginn.com/75fad79e3d4e2cfa2d408570909c00b7/8ed12181f5c918d0699ce8ebccf964de.webp)
此时现象跟前者一致。
唯一不同的是,前者是回环地址,RST数据是从本机的传输层返回的。而这次的情况,RST数据是从目的机器的传输层返回的。
![连外网地址时端口不存在](https://filescdn.proginn.com/243ed9ba70afd8212df7b16e422ce537/aa37eafd9d2df6652990c836a7dec3ba.webp)
目的IP在局域网外
找一个存在的外网ip,这里我拿了最近刚白嫖的阿里云服务器地址 47.102.221.141
。(炫耀)
进行连接连接,发现与前面两种情况是一致的,目的机器在收到我的请求后,立马就通过 RST标志位 断开了这次的连接。
![连存在的局域网外IP,端口不存在抓包](https://filescdn.proginn.com/79421c1fd82f9308467b8b65aa4cff80/f352f9cd17c73999da8dc2a5c069c079.webp)
这一点跟前面两种情况一致。
熟悉小白的朋友们都知道,每次搞事情做测试,都会用 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
发消息到给百度域名背后的 IP,且瞎随机指定一个端口 8080, 抓包。
![连baidu,端口不存在抓包](https://filescdn.proginn.com/7c6bc1c86454d4caf2feef942a8a3c59/db0b0d35fccd80710ef27b7c18743158.webp)
现象却不一致。没有 RST
。而且触发了第一次握手的重试消息。这是为什么?
这是因为baidu的机器,作为线上生产的机器,会设置一系列安全策略,比如只对外暴露某些端口,除此之外的端口,都一律拒绝。
所以很多发到 8080端口的消息都在防火墙这一层就被拒绝掉了,根本到不了目的主机里,而RST是在目的主机的TCP/IP协议栈里发出的,都还没到这一层,就更不可能发RST了。因此发送端发现消息没有回应(因为被防火墙丢了),就会重传。所以才会出现上述抓包里的现象。
![防火墙安全策略](https://filescdn.proginn.com/70ec5371ce3f0bdb99ba11ecc5351627/7f8c07ca11abd635a697d26bae151fb0.webp)
总结
连一个 IP 不存在的主机时
如果IP在局域网内,会发送N次ARP请求获得目的主机的MAC地址,同时不能发出TCP握手消息。
如果IP在局域网外,会将消息通过路由器发出,但因为最终找不到目的地,触发TCP重试流程。
连IP 地址存在但端口号不存在的主机时
不管目的IP是回环地址还是局域网内外的IP地址,目的主机的传输层都会在收到握手消息后,发现端口不正确,发出RST消息断开连接。
当然如果目的机器设置了防火墙策略,限制他人将消息发到不对外暴露的端口,那么这种情况,发送端就会不断重试第一次握手。
最后留个问题,连一个 不存在的局域网外IP的主机时,我们可以看到TCP的重发规律是:开始时,每隔1s重发五次 TCP SYN
消息,接着2s,4s,8s,16s,32s都重发一次;
对比连一个 不存在的局域网内IP的主机时,却是每隔1s重发了4次ARP请求
,接着过了32s后才再发出一次ARP请求。已知ARP请求是没有重传机制的,它的重试就是TCP重试触发的,但两者规律不一致,是为什么?
最后
欢迎大家加我微信(公众号里右下角“联系我”),互相围观朋友圈砍一刀啥的哈哈。
如果文章对你有帮助,看下文章底部右下角,做点正能量的事情(点两下)支持一下。(卑微疯狂暗示,拜托拜托,这对我真的很重要!)
![](https://filescdn.proginn.com/cbec866aae93774f385610406366f1d6/3d494c0b958dcd45ed54c308422e30b7.webp)
我是小白,我们下期见。
别说了,一起在知识的海洋里呛水吧
关注公众号:【小白debug】