透过 Chrome 深入理解浏览器导航过程
网络的导航,是从输入 url 到最终获取到文件的过程。其中牵扯到浏览器架构、操作系统、网络等一系列知识。本文将从各个角度详细论述这一过程,涉及广度与深度。如果您是已经有一定基础的同学,那么本文可以快速带你系统化整理碎片化知识。
导航篇
本小节,我们将以普通请求作为抓手,跟随请求数据包漫游整个 OSI 模型,本节目录:
解析 URI
当我们在地址栏输入需要请求的网站地址,如:晨风
并按下回车,
Chrome 首先会解析内容,判断这是 URL
还是搜索内容,若是搜索内容则自动 URL
编码并拼接为默认搜索引擎的 params
。
如果是 URI
,如:test.com
,则处理 URI
,添加 http
并默认访问 80
端口号。
在 Chrome
层面,如果你的地址栏原本就有展示页面,那么进行上述操作后,会触发当前页面的 beforeunload
与 unload
事件。同时浏览器标签进入 loading
图标状态,新页面有两个重要的时间节点,在渲染篇会详细介绍:
interactive
:它表示浏览器已经完成了HTML parser
、Recalculate Style
、Layout Tree
、Render Tree
、draw list
等工作。complete
: 它表示浏览器已经完成页面渲染,这会替换掉本窗口原本的位图,显示最新的界面。在interactive
与complete
之间,就是渲染进程中的合成线程的工作位置,Chrome
渲染进程基于 skia 进行2D
界面元素的绘制。
早些时候一些网站会在 URI
中直接制定路径和具体的后缀的文件,如:https://www.test.com/home/index.html
。但是这所带来的诸如非法访问等安全问题与互联网业务需求的爆发式增长,人们对 Web
的安全性与效率有了更高的要求,因此引入代理服务器满足 保证安全、负载均衡、缓存代理 等需求。现代 Web 几乎都采用代理服务器以隐藏真实的资源位置。
构建请求
通过 URI Check
后,Chrome
需要为它创建 get
请求,在此之前先介绍一下 Chrome
的架构组成。
Chrome
目前采用的是 SOA
架构,主要特点是将应用程序的不同的 Service
进行拆分,并通过这些服务之间定义良好的接口和协议联系起来。常用的进程如下:
- 浏览器主进程:负责页面展示,用户交互,子进程管理等功能
- 渲染进程:每个选项卡都有自己的渲染进程,无关乎是否为 same-site 站点,
SandBox
运行环境,处理HTML
、CSS
、JavaScript
。同时V8
和Blink
也都运行在该进程中。 - 插件进程:负责插件运行,根据插件的功能决定是否运行在
Sandbox
环境 - GPU 进程:处理一些特殊的 CSS 效果
- NetWork Service:处理网络资源加载,请求响应,校验 CORS。
- Storage Service:处理对
localStorage
、sessionStorage
、cookie
、Indexed DB
存储的控制。 - Audio Service:处理音视频
Buffer
的音量播放等操作 - V8 PAC tool:利用 V8 解析 PAC 文件,干一些你懂得的事 😁。
从上文得知 Chrome
主进程需要通过 IPC
把构建请求的任务委托给 NetWork Service
负责此任务。
NetWork Service
接受任务后,创建了 get
请求,其中请求行由 请求方法 + 请求路径 + HTTP 版本号 组成;请求头信息由 Chrome 内置提供。
在 HTTP 2.x 标准中引入了
Hpack
和Stream
,其中Hpack
主要的目的是压缩请求报文头信息以减少每次链接发送的冗余数据。 它会将报文头信息整合成一张Hash Table
,并使用Huffman
编码压缩文本内容。并且请求行也被取消,其内容置入Hash Table
首部并以:
开头,以此区分请求行与请求头信息。Stream
的作用我们稍后介绍
查找强缓存
NetWork Service
会委托 Storage Service
依次在 service work cache
、memory cache
、disk cache
、push cache(HTTP2 Stream)
中寻找对应的 URI
是否有可用的强缓存,如果存在强缓存,则直接使用缓存进入浏览器解析环节,否则进入 DNS
解析。
为了方便同学们学习和验证,我把非 memory cache 的 cache 资源在 MacOS 的位置统计如下:
- Service Work Cache:
/Users/YOUR_NAME/Library/Application Support/Google/Chrome/Default/Service Worker/[CacheStorage || ScriptCache]
- Disk Cache:
/Users/YOUR_NAME/Library/Application Support/Google/Chrome/Default/Application Cache/Cache
由于 Chrome 取消了通过 chrome://cache
进行访问,因此查看这类问题时需要自行安装反编译工具查看。
一般情况下大文件会默认存放在
disk cache
中,小文件存入memory cache
。但当内存使用率较高时,需要缓解使用压力会优先放入disk cache
。
HTTP 2 提供了多路复用,头部压缩、Service Push。其中 Service Push 是唯一需要手动实现的功能,Service Push 可在某次 Stream 中返回用户端还没主动请求,但是相关的数据,以节约报文上不必要的开销。
DNS 解析
若强缓存不存在或过期时,NetWork Service
继续将报文发送至接收端。这需要 OS
的配合,首先需要将报文委托给 OS
至协议栈,但 OS
无法识别报文对应的 domain
,因此无法提供相应帮助。我们必须提供 IP
地址。将制定域名转换成 IP
的工作是由 DNS
服务器提供。
域名的诞生也是为了符合人们的习惯性记忆,没有人喜欢记忆无意义的 IP 地址。于是便有了 DNS 服务赋能 IP 对应的 Domain 以方便记忆。
DNS 层级
由于域名系统是外国人发明的,因此 DNS 的层级划分是从右往左根据 **.**
进行切分,它就像英文人名一样,根域 / 姓氏
取域名最末尾的部分,不符合国人记忆习惯。
根据层级 DNS 服务器分为:
- 根域 DNS 服务器:不保存具体的域名信息,但它是通向所有顶级域 DNS 服务器的总入口
- 顶级域 DNS 服务器:代表不同的域名后缀服务器,如
cn
、com
、tech
等。同样不保存具体的域名信息,是通往对应后缀权威 DNS 服务器的总入口 - 权威 DNS 服务器:正如其名,代表着对应 Domain 映射 IP 的权威。它是存储映射关系的真实服务器。
从上图可知,DNS 服务器之间有着类似 trie
树的结构,树的每一层的信息都是完整域名的一部分且非叶子节点的信息均是没有帮助的。而叶子节点被称为权威服务器,是 IP
与 Domain
映射关系存储的实际位置。根域 DNS 服务器的信息保存在互联网中所有的 DNS 服务器中,正因如此,客户端只需要访问到任意 DNS 服务器就可以顺着它找到根域服务器,从而获取目标 IP。
Chrome 从 83 版本开始正式开始了 DOH 即 DNS-over-HTTPS,主要目的是防止原本的 DNS 请求因为是 HTTP 明文传输导致容易被中间人篡改,因此 DOH 就是批着 TLS 的 DNS 请求。
Hosts
正如 JavaScript
的 Promise
支持 thenable
,instanceof
支持 Symbol.hasInstance
,JSON.stringify
支持 toJSON
等等都会开设一个定制化行为的入口。
域名解析也存在本地定制化入口 Hosts
。它是一个本地的关联 “数据库”,将 Domain
与 IP
地址相对应。解析优先级大于 DNS
服务。
DNS 解析流程
笔者以访问 http://www.test.com
为例,DNS 的解析流程如下:
- 查看
hosts
是否存储目标domain
和IP
地址的映射关系,若找到则直接返回给客户端。 - 若
hosts
不存在对应domain
,客户端建立DNS
请求,问询本地DNS
服务器Domain
对应的IP
地址。 - 本地
DNS
服务器收到请求后,首先查看DNS
缓存能否找到domain
对应的IP
地址,若找到则直接返回给客户端。若DNS
缓存中不存在,则找到自身记录的根域DNS
地址并发起请求问询根域服务器Domain
对应的IP
地址。 - 根域服务器不保存具体的数据,但是指明了我们接下来询问的目标:对应
com
的顶级域名服务器地址。 - 本地
DNS
服务器收到根域的回应后,继续问询com
的顶级域名服务。 - 顶级域名服务器同理会返回相对应
test.com
的权威服务器地址。 - 本地服务器继续问询权威服务器,它是域名解析结果的原出处,也是最后一次问询。
- 权威
DNS
服务器返回域名对应的IP
地址给客户端。 - 本地
DNS
服务器缓存结果。将IP
发给OS
。 OS
返回IP
至Chrome NetWork Service
。
这下 NetWork Service
拥有了绿卡,已经万事俱备。终于可通过 socket library
将数据委托给 OS
以进入协议栈啦。同时也标志着即将离开 OSI
应用层。
协议栈
请求数据包在 OS
的帮助下进入协议栈。工作在应用层与传输层中间的协议栈会处理对应 H2
的 Hpack
和 Stream
,如果 domain
使用了 TLS / SSL
协议,那么 OS
会从本地加密套件列表中选取加密套件,并将信息添加至数据包。
传输层
至此数据包来到协议簇上层,它表示工作在传输层和网络层相关的协议总称。协议簇分为上下两个部分,分别承担不同的工作且上下层关系有一定的规则,上层完成部分工作后会委托下层继续执行。在上层协议簇中最先映入眼帘的便是负责数据包收发的 TCP / UDP
TCP
TCP 是面向一对一链接,可靠有状态且基于字节流的协议。在 HTTP 传输数据之前,首先需要 TCP 建立连接,TCP 连接的建立,通常称为三次握手。
在深入介绍 TCP 前,我们得先了解 MTU
,它是一个网络包的最大长度,在以太网中一般为 1500 字节。而我们的 HTTP 数据表都昌都很有可能会大于 1500,所以要对超出的内容进行切片发送,TCP
会对报文进行切分并添加一些信息以确保每个数据包能顺利到达接收端。TCP
数据的最大长度为 MSS
,它通过 MTU
- TCP head
- IP head
计算而来。至此我们介绍下 TCP Head
具体添加的信息。
源端口、目标端口
首先是一组端口号,如果没有它们,数据包到端后不知道自己是属于哪个端口应用的数据。
同时,我们也通过源 IP、源端口、目标 IP 和目标端口组成唯一的标识。
这时候可能有同学提出疑问,那么浏览器打开多个页签时,如果访问的域名和端口也一样,数据如何对应正确的标签?笔者推断 Chrome 可能通过 TCP 的 timestamp 或 ISN 进行识别,若有同学能提供准确的答案欢迎指出。
Sequence Number
简称 seq
,它代表本报文段第一个字节的序列号,序列号是一个长为 4 个字节,也就是能够表示 32 位的无符号整数。如果到达最大值了后就循环到 0。它主要有以下几个作用
- 确保端具有发送功能的标志。
- 初次发送 SYN 报文时交换 ISN。
- 确保被切分的数据包以正确的顺序组装。
ISN 即 Initial Sequence Number,通过三次握手中的前两次握手进行交换,其目的是防止不法分子得知 ISN 后伪造 IP 和 Port 通过 TCP 标志位对链接进行非法攻击。由于现在的 ISN 并不是一个固定的值,而是每 4 ms 加一,溢出则回到 0。从而大大提高了攻击者猜测 ISN 的难度。
Acknowledgment Number
简称 ack
,和 seq
一样,占有 4 个字节,具体表示小于此字节的内容均已收到。它主要有以下几个作用
- 确保端具有接收能力
- 告知发送端期望下一次发送的数据起始位置
标记位
根据 TCP 报文处理信息的类别不同,需要给予一定的标识,这就是标记位。
常见的标记位有 SYN
,ACK
,FIN
,RST
,PSH
。这方面比较基础,不清楚的同学可以结合三次握手去详细了解。
窗口大小
它赋能 TCP 做流量控制,通信双方各声明一个窗口(缓存大小),标识自己当前能够的处理能力。这也被称为初始化窗口。
除了用滑动窗口做流量控制以外,TCP 还会通过拥塞窗口做拥塞控制,通过初始化的拥塞窗口采取慢启动、快速重传和快速恢复、拥塞避免等能力。
校验和
占用两个字节,防止传输过程中数据包有损坏,如果遇到校验和有差错的报文,TCP
则直接丢弃,通过使返回的 ack
值保持不变以提醒发送端需要重传。
紧急指针
这是为了应对一些应用程序在某些紧急情况下(如在某些连接中进行强制中断)
要求在接收方在没有处理完数据之前就能够发送一些紧急数据。
选项
这是 TCP 中的可选项,其中比较重要的是
- TimeStamp:
TCP
时间戳,解决RTT
错乱与序列号回绕 - MSS:前文提过,通过
MTU
-TCP head
-IP head
计算而来。
回到三次握手,所谓的建立连接
,只是双方计算机里维护一个状态机,在连接建立的过程中,双方的状态从 close → established
。通过三次握手的 SYN
,ACK
传递确保双方的发送接收能力。
在 Linux 中我们可以通过 netstat -napt
来查看 TCP 链接状态:
tcp 0 0 0.0.0.0:5440 0.0.0.0:* LISTEN 9138/java
tcp 1070 0 199.161.10.251:9020 199.161.10.251:34512 CLOSE_WAIT 4122/java
tcp 1 0 199.161.10.251:60254 199.161.100.195:38399 CLOSE_WAIT 7377/java
tcp 1076 0 199.161.10.251:9020 199.161.10.251:34540 CLOSE_WAIT 4122/java
tcp 416 0 199.161.10.251:9020 199.161.10.251:39166 CLOSE_WAIT 4122/java
tcp 0 0 199.161.10.251:36956 199.161.10.116:22 ESTABLISHED 7377/java
在实际的网络环境中,此时数据包传输被阻塞,先和对端完成三次握手,之后才继续发送数据。这也是人们在 HTTP3.0 前,称 HTTP 是基于 TCP 的主要原因,同时从此处可以看出** TCP 的队头阻塞**是个不可避免的问题。
TCP 还提供了 keep-alive 功能,但是非常鸡肋。
UDP
UCP 是面向无连接,一对多发送且无状态的协议。
由于 TCP 的先入为主且其可靠性经受住了历史的考验,让我们很容易相信它会一直保持 Web 端传输层的主导地位,然而 Google 团队的开创能力,也再一次让笔者大开眼界。HTTP 3.0 标准将抛弃 TCP。 UDP 成功获得主导地位。
主要的原因想必大家早已略有耳闻,TCP
链接必须经历三次握手,即便你使用了 TFO (TCP Fast Open)
也一样。如果需要提高数据交互的安全性,既增加传输层安全协议(TLS),在确保安全的 Session Ticket
优化方案下也需要增加 1 RTT
。我们不考虑 PSK
,因为它不安全。总之, TCP 协议连接建立的成本相对较高,由于 TCP 是在操作系统内核和中间件固件(上文所提的协议栈)中实现的,因此对 TCP 进行重大更改几乎是不可能的。
而 UDP 协议是无连接协议。客户端发出 UDP 数据包后,只能“假设”这个数据包已经被服务端接收。好处是在传输层无需对数据包进行校验,一般用于网络游戏、流媒体数据的一些传输。与之相对的,如果需要确保数据传输的可靠性,应用层协议需要自己对包传输情况进行确认。此协议就是 QUIC
QUIC 协议是基于 UDP 的低时延的互联网传输层协议。 HTTP 2.0 解决了由 HTTP 引起的队头阻塞问题,但更深层次的 TCP 队头阻塞问题无法避免,QUIC 基于 UDP 协议,因此彻底解决了所有队头阻塞问题。
QUIC 协议根据连接的服务器是新的还是已知的,可在 1-2 个 RTT 内完成连接的创建(包括支持 TLS),这具备很高的诱惑力。
QUIC 虽然有诸多优势,但目前仍未达到大量普及的阶段,并且目前部分路由会封杀 QUIC 所在的 443 端口,UDP 包过多让服务商误以为是攻击、防火墙对 QUIC 的支持等均未到位。让我们一起期待 QUIC 协议规范能够成为终稿并实现推广的那天。
本文主要还是以目前主流的 TCP 为主,经过 TCP 包头后,当前数据包如下:
TCP / IP 协议簇下层
在传输层执行连接、收发、断开等各阶段操作都需要委托 IP 协议将数据包封装成网络包发送给通信对象。下面我们来看看 IP 报文头部的格式
其中最重要的是源 IP 与目标 IP。
- 源 IP 即当前客户端的 IP 地址
- 目标 IP 为 DNS 域名解析得到的接收端服务器 IP
其次是 IP Header 中的协议号,表示传输层使用的协议。以十六进制表示。例如 06 表示的是 TCP。经过 IP Header 包装后,如下:
路由表
通过 IP Head,我们知晓了接收数据的目标 IP,但我们不能确定这个 IP 距离我们地址位置有多远,很多时候或许我们无法直接发送到对端,而是要在网关中进行数次中转,控制此过程就是根据路由表的规则。
在 Linux 系统可以根据 route -n
查看当前系统的路由表。
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 199.161.100.1 0.0.0.0 UG 100 0 0 eth0
199.254.169.254 199.161.100.153 255.255.255.255 UGH 100 0 0 eth0
199.161.100.0 0.0.0.0 255.255.252.0 U 100 0 0 eth0
此步也是网关是否介入链接过程的关键分歧点。来看看路由表是如何工作的。
- 首先根据路由表 list 依次取出每一条信息
- 根据每条信息中的子网掩码(Genmask)与接收方的目标 IP 进行与运算,如果结果和 Destination 匹配,说明与我们通信的对端处在同一以太网中,接下去的发送不需要走网关。并确定以当前的 IP 作为 IP 包头地址。
- 若以上匹配都失败了,那么会匹配到默认网关,一般这就是路由器的 IP,最后网络包会转发给路由器,让路由器帮忙发送。
ARP
生成 IP 包头后,网络包还需要加上 MAC 包头,IP 的诞生是为了更加方便的管理计算机在各类以太网中的身份。而连入所有网络的每一个计算机都会有网卡接口,每一个网卡都会有一个唯一的地址,这个地址就叫做 MAC 地址。计算机之间的数据传送,就是通过 MAC 地址来唯一寻找、传送的。MAC 包头的结构如下:
其中发送方的 MAC 非常容易确认,因为 MAC 在网卡生产过程中已经写入 ROM 中,直接读取此值写入 MAC 头部即可。
接收端的 MAC 地址相对复杂一些,当前我们已经知晓了接收方的 IP,通过子网掩码,我们能把接收端分为两类
- 处于同一子网的邻居
- 处于外部子网的通信对象,我们交给居委会大妈(网关)去通信
可以看得出来,不论通讯对象是否是邻居,我们首次发送的对象都是同一子网的,也许是邻居服务器也许是网关。因此我们使用广播进行问询目标的 MAC 地址。
广播
ARP 协议会在以太网中以广播的形式,对以太网所有的设备问话路由表匹配的目标 IP 地址对应的 MAC 地址。
就像操场的喊话,所有人都可以听见,但如果喊话对象不是自己,不再回应就是。被喊话的对象听见后,以 MAC 地址作为回应。
ARP 缓存
正如大部分服务一样,ARP 也有自己的缓存系统,以空间换时间提高效率。获取到 MAC 地址后,OS
会把本次查询结果放到一块叫做 ARP 缓存的内存空间留着以后用,不过缓存的时间就几分钟。
也就是说,在构建 MAC 包头时:
- 先查询 ARP 缓存,如果其中已经保存了对方的 MAC 地址,就不需要发送 ARP 广播查询,直接使用 ARP 缓存中的地址。
- 而当 ARP 缓存中不存在对方 MAC 地址时,则发送 ARP 广播查询。
linux 中可以使用 arp -a
查看 ARP 缓存的内容
gateway (199.161.100.1) at 79:2c:29:11:0a:32 [ether] on eth0
? (199.161.100.251) at ff:91:13:17:a0:00 [ether] on eth0
? (199.161.101.189) at ff:63:8a:1f:83:00 [ether] on eth0
? (199.161.100.153) at b3:ab:ef:43:1d:40 [ether] on eth0
得到接收方 MAC 地址后,读取自身网卡 ROM 的 MAC 地址,塞入 MAC 头,目前的数据包呈现为:
ICMP
ICMP 协议是 IP 的一个组成部分,必须由每个IP模块实现。
主要用于在 IP 主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。
网卡与驱动
通过协议栈生成的网络包只是内存中的一串二进制信息,还是无法直接进行发送。需要将数字信息转换成电信号,计算机底层实际上也是各种逻辑电路的组合,通过晶体管等硬件改变高低电压。转换成数字信号后,就可以在网线上进行传输。这就是数据真正的发送过程。同时网卡负责的部分也被称为 Ethernet Frame
网卡负责执行这一操作,但是要控制网卡,必须依靠网卡驱动程序,它内置了网卡行为的一些方法。具体步骤如下:
- 网卡驱动从 IP 模块获取到网络包后,会复制其二进制信息至网卡内的缓存区。为了区分这一段段的数据,我们需要一套规则来传输二进制,比如多少电信号为一组,如何识别开头和结尾等等。
- 因此我们在二进制信息的起始位置添加报头和二进制帧的起始分解符,用来表示包的起始位置
- 在数据包结尾加上 FCS,也被称为帧校验序列,检查包在传输过程中是否损坏。
- 最后网卡将包转换为电信号,通过网线和光纤等物理介质进行传输。
最后整个数据帧呈现如下图:
电信号护航者
中继器
由于电信号在传输过程中会不断衰减,为了不让信号衰减对通信质量产生影响,产生了中继器。它仅做放大信号作用,能把信号传导偏远的地方。
集线器
我们假设参与网络链接的双方都只有一个网络接口,那么只能够建立一对一的通信,从前文也可以发现,我们需要有广播这样一对多的场景,广播也就是将信号进行复制,这就是集线器的作用,并且可以将电信号整形再放大。它工作于物理层。
顺便提一下它和交换机的区别,它没有交换机的智能记忆和学习能力,也不具备交换机所具有的 MAC 地址表。它发送数据时都是没有针对性的,可以说它就是广播发送的代名词。
网桥
自从有集线器以来它解决了一对多的效率问题,同时也带来了问题,在真实的网络环境中,很有可能有多个集线器连接在一起,但由于是用来做广播通信,会互相冲突,因此我们需要能够有效隔离各个子网,这就是网桥。名称也非常形象。它位于数据链路层,而集线器是在物理层。因此它可以有效的控制让广播通信仅仅在于一个局部,局部和局部中间用网桥连接。
网桥原理
现在我们来介绍下网桥是如何解决广播冲突的。网桥只有两个端口,连接两个端口的网络被切分成 A、B 两个子网,网桥内部会为每个子网维护一张表,一开始表是空的,网桥会分别根据 A、B 子网发送的数据包,并解开 MAC 头部获取源 MAC 地址,并记录在对应的表中,并转发给另一子网。工作一段时候后几乎可以记录下 A、B 子网中所有的机器的 MAC 地址。此时假设网桥接收 A 子网的数据包,它还是会拆解 MAC 头部查看接收端 MAC 地址。如果发现 A 表已经记录了此 MAC 地址,说明这不需要广播给 B 子网,A 子网内就可以解决,网关会丢弃此数据包,如果 A 表不存在接口端 MAC 地址,则转发给 B 子网,再查看源 MAC 地址,如果不存在则继续补充在 A 表上。到此就彻底解决了因集线器整形扩大数据包后子网间广播冲突的问题。
在实际环境中,网桥内部不一定有两个子表,也可能是收集在一起的,具体要看内部实现决定。
交换机
网桥是切分一个局域网一分为二,也解决了广播冲突问题,但历史的车轮总在向前,由于网桥是数据链路层的广播通信,A 和 B 通信的时候,C 和 D 就没法通信。就像一座小桥负载有限,无法让多人一起通过。为了能够实现多对多的通信,于是多端口的网桥诞生了,这就是交换机。
电信号与交换机
我们回到正轨,网卡根据以太网协议给二进制数据添加起始符和 FCS,并转换为电信号进行发送。
之后电信号通过网线到达交换机网线接口,交换机内模块接收后会将电信号转换为数字信号,数字信号表示让信息参数在给定范围内表现的更加连续,而不是离散。与之相对的是模拟信号。
交换机的一个重要的作用就是确保数据包能够原样的转发到目的地。他会拆解以太网头部获取 FCS 校验错误,如果数据没问题则进入交换机缓冲区,之后部分基本和之前网卡的概念相同,但是工作方式和网卡不一样,因为网卡的 ROM 中有 MAC 地址,而交换机没有。取而代之的是交换机会维护一张 MAC 地址表。地址表主要包含两个信息:
- 记录下接收方 MAC 地址的信息
- 记录下此接收方的设备链接在交换机的哪个端口上。
细心的同学应该发现了,这部分和网桥非常相似,只是网桥只有两个端口,通过拆表记录,可以不记录端口位置信息。如果目前的数据包和 MAC 表上记录的 MAC 地址匹配上了,就会直接转发到对应的端口。如果找不到指定的 MAC 地址,很可能此地址背后的设备还没有和交换机发送过包,或者因为持续没有工作,导致交换机把它从地址表中删除了。此时只能和广播一样,发送给所有的端口,前文也提过,在同一以太网中,设计之初就是以广播的形式发送给整个网络的所有设备,只有接收者才会接收包,其他设备会忽略。接收方返回响应后,交换机会对其 MAC 地址进行记录。
除了没有记录的 MAC 地址会转发到除了源端口外的所有端口外,如果接收地址满足广播地址,也会触发同样的行为,常见的广播地址有:
- MAC 地址的 FF:FF:FF:FF:FF:FF
- IP 地址的 255.255.255.255
网关
前文讲述路由表时提过,如果没有比配到默认网关的情况下,是可能不需要网关的,因此我们假设之前的接收方 IP 地址匹配到了默认网关。
默认网关一般就是路由器的别称,到达路由器时也可以比作高速路的关卡,数据包准备离开子网了。下文我们以路由器代指网关。
路由器,也被称为三层网络设备,路由器每个端口都有 MAC 地址和 IP 地址。所以它可以作为以太网的发送和接收端,从此角度来看,它和网卡是一样的。我们来看看路由器的工作流程:
- 路由器会拆解以太网首部,验证 FCS 校验,如果没有问题进入下一步
- 接下来拆解 MAC 首部,查看接收方的 MAC 地址是否是自己,不是自己就丢弃数据包
- 如果是发送给自己的包,此 MAC 首部的任务彻底完成,便完全删掉 MAC 头部。继续拆解 IP 首部,读取到 IP 地址。
- 接着查询自身的路由表,这和 IP 层查询路由表的操作一致,先验证子网掩码,再看具体的子网 IP
- 如果网关为空,则表示对应的 IP 地址就是目标地址,已经抵达终点。
- 如果没有匹配上路由表的子网,说明还未抵达终点,继续把数据包转发给路由器的默认网关
递归至对端
路由器会根据查询到的默认网关 IP ,通过 ARP 获取 MAC 地址,并且也具有 ARP 缓存,查询到 MAC 地址后,给数据包新增 MAC 头,之后数据包加上以太网首部,通过端口转发给其他网关。虽然路由器读取了 IP 包的目标 IP,但是发送方和接收方的 IP 地址是永远不会被改变的。
转发到其他网关后还会递归这些步骤,进行网关到网关的中转,直到抵达对端 IP。
到达接收端后会依次去除以太网首部、MAC 首部、IP 首部、TCP 首部,最后读取 HTTP 信息。到此 test.com
成功接收到了 get 请求,向我们发送资源文件。
服务器的 HTTP 进程看到,原来这个请求是要访问一个页面,于是就把这个网页文件封装在 HTTP 响应报文里。
HTTP 响应报文也需要穿上 TCP、IP、MAC 头部,不过这次是源地址是服务器 IP 地址,目的地址是客户端 IP 地址。
套上各种首部后,再次从网卡发送出去,只交换机转发到网关路由,路由器就把响应数据包发到了下一个路由器,接着递归过程,直到跳到了客户端的路由器,路由器扒开 IP 头部发现确实是给本子网的信息,于是把包发给了子网交换机,再由交换机转发到我们一开始的发送端。
发送端 OS
收到了服务器的响应数据包后,去除各种头部,拿到最后 HTTP 的响应报文,通过 IPC
将包发给 Network Service
。
Network Service
收到报文后判断响应状态码,还记得我们一开始的访问地址吗? test.com
因此返回了 301
状态码,我们可以通过 curl -I test.com
查看:
HTTP/1.1 301 Moved Permanently
Server: nginx/1.18.0
Date: Tue, 07 Sep 2021 03:21:49 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Keep-Alive: timeout=20
X-DIS-Request-ID: 241ea10b621b0644e9844c0f52ef76e1
Location: http://www.test.com/
此时 Network Service
会自动构建新的请求,请求的目标为响应报文的 location
,那么回到本文构建请求的环节,重新走一遍整个流程,值得注意的是现代浏览器会默认开启 connection: keep-alive
这会复用之前建立的 TCP 链接,加快请求速度。直到 Network Service
再次收到响应。
本次收到响应后,状态码正常,接着查看响应头部 content-type
。它是 MIME
的子集。若内容无法解析,浏览器会启动自动下载,如果为 text/html
,就正式进入编译篇。可以通过 curl -i [https://www.test.com/](https://www.test.com/)
查看:
HTTP/1.1 200
Server: nginx/1.18.0
Date: Tue, 07 Sep 2021 03:39:19 GMT
Content-Type: text/html
Content-Length: 8859
Connection: keep-alive
Keep-Alive: timeout=20
ETag: "5e53086c-229b"
X-DIS-Request-ID: c821b8e4044843e8855e76558a610532
Set-Cookie: dis-request-id=c821b8e4044843e8855e76558a610532; secure
Set-Cookie: dis-timestamp=2021-09-06T20:39:19-07:00; secure
Set-Cookie: dis-remote-addr=61.175.192.50; secure
X-Frame-Options: sameorigin
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
...
最后,如果客户端要离开了,向服务器发起了 TCP 四次挥手,至此双方的连接就断开了。
笔者在到端后省略了 TCP 首次到端后进行的二次握手,QUIC 以及 TLS 的校验等工作。它们在 HTTP 首次响应前就被完成。
网络的导航,是从输入 url 到最终获取到文件的过程。其中牵扯到浏览器架构、操作系统、网络等一系列知识。本文将从各个角度详细论述这一过程,涉及广度与深度。如果您是已经有一定基础的同学,那么本文可以快速带你系统化整理碎片化知识。