前言

最近在整理网络抓包分析相关的资料,同时又在阅读《网络是怎样连接的》。上一篇从网络协议层对设备连网的过程和发送数据的过程进行了探讨。本篇讨论的是TCP协议的数据收发的过程。

创建Socket

由于TCP协议是需要建立连接的,在建立连接之前,需要先初始化Socket。在初始化Socket时,操作系统分配一片内存空间保存该Socket的必要信息。这些信息除了传输数据时需要TCP头部的信息以外,还包括操作系统TCP协议需要用的SO_REUSEADDRTCP_NODELAY等TCP控制参数。

创建完成后,就会返回一个Socket描述符。这个描述符相当于一个唯一的Socket识别号,通过这个描述符就能从操作系统获取到Socket实体对象。

建立连接

我们知道TCP建立连接三次握手实际是为了交换客户端与服务端必要的信息,或者可以称之为协商。主要协商的信息包括双方的IP和端口号、接收缓冲区大小、以及其他TCP控制参数(如ECN:显式拥塞通告,SACK:选择确认选项等)。

在创建Socket时,还不知道需要和谁传输数据,仅初始化了一个Socket结构。在建立连接时,我们需要告诉底层操作系统,需要连接的目标地址。就好比寄快递时,我们需要在快递单写上收件人的地址和以及收件人信息。

初始化缓冲区

在执行收发数据前,我们需要分配一个空间用于临时存放数据,这个空间被称为缓冲区。接收数据有接收数据缓冲区,发送数据有发送数据缓冲区。这两个缓冲区也是在建立连接的时候分配的。

DNS解析器和ARP协议

操作系统支持我们直接传递一个明确的IP地址或者传入一个域名。如果传入的是域名,操作系统会调用DNS解析器。DNS解析器会通过DNS协议去查询域名对应的IP,这个过程就是我们上一篇文章将的DNS协议部分。

当然如果我们还需要知道DNS的MAC地址,如果操作系统底层的ARP缓存没有DNS的IP所对应的MAC地址记录,还需要通过ARP协议进行广播获取DNS服务器的IP所对应的MAC地址。当获取到该MAC地址后,操作系统就会将DNS的IP和MAC地址保存到ARP缓存中,下一次再调用DNS解析器时,就可以直接与DNS服务器进行通讯了。

这整个过程就像一个黑盒一样,应用层和开发人员完全无需干预。TCP协议的分层及操作系统提供的接口大大降低了开发人员的工作量,甚至无需深入了解TCP协议就能开发出一套网络通讯程序。就好比快递员只要会开车,无需了解汽车内部结构一样。

三次握手

当我们获取到了需要连接的目的地址的真实IP时,就可以建立TCP连接了。建立连接需要三次握手,过程如下。

网络数据传输时操作系统干了什么?-LMLPHP

上图看着是非常简单,交互3次成功建立连接。实际这个过程中还有许多细节。在发送第一个SYN包以前(客户端发送到服务端的第一个请求,TCP协议的Flags字段中将SYC设置成了1)。操作系统需要获取到TCP头部所需要的控制信息。
网络数据传输时操作系统干了什么?-LMLPHP

在建立连接时,操作系统需要从动态端口池中获取一个可用的端口,将端口号信息保存到Socket对象的内存中。然后将头部的控制位SYN设置为1,表示建立连接。接着还需要获取操作系统的接收缓冲区大小,将其保存到TCP头部的窗口大小字段中,当然发送序号和确认序号目前都为0。

当TCP头部信息填写完成后,操作系统就将TCP协议的数据传递给IP模块。IP模块执行网络包发送之前会对数据包填充IP头部,若有需要则对数据包进行分片,这个过程后面在讨论。

获取发送方IP

若客户端只有一个网卡,发送方IP就是这个网卡的IP。但是如果有多个网卡,那么操作系统就需要选择一个合适的网卡作为发送网卡。IP模块通过路由表来判断哪条路由能够匹配,从而选择一条合适的路由,最后就可以知道应该选择哪个网卡。获取该网卡的IP保存到IP头部的源IP地址中,将目标IP保存到IP头部的目标IP地址中。IP模块添加IP头部完成之后,继续添加MAC头部(MAC头部实际也是IP协议添加的)。

获取目的MAC地址

IP模块需要获取到目的设备的MAC地址。和前面DNS讨论的一样,若ARP缓存中有记录,无需查找,若没有记录则会通过ARP协议查找目的设备的MAC地址,如果目的设备与当前设备在同一个局域网内,则会获取到目的设备的MAC地址。如果目的设备与当前设备不在一个局域网,则会查找到下一个路由器的MAC地址。具体MAC地址是谁的IP模块并不关心,他的任务就是获取到目的MAC地址并将其填入到MAC头部。

