第二章 探索协议栈和网卡
- 这章主要讲的是在收发数据过程中的具体工作流程,比如怎么创建套接字、怎么建立连接、怎么将数据发送出去等等。
2.1 创建套接字
- 协议栈的内部结构:

浏览器、邮件等一般应用程序收发数据时用TCP;DNS查询等收发较短的控制数据时用UDP。
- 到底啥是套接字?
套接字是实体就是通信控制信息。协议栈的内部有一块空间用于存放控制信息的内存空间,记录了控制通信操作的控制信息,其实体就是套接字。举个栗子,使用netstat命令可以显示套接字:

- 调用socket来创建套接字时到底发生了什么?
首先看看消息收发操作的整个流程:

消息收发流程中,先调用socket创建套接字,其本质就是协议栈为改套接字分配一块内存空间,用于存放控制信息,并向其中写入初始状态。接着确定一个该套接字的唯一标识,并将该标识告诉应用程序,这样应用程序就知道该和谁进行通信了。
2.2 连接服务器
- 什么是连接?
创建套接字之后,应用程序(浏览器)就会调用 connect,随后协议栈会将本地的套接字与服务器的套接字进行连接。连接就是让客户端也就是应用程序和服务器端知道该和谁通信,这些信息都是保存在套接字中的。
有关控制信息:
- 第一类是客户端和服务器相互联络时交换的控制信息。该信息不仅在连接时需要,在收发和断开阶段都需要,这些内容在TCP协议的规格中定义,如下表。

这些信息会放在网络包的开头,如下图:

当处于连接阶段网络包还没有数据的时候,网络包长这样:

- 第二类,保存在套接字中,用来控制协议栈操作的信息,协议栈会根据这些信息来执行每一步的操作。就是第一节中的套接字。
连接操作的实际过程,调用Socket库中的connect:
- 首先,客户端创建一个包,其中包含表示开始数据收发操作的控制信息的头部,就是上面那张表中的信息。重点是发送方和接收方的端口号,这样客户端就能知道连接服务器端的哪个套接字了。
- 然后,将头部控制位中的SYN比特设置为1,用来表示进行来连接操作。此外,还要设置序号和窗口大小。
- TCP头部创建好后,TCP模块将信息传递给IP模块,委托它进行发送。IP模块将网络包发送给服务器端。
- 服务器端IP模块将接收到的数据传递给服务器端的TCP模块,TCP模块根据TCP头部的信息找到端口号对应的套接字,并且修改该套接字为正在连接的状态。
- 上述完成后,服务器端的TCP模块会返回响应,整个过程和客户端一样,设置TCP头部的控制信息,比如双方的端口号和SYN比特,以及将ACK控制位设置为1。然后委托IP模块进行发送。
- 然后,客户端就会收到该网络包,通过IP模块到达TCP模块。通过TCP头部信息判断连接操作是否成功,如果SYN为1表示成功,那么客户端就像套接字中写入服务器的IP、端口等信息,将状态改为连接完毕。至此,客户端的连接操作完毕。
- 最后一步,刚刚服务器端返回响应时将ACK设置为1,相应地,客户端也需要将ACK设置为1发给服务器,代表服务器刚刚发的包已经收到。当服务器收到这个返回包之后,连接操作才算全部完成。
2.3 收发数据
- 建立连接之后,下一步就该收发数据了。主要就是应用程序把要发送的数据通过调用write先交给协议栈,协议栈收到数据后再执行发送操作。
- 协议栈发送数据时:
- 首先,协议栈不关心数据中到底是啥,一切都只是二进制字节序列。
- 其次,协议栈并不是一收到数据就马上发送,先放在内部的发送缓冲区,累计到一定量再发出去,根据以下两个要素判断,二者不可得兼。
- 第一,每个网络包能容纳的数据长度。
- 第二,时间,应用程序发送数据的频率。
- 对较大的数据进行拆分:
当要发送的数据超过一个网络包能容纳的最大数据时,就要对较大数据进行拆分。对拆分出来的数据,加上TCP头部,设置发送方和接收方的端口号,然后交给IP模块进行发送。

- 使用ACK号确认网络包已收到:
原理如下图:

序号用于表明当前从第几个字节开始发送,长度表明发送的数据包有多长,ACK号表明下一次数据该从第多少字节发送,在这之前的数据已经收到。
然而实际情况要更复杂一点。实际上序号并不会从1开始,而是用随机数计算一个初始值。如果序号从1开始,通信过程是可以预测的,很不安全。
当数据双向传输的时候原理如下图:

