1. 引言
上一篇文章中,我们介绍了浏览器是如何生成消息的:
网络是怎样连接的(一) -- 浏览器是如何工作的
在浏览器生成消息以后,他就要通过调用 Socket 库中的系统调用,委托操作系统协议栈将消息发送出去了,这就是我们今天这篇文章的重点内容。
2. 创建套接字
首先,浏览器要做的是调用 Sockect 库提供的 socket 系统调用,创建套接字,那么,什么是 socket 呢?
在操作系统协议栈中,维护了一块内存空间,专门用来存放用来控制通信操作的控制信息,比如 ip 地址、端口号、通信状态等等内容,Socket 库返回的 socket 就是用来索引这块内存空间的句柄。
每一个 socket 对应协议栈内一块独立的内存空间,因此,当需要让操作系统协议栈进行连接、读写等操作时,都需要在调用 Socket 系统调用时传递 socket 作为参数,从而让协议栈可以去对应的内存空间中查询当前连接的控制信息。
那么,这块内存空间是什么时候被初始化的呢?就是在我们调用 Socket 库的 connect 函数时进行的,此时,操作系统向服务端发出连接请求,并且收到服务端的相应数据,然后,按照客户端参数及服务端的相应信息对存储控制信息的内存区域进行初始化,这些控制信息主要包含:
- 各种头部信息,例如 tcp 头部、IP 头部、以太网头部等。
- 协议栈操作所需的信息。
除了 socket 对应的控制信息缓存外,协议栈还会为本次连接分配一块数据缓冲区,用来对通信过程中的数据进行缓存。
通过 netstat 命令,可以查看每一个套接字对应的具体控制信息,每个 socket 占用一行。
3. 收发数据
完成连接的创建与初始化后,我们就可以通过调用 write 系统调用在 socket 上写入数据实现数据的发送了。
3.1 数据的发送
发送数据的步骤如下:
- 协议栈将收到的数据写入发送缓冲区;
- 协议栈根据 MTU(网络包最大长度)减去头部长度,得到单个包的长度,对缓冲区中的数据进行拆分,然后将拆分后的数据逐个发送,如果达到计时器时间缓冲区中仍然不足一个包,协议栈会立即发送;
- 协议栈调用 TCP 模块创建 TCP 头部;
- TCP 模块将消息传递给 IP 模块,并委托 IP 模块进行发送;
- IP 模块添加 IP 头部和以太网头部,分别存储目的 ip 地址和下一跳路由器 MAC 地址;
- IP 模块通过查询路由表,找到关心该以太网包的网络硬件,并将封装好的以太网包交给相应的网络硬件,例如计算机网卡的驱动程序;
- 网卡驱动程序调用硬件,将以太网包转换成 0/1 组成的一连串数字信息发送出去,这些信号就会到达集线器、路由器等转发设备,再由转发设备一步一步地送达接收方。
3.2 IP 模块与以太网包的收发操作
操作系统协议栈的 IP 模块在收到数据后,会根据 IP 协议,在数据的基础上写入 20 字节的 IP 头部,其中最为核心的,是包含了目的 IP 地址。
然后,IP 模块会在 IP 包的基础上,拼接 MAC 头部,组成一个以太网包。
MAC 头部中包含的最为关键的信息就是当前报文将要发送到的下一个网络节点的物理地址,也就是下一跳路由器的 MAC 地址,那么,操作系统协议栈的 IP 模块是如何知道下一跳路由器的 MAC 地址是什么呢?
3.3 下一跳 MAC 的获取 -- ARP 协议
要想获得下一跳路由器的 MAC 地址,这就需要地址解析协议 – ARP 协议。
IP 模块拿到目的 IP 地址后,会首先在协议栈维护的一个缓存区域中查询这个 IP 地址对应的 MAC 地址,如果查询不到就会向所在子网中的每一台设备发送 ARP 广播,询问那台设备关心当前 ip 地址,此时,子网中可以处理这一 IP 地址的设备就会向发出广播的设备发送回应,报告自己的 MAC 地址,当协议栈 IP 模块接收到设备的回应时,就可以将这个 ip 地址与回应中的 mac 地址相关联,并存储在刚刚提到的那块缓存中。
这块缓存区域就是 ARP 缓存,协议栈每隔几分钟会自动清空一次 ARP 缓存,防止由于网络环境变化造成的数据不实时的问题。
3.4 传输过程
当协议栈 IP 模块获取到下一跳主机的 MAC 地址以后,生成 MAC 头部拼接在 IP 包的前面,这样就生成了一个以太网包,经过网络传输,就可以到达下一跳主机的网卡,网卡收到电信号后,转换为 0/1 组成的数字信息串交给网卡驱动程序,网卡驱动程序调用操作系统协议栈的 IP 模块。
IP 模块首先丢弃以太网头部信息,获得 IP 数据包,通过对比目标 IP 与本机 IP 可以判断当前主机是否是最终接收者,如果不是,那么就继续按照上述过程如法炮制,使用下一跳 MAC 地址生成新的以太网头部拼接在 IP 数据包前,得到新的以太网包再发送出去,直到到达最终目的 ip 所在主机。
最终的这台主机协议栈的 IP 模块会丢弃 MAC 头部与 IP 头部,获得原始数据交给 TCP 模块,TCP 模块再将数据发送给监听指定端口的应用程序,完成数据的接收工作。
4. 网卡的工作
4.1 网卡的选取
上文中,我们忽略了一个细节,那就是 IP 模块要使用本地的哪块网卡来进行通信呢?
这取决于协议栈内部维护的另一个缓存数据 -- 路由表。
通过 netstat -r 命令或者 route 命令,我们可以查看主机当前维护的路由表:
如上图所示,每一行代表了一个可用的路由选项,对于一个给定的路由项,可以打印出五种不同的标识(Flags):
- U -- 该路由项可用
- G -- 该路由是到一个网关(路由器),没有设置该位则说明目的地是直接相连的
- H -- 该路由是到一个主机,也就是说目的地址是一个完整的主机地址,没有设置该位则说明该路由是一个网络,而目的地址是一个网络地址(网络号或网络号与子网号的组合)
- D -- 该路由是重定向报文创建的
- M -- 该路由已被重定向报文修改
通过目的 IP 地址在路由表中查询,IP 模块就可以获取到表中 Iface 项所指向的本地网卡设备。
4.2 网卡的发送工作
网卡硬件设备的基本组成如图所示:
IP 模块在完成以太网包的拼装后,会将以太网包交给指定网卡的驱动程序,网卡驱动程序从 IP 模块获取到以太网包之后,就会复制到网卡内的缓冲区中,然后向网卡的 MAC 模块发出发送指令。
MAC 模块将以太网包从缓冲区中取出,在数据包的前后分别拼接上报头、分隔符和校验序列,从而形成一个互联网帧。
这里提到了报头、分隔符和校验序列:
- 报头是一串 0 与 1 交替出现的序列,长度为 56 bit,用来让接收端在这段时间内准备好接收消息。
- 报头的后面是一个 1byte 的分隔符(SFD),固定为 2 进制的 10101011,它是为了告知接收者消息的起始位置。
- 校验序列则(FCS)是通过一定的算法(通常使用 CRC32 算法)对报文内容进行计算,得到一个签名,接收者使用同样的算法生成签名并对比就可以验证包在互联网传输过程中是否有数据错误或丢失。
在此之后,MAC 模块就负责通过硬件将这个完整的互联网帧转换为电信号或光信号,发送给 MAU 模块,由 MAU 模块将电信号或光信号在网线或光纤上传送出去。
4.3 网卡的接收工作
网卡的接收工作可以看成是上述过程的逆向过程,当网卡硬件监测到互联网帧的报头和 SFD 到来时,网卡的 MAU 模块就会开始进入接收状态,他将接收到的信号发送给 MAC 模块,由 MAC 模块将电信号或光信号转换为 0/1 的数据,并存储在接收缓冲区中,当 MAC 模块完成一整个互联网帧的接收工作后,他就会检查 FCS 来确认包的内容没有在传输过程中发生紊乱,如果存在紊乱,则丢弃这个包,否则就会通过扩展总线接口中的硬件中断总线与级联在 CPU 上的中断控制器通信。
CPU 收到中断后,会立即停止当前所有的工作,根据中断号,获知这是一次网络中断,于是就会去调用协议栈中的 IP 模块接口,让 IP 模块调用网卡驱动程序,从而获取到缓冲区中的互联网帧,通过丢弃全部头部信息,获取到接收到的原始数据,一次完整的接收过程就这样完成了。