前言

这个系列的博客是针对阅读《网络是怎样连接的》([日] 户根勤, 译 [中] 周自恒, ISBN: 9787115441249)一书的记录与思路整理。
有些概念是做纯路由交换的网络工程师很少接触但又很重要的部分,因此整理到这里以做分享。

将IP 包发送出去

IP 生成的网络包只是存放在计算机内存中的一串数字信息,没有办法直接发送给接收方。因此,需要将数字信号转换成电或光信号,才能在网线或光纤上传输。

负责这一操作的是网卡,在操作系统中需要配置网卡需要的网卡驱动程序,操作系统才能够正确地调度网卡的硬件资源来完成电或光信号的转换。

网卡内部逻辑结构图(实际结构随厂商和型号的不同而不同)

硬件设备初始化

网卡和其它硬件一样,并非通电后就可以直接工作的。在设备开机之后,网卡驱动程序会对硬件进行初始化操作,然后硬件才进入可以使用的状态。

操作包括了:

  • 硬件错误检查(通用硬件操作)
  • 初始设置(通用硬件操作)
  • 在控制以太网收发操作的MAC (Media Access Control)模块中设置MAC 地址(网卡独有)

网卡的ROM 中保存着全世界唯一的MAC 地址,这是在生产网卡时写入的,将这个值读取并写入MAC 模块的设置中,MAC 模块就知道自己对应的MAC 地址了。当然也可以通过一些特殊的方法,比如从命令或者配置文件中读取MAC 地址并分配给MAC 模块。这种情况下,网卡会忽略ROM中的MAC地址。

“有人认为在网卡通电之后,ROM 中的 MAC 地址就自动生效了,其实不然,真正生效的是网卡驱动进行初始化时在 MAC 模块中设置的那个 MAC 地址 。在操作系统启动并完成这些初始化操作之后,网卡就可以等待来自 IP 的委托了。”
摘录来自: [日] 户根勤. “网络是怎样连接的。”

给网络包再加3个控制数据

哪3个控制数据

  • 报头(帧头)
    • 由7个“1010”电信号构成的序列,一共7bytes
  • SFD(起始帧分界符)
    • 由“1011”电信号构成,1bytes。告诉接收方这是数据开始的地方。
  • FCS(帧校验序列)
    • 用来检查包传输过程中因噪音等因素导致的波形紊乱、数据错误,4bytes 的序列。通过循环冗余校验计算得出

数字信号转换成电信号

需要让“0”和“1”两种比特分别对应特定的电压和电流即可,这样电信号就可以表达数字信号了。

将电信号转换成数字信号

通过电信号读取数据的过程就是将电压和电流重新转换成“0”和“1”的过程。我们可以通过测量电压和电流来判断,电信号表达的是数字信号中的“1”还是“0”。如下图:

但这样会存在一个问题:如果出现连续的“1”或者“0”的信号时,电压和电流会在一段时间保持一致,我们该如何确定这一段持续的时间内的电压和电流表示了多少个连续的“1”或者“0”呢?

因此,我们必须判断出每个比特在电流中的界限在哪里,就像上图中每个比特之间都用一个虚线作出了间隔,这样就能解决遇到连续的“0”和“1”的问题了。那么在实际的工程应用中,这个虚线肯定是不存在的,最简单的方法是在数据信号之外再发送一组用来区比特间隔的信号,即“时钟信号”。那么,在接收方读取电信号的时候,就可以规定当“时钟信号”从下往上时(也可以规定当 “时钟信号” 从上往下时),读取“数据信号”的电压和电流值,并转换成一个比特的数字信号。如下图:
数据信号与时钟信号同时发送

在这里有另一个问题需要考虑,当线路过长时,两条线路的长度对于光速(无论是光纤还是电缆)都是有差异的,也就是数据传输和时钟传输会有时间差。这样“时钟信号”和“数据信号”就无法对应了。