实际工作过程如下:

如果对方没有返回某些包对应的ACK号,那么就重新发送这些包。
- 根据网络包平均往返时间调整 ACK 号等待时间:
当网络堵塞的时候,ACK号返回就会变慢,就需要将等待时间设置的稍长,但是也不能过长也不能过短。TCP采用动态调整等待时间的方法,这个等待时间是根据 ACK 号返回所需的时间来判断的。具体来说,TCP 会在发送数据的过程中持续测量 ACK 号的返回时间,如果 ACK 号返回变慢,则相应延长等待时间;相对地,如果 ACK 号马上就能返回,则相应缩短等待时间。
- 使用窗口有效管理 ACK 号:
发送一个包就等待一个包的ACK号的方式虽然简单,但是在等待ACK的时候什么也不做,效率太低,于是采用滑动窗口的方式来操作,如下图:

但是有个问题,如果发送方发送的太快,接收方处理不过来咋办?
接收方的TCP收到包后先将包放到接收缓冲区中,如果缓冲区满,那么发来的数据就丢失了。所以接收方应该先告诉发送方自己最多能接收多少数据。思路如下图:

接收方能够接受的最大数据量称为窗口大小,一般和缓冲区大小一致。
- ACK 与窗口的合并:
发送方可以根据自己发送出去的数据计算窗口减小了多少,那么增大呢?当接收方的应用程序从缓冲区中取出数据时,窗口应该增大。而ACK号是当接收方收到数据时计算,然后发送给发送方的。要是分开发送这两个参数给发送方,效率太低,所以要想办法结合在一起发送。
并且当有多个ACK号要发送时,只需要发送最后一个即可;有多个窗口更新要发送时只需要发送最后一个即可。
- 接收 HTTP 响应消息:
下面来讲讲接收响应。
首先,调用read程序来获取响应消息。然后该协议栈上场了,协议栈尝试从接收缓冲区中取出数据传递给应用程序,如果没有数据就i将应用程序挂起。
接收数据和发送数据比较相似,总结如下:
首先,协议栈检查收到的数据块和TCP头部的内容,判断是否有数据丢失,没有则返回ACK号。然后,协议栈将数据块暂存到接收缓冲区中,并将数据块连接起来还原出原始书数据。最后交给应用程序。
2.4 从服务器断开并删除套接字
- 双方都可以首先发起断开连接,下面以服务器乙方发起断开过程为例:

第一,服务器方的应用程序会调用Socket库的close程序。然后协议栈登场,生成包含断开信息的TCP头部,也就是将控制位的FIN比特设置为1。接着,TPC模块委托IP模块向客户端发送数据,同时,服务器的套接字记录断开的相关信息。
第二,就是客户端,返回ACK号表明收到了上一个有断开信息的包。
第三,客户端调用close,之后的过程就和第一步一样了。
第四,同第二步。
- 删除套接字:
通信结束后,等待一段时间就删除套接字。之所以等待,是为了防止误操作。
- 至此,整个连接、收发、断开过程如下:

2.5 IP 与以太网的包收发操作
- 包的基本知识:
网络包的结构:头部就相当于快递的快递单,数据就相当于快递的货物。

一个包发往目的地的过程:

发送方和接收方统称为终端节点。
对于转发设备:1.路由器根据目标地址判断下一个路由器的位置 2.集线器在子网中将网络包传输到下一个路由。
实际上,集线器是按照以太网规则传输包的设备,而路由器是按照IP规则传输包的设备,因此:1. IP 协议根据目标地址判断下一个 IP 转发设备的位置 2. 子网中的以太网协议将包传输到下一个转发设备
TCP/IP包:

其中,包含两个头部:MAC头部(用于以太网协议),IP头部(用于IP协议)。对于IP协议,发送方将包的目的地也就是要访问的服务器的IP地址写入IP头部,IP协议就可以根据这一地址查找包的传输方向,找到下一个路由器的位置。接下来,IP 协议会委托以太网协议将包传输过去。这时,IP 协议会查找下一个路由器的以太网地址(MAC 地址),并将这个地址写入 MAC 头部中。这样一来,以太网协议就知道要将这个包发到哪一个路由器上了。