发送方的MAC地址就是网卡的MAC地址,这个值是在网卡生产时写入到网卡的ROM中的,只要将其读取出来填入到MAC头部的发送端MAC地址字段中即可。

经过以上步骤之后,一个完整的建立连接的SYN包就创建完成了。接下来就要将这个包发送到对端。发送SYN的过程和发送数据的过程一样,具体的发送逻辑在发送数据一节在做具体说明。

客户端发送SYN包给服务端时,服务端会返回SYN+ACK包,在这个包中也会将自己的接收缓冲区的大小填写到TCP头部的窗口大小字段中。这样客户端发送数据时就可以尽量避免发送的在途包超过接收方的接收缓冲区导致丢包重传。

当连接建立完成时,接下来双方就可以收发数据了。

发送数据

在讨论网卡发送数据之前,我们先看看应用层调用发送数据时发生了什么。

应用程序调用操作系统发送数据的接口时,并不是直接将数据发送到出去,而是先将数据保存到socket的发送缓冲区中。具体何时发送数据TCP协议栈决定的。

当发送缓冲区有数据要发送时,首先TCP协议栈会根据MSS对需要发送的数据进行分段。分段的目的是避免数据超过MTU大小导致数据被IP层分片。

MSS(Maximum Segment Size)表示最大段长度,MTU(Maximum Transmission Unit)表示最大传输单元。MTU是以太网是一个网络包的最大长度,以太网中默认是1500。当数据包超过MTU时,IP模块就会对数据进行分片,每一片都会有自己的TCP头部和IP头部。

Nagle算法

当Socket配置了Nagle算法时,当Socket的发送缓冲区短时间内有大量的小包(数据长度小于MSS),则操作系统不会立即为每个包添加TCP头部和IP头部,而是会尝试等待一段时间以便一次性将多个数据进行合并成一个大包发送。这样能够提升带宽利用率,但是也可能会造成数据的短暂延迟发送。因此大多数情况下,我们通常会禁用Nagle算法。

TCP VS UDP