解决这个问题的办法是,将“数据信号”和“时钟信号”叠加到一起,合并为一个电流(或光)进行传输。由于“时钟信号”是按固定频率进行变化的,只要能够找到“时钟信号”的变化周期就能从叠加信号中提取“时钟信号”,进而计算出“数据信号”。

所以,我们不能直接就发送包的数据,而是要在前面加上一段用来测量时钟信号的特殊信号,这就是报头的作用。即下图:

时钟信号和数据信号叠加

“如果在包信号结束之后,继续传输时钟信号,就可以保持时钟同步的状态,下一个包就无需重新进行同步。有些通信方式采用了这样的设计,但以太网的包结束之后时钟信号也跟着结束了,没有通过这种方式来保持时钟同步,因此需要在每个包的前面加上报头,用来进行时钟同步。”
摘录来自: [日] 户根勤. “网络是怎样连接的。”

以太网根据速率和网线类型的不同分为多种派生方式,每种方式的信号形态也有差异,并不都是像本例中讲的这样,单纯通过电压和电流来表达 0 和 1 的。因此,101010…这样的报头数字信息在转换成电信号后,其波形也不一定都是图 2.25 中的那个样子,而是根据方式的不同而不同。但是,报头的作用和基本思路是一致的。

将数据包发送出去

加上报头、起始帧分界符和FCS 之后,我们就可以将包通过网线发送出去了。(在这里我们只讨论全双工模式,所以不讨论半双工下如何避免信号碰撞)

首先,MAC 模块从报头开始将数字信息按每比特转换成电信号,然后由PHY(在100Mbit/s 以上的以太网中都叫PHY, Physical Layer Device;其它时候,有些地方也叫MAU,Medium Attachment Unit)信号收发模块发送出去。在这里,将数字信号转化为电信号的速率就是网络的传输速率、例如每秒将10Mbit 的数字信息转换成电信号发送出去,则速率就是10Mbit/s。

PHY模块会将信号转换为可在网线上传输的格式,并通过网线发送出去。以太网对不同网线类型、速率以及其它对应的信号格式进行了规定,但MAC模块并不关心这些区别,而是将可转换为任意格式的通用信号发送到PHY模块,然后由PHY模块再将其转换为可传输格式。

接收返回包

信号的开头是报头,通过报头的波形同步时钟,然后遇到起始帧分界符时开始将后面的信号转换成数字信息。

这时与发送时是相反的,即PHY模块会将信号转换成通用格式并发送给MAC 模块,MAC模块再从头开始将信号转换为数字信号,并放入缓冲区中。当到达信号的末尾时,还需要检查FCS。如果FCS 检测失败,则判断这个包为错误包,网卡将其丢弃。

如果FCS 检验无误,接下来就要看一下MAC头部中的接收方MAC地址与网卡在初始化时分配给自己的MAC地址是否一致,以判断这个包是不是发送给自己的。如果不是,则直接将包丢弃;如果是,则将包放入缓冲区中。到这里MAC模块在接收数据包时的工作就完成了,接下来网卡会通知系统收到了一个包。

“有一个特殊的例子,其实我们也可以让网卡不检查包的接收方地址,不管是不是自己的包都统统接收下来,这种模式叫作“混杂模式”(Promiscuous Mode)。”
摘录来自: [日] 户根勤. “网络是怎样连接的。”

通知计算机

通知计算机操作系统的操作会使用到中断机制。

为什么这里会有中断?

在网卡接收数据包的操作中,操作系统不会一直监控网卡的活动,而是去执行其它任务。因此,如果网卡一直不通知操作系统,操作系统是无法得知网卡收到了数据包。网卡驱动也是操作系统中运行的一个程序,因此它也不知道包到达了。这种情况下,我们需要一种机制能够打断操作系统正在执行的任务,让它注意到网卡中发生的事情,这种机制就是中断。