- 包收发操作的整体过程:

- 生成包含接收方 IP 地址的 IP 头部:

最重要的头部是IP地址,这是根据网卡来决定的。那么该如何判断应该把包交给哪块网卡呢?用路由表,具体在第三章再说。
- 生成以太网用的 MAC 头部
MAC 头部是以太网使用的头部,它包含了接收方和发送方的 MAC 地址等信息。

发送方的MAC地址是网卡生产时写入ROM里的,只要读出来即可。对于接收方的MAC地址,是根据接收方的IP地址查询到的。那么怎么查询呢?
- 通过 ARP 查询目标路由器的 MAC 地址:

广播:把包发送给连接在同一以太网中的所有设备。
ARP(Address Resolution Protocol,地址解析协议)就是利用广播,对所有设备提问:“×× 这个 IP 地址是谁的?请把你的 MAC 地址告诉我。”然后就会有人回答:“这个 IP 地址是我的,我的 MAC 地址是××××”。

同时设置MAC地址缓存,持续时间几分钟。
- 以太网的基本知识:
原型:谁都可以接收

改进后:还是谁都可以接收

再改进:只有特定设备可以接收

可以认为,具备以下三个性质的就是以太网:1. 将包发送到MAC头部的接收方MAC地址代表的目的地;2. 用发送方地址识别发送方;3. 用以太类型识别包的内容
- 将 IP 包转换成电或光信号发送出去:
最终到底是怎么把数据发出去的?需要将数字信息转换为光电信号,才能在网线上传输。
负责执行上述操作的就是网卡,而网卡必须要有网卡驱动程序来控制。网卡的概念结构如下图:

网卡的 ROM 中保存着全世界唯一的 MAC 地址,这是在生产网卡时写入的。
网卡中保存的 MAC 地址会由网卡驱动程序读取并分配给 MAC模块。
在启动时,驱动程序初始化,在MAC模块中设置MAC地址,网卡就可以等待来自IP的委托了。
- 如何将包转换成电信号并发送到网线中?
首先,MAC 模块会将包从缓冲区中取出,并在开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列。

报头和起始帧分节符:

报头是一串像 10101010…这样 1 和 0 交替出现的比特序列,长度为 56比特,它的作用是确定包的读取时机。起始帧分界符是一个用来表示包起始位置的标记。
末尾的 FCS(帧校验序列)用来检查包传输过程中因噪声导致的波形紊乱、数据错误,它是一串 32 比特的序列,是通过一个公式对包中从头到尾的所有内容进行计算而得出来的。
略去如何根据电信号读取数据…
- 向集线器发送网络包
加上报头、起始帧分界符和 FCS 之后,我们就可以将包用电信号通过网线发送出去了。使用网卡中的PHY(MAU)模块。
- 接收返回包:
首先,PHY(MAU)模块会将信号转换成通用格式并发送给 MAC 模块,MAC 模块再头开始将信号转换为数字信息,并存放到缓冲区中。当到达信号的末尾时,还需要检查 FCS。如果 FCS 校验没有问题,接下来就要看一下 MAC 头部中接收方MAC 地址与网卡在初始化时分配给自己的 MAC 地址是否一致,以判断这个包是不是发给自己的。接下来网卡会通知计算机收到了一个包。
网卡驱动程序发起中断,该中断被处理程序处理后,会从缓冲区取出收到的包。根据MAC头部中的以太类型判断协议栈的类型,然后发送给响应协议栈。
- 将服务器的响应包从 IP 传递给 TCP
现在来到了协议栈中,IP模块检查包的IP头部,确认格式,接着确定接收方IP地址是不是客户端网卡地址。如果一切没问题,IP模块处理分片的包,当属于同一个包的所有分片都到了,将其拼成完整的包然后交给TCP模块。
TCP模块根据TCP头部信息来查找对应的套接字,找到对应的套接字之后,就可以根据套接字中记录的通信状态,执行相应的操作了。例如,如果包的内容是应用程序数据,则返回确认接收的包,并将数据放入缓冲区,等待应用程序来读取;如果是建立或断开连接的控制包,则返回相应的响应控制包,并告知应用程序建立和断开连接的操作状态。
2.6 UDP 协议的收发操作
不需要重发的数据用 UDP 发送更高效
控制用的短数据
音频和视频数据
以上情况比较适合用UDP协议来发送。