经典网络问题,贯穿整个计算机网络学习始终,每个阶段拿出来回味都有不同的感受与收获。
参考资料:《网络是怎样运行的》 by 户根勤
通过 DHCP 拿 IP 地址
假设这台主机刚刚插上网线准备接入互联网,那么此时此刻我们能够知道的仅有本机(或称之为客户端)的 MAC 地址这一信息,毕竟出厂就烧在 ROM 里。如果希望其能在网络中与其他节点进行通信,就需要知道客户端的 IP 地址。由于刚开始完全不知道任何信息,那么最先要做的事,就是去获得最近的 DHCP 服务器(下面简称 DS) 的 IP 地址,只有这样才能有足够的信息去构建请求消息,让其分配一个。
那么怎么获得呢?最直接的做法就是广播一条只有 DS 收到后会回复的消息,这样我们拿到这条回复后就知道 DS 的信息了。这条广播消息长这样:
链路层(MAC) | 网络层(IP) | 传输层(PORT/TYPE) | 描述(DESCRIPTION) |
---|---|---|---|
From: 客户端 MAC 地址 To: FF-FF-FF-FF-FF-FF |
From: 0.0.0.0 To: 255.255.255.255 |
UDP From: 68 To: 67 |
DHCP DISCOVER |
这条消息或许会经过交换机,那么交换机此时会记录下 <客户端 MAC 地址, 连接该主机的端口> 这一条目以构建转发表,这样从 DS 发来的回复就快速定位到特定端口而无需广播。
因为该消息的 MAC 地址为广播地址,就从所有端口出发进行广播。DS 收到该消息后,发现是个 DISCOVER 类型的消息,就知道有个小兔崽子在找我,我得赶紧告诉他,然后构建一条 OFFER 消息交给网卡去发送,包含自己的 IP 地址与 MAC 地址等信息。
客户端收到 OFFER 后,就开始正式发起请求了,就是发一条 REQUEST 消息过去,只不过这次的消息中 MAC 头部与 IP 头部的 To
字段都明确了。
当然,可能有多个 DS 收到该广播消息,它们都会回复 OFFER,那么就由客户端来选择向哪个 DS 请求 IP 地址。如果跳过这一个来回,直接广播 REQUEST,就有可能收到多个为客户端分配的 IP 地址,而我们只有一个能设置,其他的就浪费了。
DS 收到后会发一个 ACK 回来,告知客户端的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,客户端收到后进行配置,并在其 IP 转发表中安装默认网关。此刻我们正式完成了连接互联网的准备工作。
此时此刻,我们已知的信息有:客户端 MAC 地址、客户端 IP 地址、默认网关 IP 地址、DNS 服务器 IP 地址。
通过 DNS 获取 Web 服务器的 IP 地址
在浏览器内输入链接敲回车,或点击链接后,浏览器应当解析该链接生成 HTTP GET 请求,然后调用 Socket 库委托协议栈生成消息发出去。这之间还有一些工作要完成,最先要完成的就是获取 Web 服务器的目的 IP 地址,这一步是通过向 DNS 服务器发起查询完成的,理由和请求分配 IP 地址一样。这条查询会被组装为:
链路层(MAC) | 网络层(IP) | 传输层(PORT/TYPE) | 描述(DESCRIPTION) |
---|---|---|---|
From: 客户端 MAC 地址 To: 网关路由器 MAC 地址 |
From: 客户端 IP 地址 To: 网关路由器 |
UDP From: To: |
DNS Query FOR url 'xxx.xx.xxx' |
然而网关路由器 MAC 地址这一信息我们是不知道的,只知道其 IP 地址。网络中有一项协议为我们提供了根据 IP 地址查询 MAC 地址的功能,那就是 ARP。客户端生成下面这样一个 ARP 查询消息后,通过交换机广播出去:
链路层(MAC) | 网络层(IP) | 传输层(PORT/TYPE) | 描述(DESCRIPTION) |
---|---|---|---|
From: 客户端 MAC 地址 To: FF-FF-FF-FF-FF-FF |
From: 客户端 IP 地址 To: 网关路由器 IP 地址 |
UDP From: To: |
ARP |
网关路由器收到后,就会回复其 MAC 地址,这样客户端就知道了。
当然如果每次都这样广播一次的话倒也不是很有必要,一般情况下都会有一个 ARP Cache,每次想知道 MAC 地址的时候先去 Cache 里查一查,如果有就不用广播了。
fine,这下可以正式发送 DNS 服务器查询请求了。
- 首先搜索浏览器的 DNS 缓存,缓存中维护一张域名与 IP 地址的对应表;
- 如果没有命中,则继续搜索操作系统的 DNS 缓存;
- 如果依然没有命中,则操作系统将域名发送至本地 DNS 服务器,它查询自己的 DNS 缓存,查找成功则返回结果;
- 如果没有命中,则向上级域名服务器进行迭代查询,最终得到该域名对应的 IP 地址,返回给操作系统,操作系统又将 IP 地址返回给浏览器(这三者还会将得到的 IP 地址进行缓存);
这下所有手续齐全了,可以正式开始数据交互了。
此时此刻,我们新增的已知信息有:Web 服务器 IP 地址、默认网关 MAC 地址、DNS 服务器 MAC 地址。
TCP 三次握手建立连接
由于 HTTP 请求常常交给 TCP 协议,而 TCP 又需要保证在数据的传输之前,双方建立起了稳定的连接。首先客户端会创建一个 socket,用于唯一标识连接——毕竟可能存在多个网页同时点击该链接——同时会为客户端分配一个端口 p
,该 socket 应当长成这样:
源 IP 地址与端口 | 目的 IP 地址与端口 |
---|---|
<客户端 IP 地址, p> | <Web 服务器 IP 地址, 80> |
Web 服务器固定以 80 作为端口。
上面这些信息都是已知的,所以可以毫无阻碍的进行创建。然后协议栈生成一个连接请求发送给 Web 服务器:
链路层(MAC) | 网络层(IP) | 传输层(PORT/TYPE) | 描述(DESCRIPTION) |
---|---|---|---|
From: 客户端 MAC 地址 To: 默认网关 MAC 地址 |
From: 客户端 IP 地址 To: Web 服务器 IP 地址 |
TCP From: p To: 80 |
SYN=1 |
网关路由器收到该消息后,根据 ARP 获取到 Web 服务器将链路层头部改为:
链路层(MAC) From: 默认网关 MAC 地址
To: 下一个 MAC 地址这里的
下一个
由路由器的转发表决定。如果距离 Web 服务器之间还有若干路由器,那就根据寻路算法填下一个路由器的 MAC 地址,让其进行下一步的转发;如果直达,那就是 Web 服务器的 MAC 地址。所以其实 MAC 地址就是指明发到哪一个 Router/Host/Server。
Web 服务器收到该连接请求后,会取出传输层头部检查自己是否有这个端口(我们这个情况下是有的,而且会有一个目的 IP 和目的端口均为空的 {<null, null>, <IP, 80>} 这样一个 socket 处于监听状态),一看哦有啊,就创建一个该 socket 的副本,然后填入客户端的 IP 地址和端口,并发送一个 SYN=1, ACK=1 的回复。
这个副本并不会影响原来的 socket,因为一个 socket 是由四个要素决定的,如果不创建副本直接连接,原来那个 socket 就会被本次连接占用,那就没有 socket 进行监听,后续的所有来自其他 host/server 的连接都得排队或 abort 了。
客户端收到这条回复后,再发一个 ACK 回去表明收到了服务器发来的回复,如果一段时间内没有收到重复消息,说明这条 ACK 也被服务器收到了,连接正式确立。
进行数据传输
此时此刻,客户端与 Web 服务器各有一个 socket 被用于该连接,两边的数据传输也是基于各自的 socket 进行。所有工作做完后,对于客户端而言,就是生成 HTTP GET 请求然后不断加上协议栈各层的头部通过网卡发送出去,其中链路层头部就是客户端 MAC 地址与网关 MAC 地址,网络层与传输层的源地址与目标地址直接根据 socket 确立。经过交换机、路由器的层层转发,消息终于来到了 Web 服务器的 80 端口,然后 Web 服务器验证有效性后将消息进行拆解,发现是一条 GET 请求,遂将本地的 html 文件以二进制的形式进行答复。
客户端收到该答复后,将二进制数据交给浏览器进行渲染,显示出网页。如果还有其他图片、视频等要素,依然也是通过上面的手段进行获取。