中断的工作过程是这样的:

  1. 网卡向扩展总线中的中断信号线发送信号,该信号线通过计算机中的中断控制器连接到CPU;
  2. 当产生中断信号时,CPU会暂时挂起正在处理的任务,切换到操作系统中的中断处理程序;
  3. 中断处理程序会调用网卡驱动,控制网卡执行相应的接收操作;
  4. 网卡驱动被中断处理程序调用后,会从网卡的缓冲区取出收到的包,并通过MAC头部中的以太网类型字段判断协议的类型;
  5. 协议类型如果是0x0800代表IP协议(除了TCP/IP协议外,还有很多其它的协议,不过现在的主流是TCP/IP协议了),网卡驱动就会把这样的包交给TCP/IP协议栈。接下来,协议栈会判断这个包应该交给哪个应用程序,并进行相应的处理。

“中断是有编号的,网卡在安装的时候就在硬件中设置了中断号,在中断处理程序中则将硬件的中断号和相应的驱动程序绑定。例如,假设网卡的中断号为 11,则在中断处理程序中将中断号 11 和相应的网卡驱动绑定起来,当网卡发起中断时,就会自动调用网卡驱动了。现在的硬件设备都遵循即插即用 98 规范自动设置中断号,我们没必要去关心中断号了,在以前需要手动设置中断号的年代,经常发生因为设置了错误的中断号而导致网卡无法正常工作的问题。”
摘录来自: [日] 户根勤. “网络是怎样连接的。”

将包交给上层协议栈处理

假设服务器返回的数据包是以太网类型0x0800,网卡会将数据包交给TCP/IP协议栈来进行处理。

接下来就轮到IP模块先开始工作了。第一步检查IP头部,确认格式是否正确。如果格式正确,下一步就是查看接收方IP地址,返回的数据包的目的地址一定与设备的网卡地址一致,检查确认之后我们就可以接收这个包了。

如果接收方IP地址与IP包头的目的地址不一致,那么肯定是发生了错误(客户端不负责对包进行转发,因此不应该收到不是发给自己的包)。当发生这样的错误时,IP模块会通过ICMP消息将错误告知发送方。

如果接收方IP地址与IP包头的目的地址一致,则这个包会被正常接收。此时,IP模块会检查收到的包是否为IP分片(IP分片再另外章节讨论),如果是IP分片,则IP模块会将其暂存至内部的内存空间,等待IP头部中具有相同标识的包全部到达,并根据分片偏移量字段,将所有分片(数据包)还原成原始的包,这个操作叫做分片重组。

到达这一步IP模块的工作就结束了,接下来包会被交给TCP模块。TCP模块根据IP包头中的源目IP地址,和TCP头部中的源目端口号来查找对应的套接字。找到对应的套接字之后,就可以根据套接字中记录的通信状态,执行相应的操作了。例如:如果包的内容是应用程序数据,则返回确认接收报文,并将数据放入缓冲区,等待应用程序接收;如果是建立或断开连接的控制包,则返回相应的相应控制包,并告知应用程序建立或断开连接的操作状态。

“严格来说,TCP 模块和 IP 模块有各自的责任范围,TCP 头部属于 TCP 的责任范围,而 IP 头部属于 IP 模块的责任范围。根据这样的逻辑,当包交给 TCP 模块之后,TCP 模块需要查询 IP 头部中的接收方和发送方 IP 地址来查找相应的套接字,这个过程就显得有点奇怪。因为 IP 头部是 IP 模块负责的,TCP 模块去查询它等于是越权了。如果要避免越权,应该对两者进行明确的划分,IP 模块只向 TCP 模块传递 TCP 头部以及它后面的数据,而对于 IP 头部中的重要信息,即接收方和发送方的 IP 地址,则由 IP 模块以附加参数的形式告知 TCP 模块。然而,如果根据这种严格的划分来开发程序的话,IP 模块和 TCP 模块之间的交互过程必然会产生成本,而且 IP 模块和 TCP 模块进行类似交互的场景其实非常多,总体的交互成本就会很高,程序的运行效率就会下降。因此,就像之前提过的一样,不妨将责任范围划分得宽松一些,将 TCP 和 IP 作为一个整体来看待,这样可以带来更大的灵活性。”
摘录来自: [日] 户根勤. “网络是怎样连接的。”