众所周知UDP性能比TCP性能高得多,主要原因就是TCP为了保证可靠性做了大量的工作,如建立连接、重传、拥塞避免、慢启动等。但是为什么许多协议仍然选择TCP而不是UDP呢?一个重要的原因就是传输小包数据适合使用UDP,比如DNS协议、DHCP协议。这些协议能够保证数据在以太网传输时小于MTU避免被IP层分片。在复杂的网络环境中,无法得知中间某个设备的配置,若数据超过设备MTU而设备又设置了DF标记(Don't Frgment,不分片)时就会将数据包丢弃。大多数应用层协议的数据很可能会超过MTU,使用TCP协议就可以通过分段来避免IP层分片。TCP分段数据丢失时TCP有超时重传、快速重传等机制保证数据的可靠性。而IP层没有这个机制,同时IP层分片后即使一个片丢失也必须重传所有分片。而TCP层数据丢失仅需重传丢失的数据即可(SACK机制)。

网卡发送数据

现在需要发送的数据包已经加上了TCP头部、IP头部以及MAC头部。接下来就可以将数据交给网卡将数据转换为电信号或光信号在网线上传输了。IP模块执行完成之后就会调用网卡驱动执行发送数据,网卡驱动从IP模块获取到数据包后,会将其复制到网卡的缓冲区中,然后会在包的开头和结尾添加报头和起始帧分界符,在包尾添加用于检测错误的帧校验序列(FCS)。

网络数据传输时操作系统干了什么?-LMLPHP

报头是由共计56个比特的01,起始帧分界符是10101011,网卡通过报头和起始帧分界符来判断帧的位置。具体如何判断这里不做具体讨论。

网络数据传输时操作系统干了什么?-LMLPHP

ACK

当发送到对端时,并没有结束,TCP协议通过ACK确认机制保证数据可靠。当接收端收到数据时,需要返回一个ACK包给客户端来通知自己收到的数据包长度。发送数据时TCP首部有几个关键参数,SEQ表示发送的数据起始偏移量,ACK表示确认收到对端的数据长度。当客户端发送的SEQ=1461,数据长度是1460时,服务端若确认已收到SEQ小于1461以前所有的包和当前包时,就需要返回一个ACK包,该包的TCP头部的ACK值为SEQ+数据长度,也就是2921。这样客户端就能确认服务端是否收到了数据,如果没有收到数据,可通过重传和快速重传等机制重传数据。具体重传逻辑这里不进行详细讨论。

接收数据

接下来我们看接收数据时发生了什么。当网卡接收到网络上的数据时,会将数据从电信号或光信号转换为数字信号(也就是0和1),当网卡判断到包尾时,会通过CRC算法计算出错误校验码和包尾的FCS进行对比,若不一致,则说明数据传输时发生了错误,则丢弃,客户端最终通过重传机制重发。若FCS校验通过,还会校验一下数据包的接收方MAC地址和当前网卡的MAC地址是否一致,若不一致则会丢弃,如果一致则会将数据包存放到网卡的接收缓冲区中。然后就会通过计算机的中断机制通知计算机收到了数据。

中断

首先,网卡向扩展总线中的中断信号线发送信号,该信号线通过计算机中的中断控制器连接到CPU。当产生中断信号时,CPU会暂时挂起正在处理的任务,切换到操作系统中的中断处理程序。(由于中断例程的优先级较高,如果不先处理网卡缓冲区的数据,网卡缓冲区的数据很快就会满,从而造成丢包)然后,中断处理程序会调用网卡驱动,控制网卡执行相应的接收操作。中断是有编号的,网卡在安装的时候就在硬件中设置了中断号,在中断处理程序中则将硬件的中断号和相应的驱动程序绑定。因此哪个网卡收到了数据就会调用哪个中断处理程序从而调用到对应的网卡驱动。

网卡驱动被中断处理程序调用后,就会从网卡的缓冲区中获取到接收到的数据包,并通过MAC头部判断网络层的协议栈(目前通常是TCP/IP协议),网卡驱动就会把包交给对应的协议栈处理。从这里可以看到网卡并不关心数据包的内容是什么,它只需要校验数据是发给自己的,而且数据传输过程中没有错误,就可以放入到缓冲区中。

IP模块接收数据

当IP模块获取到接收的数据时,首先要判断接收者是不是自己。因此会获取到目的IP地址和当前IP地址比较是否一致。如果不一致,若服务器配置了路由功能则会转发到目标IP,否则IP模块就会通过ICMP消息将错误告知对方并丢弃数据包。如果收到的IP和当前IP一致,则需要进行一个分片重组过程。首先判断数据包是否发生了分片。如果发生了分片,则需要将分片包还原。分片包的IP头部的唯一标识符是一样的,每个分片包会有自己的分片偏移量。

网络数据传输时操作系统干了什么?-LMLPHP

当接收到所有分片后就可以进行重组还原成一个完整包,IP模块的任务就结束了,接下来就将数据包交给TCP模块。TCP模块会根据收方和发送方的IP和端口查找到对应的套接字,并根据套接字的状态执行响应的操作。

TCP模块接收数据

若接收到的是应用程序数据,则返回ACK,然后将数据放入socket的接收缓冲区中,等待应用来读取(如果是异步IO则会发送一个完成通知到完成队列,从而触发应用程序读取数据);如果是连接或关闭连接的控制包,则会返回对应的响应控制包(如SYN或FIN等),并告知应用程序的连接和关闭连接的操作状态。

顺便提一下,若操作系统开启了延迟确认功能,则会延迟一段时间(windows下时200ms)返回ACK。

断开连接

TCP断开连接通过四次挥手,由于TCP协议是全双工的,因此双方互相发送FIN包以及返回ACK包,断开双向的连接。TCP协议也支持只断开单向连接,被称为半连接。Windows和Linux许多管道机制都是通过socket的半连接实现的。

客户端再关闭连接后并不会立即释放socket资源,而是进入TIME_WAIT状态等待2MSL时间以后再释放客户端的Socket资源。

主要原因是客户端的最后一个ACK可能丢包,此时服务端会重传FIN包,若客户端不等待一段时间才释放资源而是立即释放。新创建的Socket可能恰好又使用了相同的断开,这时候接收到了服务端的FIN包就会立即错误的关闭了新的连接。

结语

通过两篇文章分别从网络协议和操作系统的协议栈2个方面的处理过程对网络数据传输的过程进行了讨论。一些更细节的东西本篇并没有说明,比如路由器、交换机等网络传输的具体过程。网卡内部各模块的处理细节等。网卡更细的内容大部分开发者无需了解,但是操作系统协议栈的处理过程能够帮助开发者理解自己所开发的网络应用到底是如何处理的。

参考文献

  1. 《网络是怎样连接的》
  2. 《Wireshark数据包分析实战详解》
  3. I/O中断原理

08-04 02:24