首页 > 技术文章 > 网络是怎样连接的-读书笔记-用电信号传输 TCP/IP 数据-数据收发部分

HermioneBlog 2020-11-11 21:14 原文

用电信号传输 TCP/IP 数据

本章我们将探索操作系统中的网络控制软件(协议栈)和网络硬件(网 卡)是如何将浏览器的消息发送给服务器的。整体结构如下:

ps.浏览器、邮件等一般应用程序收发数据时用 TCP,DNS 查询等收发较短的控制数据时用 UDP。
其中,数据收发部分的工作框架如下所示:

创建套接字

套接字的本质就是,存放控制信息!这里记录了用于控制通信操作的控制信息,例如通信对象的 IP 地址、端口号、通信操作的进行状态等。本来套接字就只是一个概念而已,并不存在实体,如果一定要赋予它一个实体,我们可以说这些控制信息就是套接字的实体,或者说存放控制信息的内存空间就是套接字的实体。
协议栈是根据套接字中记录的控制信息来工作的。
创建套接字时,首先分配一个套接字所需的内存空间,然后向其中写入初始状态。接下来,需要将表示这个套接字的描述符告知应用程序。收到描述符之后,应用程序在向协议栈进行收发数据委托时就需要提供这个描述符。由于套接字中记录了通信双方的信息以及通信处于怎样的状态,所以只要通过描述符确定了相应的套接字,协议栈就能够获取所有的相关信息,这样一来,应用程序就不需要每次都告诉协议栈应该和谁进行通信了。
(托管服务~)
一般而言,服务器都是创建好了套接字在那等待连接,而客户端一般是根据用户需要创建套接字连接服务器。

连接服务器

创建套接字之后,应用程序(浏览器)就会调用 connect,随后协议栈会将本地的套接字与服务器的套接字进行连接。
连接是在干嘛?实际上就是在准备控制信息!
套接字刚创建完时,里面空空如也,说明控制信息都没有。Client要做的事情是,将通信目标的IP地址、端口号告诉协议栈,当然还有一些别的奇奇怪怪的东西。Service要做的是,等待Client的连接请求,这样Service才知道通信目标的IP地址和端口号。
此外,当执行数据收发操作时,我们还需要一块用来临时存放要收发的数据的内存空间,这块内存空间称为缓冲区,它也是在连接操作的过程中分配的。
总的来说,控制信息分为两类:

  1. 头部信息:这是客户端和服务器相互联络时交换的控制信息,保存在消息的头部。这些信息的交互是非常重要的。

  2. 控制信息还有另外一类,那就是保存在套接字中,用来控制协议栈操作的信息。应用程序传递来的信息以及从通信对象接收到的信息都会保存在这里,还有收发数据操作的执行状态等信息也会保存在这里,协议栈会根据这些信息来执行每一步的操作。

最后是连接的实际操作,connect(< 描述符 >, < 服务器 IP 地址和端口号 >, …)!也就是要将控制信息,通过描述符,在connect接口的帮助下交给目标。

收发数据

应用程序通过使用OS提供的接口write进行数据发送,在这个过程中:

  • 协议栈不关心你给我的是什么数据。
  • 协议栈不是一收到数据就发送,而是等当前buffer填满(网络包长度MTU一般是1500Byte,而除去头后所能容纳的最大长度叫做MSS则是具体的判断指标) or 等待时间到了两个条件满足一个的时候,完成数据发送的功能。发送的数据太短会导致效率下降,因为每个网络包都具有一定长度的头,而等待时间太长也是不能接受的,这个trade-off就靠应用自己去设计。

HTTP 请求消息一般不会很长,一个网络包就能装得下,但如果其中要提交表单数据,长度就可能超过一个网络包所能容纳的数据量,比如在博客或者论坛上发表一篇长文就属于这种情况,这种情况下就要对数据进行拆分。发送缓冲区中的数据会被以 MSS 长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中。当判断需要发送这些数据时,就在每一块数据前面加上 TCP 头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号,然后交给 IP 模块来执行发送数据的操作。

ACK机制

为了安全起见,在发送完数据后还要等待对方的确认,一次发送才算结束,这就是ACK机制
首先,TCP 模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节,接下来在发送这一块数据时,将算好的字节数写在 TCP 头部中,“序号”字段就是派在这个用场上的。基于信号,我们可以知道有没有丢包,最后如果确认没有遗漏,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入 TCP头部的 ACK 号中发送给发送方 A。
简单来说,发送方说的是“现在发送的是从第 ×× 字节开始的部分,一共有 ×× 字节哦!”而接收方则回复说, “到第 ×× 字节之前的数据我已经都收到了哦!”这个返回 ACK 号的操作被称为确认响应,通过这样的方式,发送方就能够确认对方到底收到了多少数据。
不过,在发送之前,通信双方得告诉对方自己发送的序列号初始值,这个数值是随机的,为了增加网络的安全性。


如果对方没有返回某些包对应的 ACK 号,那么就重新发送这些包。通过“序号”和“ACK 号”可以确认接收方是否收到了网络包。
ACK号的机制可以分为以下几个关键点:
ACK号的等待时间:
如果网络比较阻塞,那么ACK号的回传会变慢,但是若设置了过长的等待时间,那么阻塞又会更加严重,所以需要设置一个较好的值。
TCP 采用了动态调整等待时间的方法,这个等待时间是根据 ACK 号返回所需的时间来判断的。具体来说,TCP 会在发送数据的过程中持续测量 ACK 号的返回时间,如果 ACK 号返回变慢,则相应延长等待时间;相对地,如果 ACK 号马上就能返回,则相应缩短等待时间 B。
使用窗口有效管理 ACK 号:
如果是发一个包等到ACK确认后再继续发送,那样会造成时间浪费的问题。我们可以选择一直发送下去,一次性发完,但这样非常考验接收方的接受能力。
综合考量,接收方首先要告诉发送方自己能接受多少数据,然后发送方根据这个size决定一定时间发出的数据量,这就是滑动窗口方法。

在这张图中,接收方将数据暂存到接收缓冲区中并执行接收操作。当接收操作完成后,接收缓冲区中的空间会被释放出来,也就可以接收更多的数据了,这时接收方会通过 TCP 头部中的窗口字段将自己能接收的数据量告知法送方。这样一来,发送方就不会发送过多的数据,导致超出接收方的处理能力了。

ACK 与窗口的合并:
返回ACK号和更新窗口的时机如何选择呢?
发送方一发出数据,就可以自行计算对方剩余的容量大小,当接收方将数据传递给应用程序,导致接收缓冲区剩余容量增加时,就需要告知发送方,这就是更新窗口大小的时机。
对于ACK号而言,如果确认内容没有问题,就应该向发送方返回ACK号,因此我们可以认为收到数据之后马上就应该进行这一操作。虽然理论上说是这样最好,但会发出两个包,降低网络传输效率,不如做出一些妥协,等应用程序取出后,和窗口更新信息一起发出是最吼的。
另一方面,ACK号的目的就是为了表示已经收到的数据量,也就是说,告诉发送方最后收到的数据的位置在哪里,因此只需要发送最后一个ACK号即可,中间全部可以省略。同样的,窗口大小更新也可以这样做,省略中间的连续更新,最后一起更新。

收发部分的总结:
首先,协议栈会检查收到的数据块和 TCP 头部的内容,判断是否有数据丢失,如果没有问题则返回 ACK 号。然后,协议栈将数据块暂存到接收缓冲区中,并将数据块按顺序连接起来还原出原始的数据,最后将数据交给应用程序。具体来说,协议栈会将接收到的数据复制到应用程序指定的内存地址中,然后将控制流程交回应用程序。将数据交给应用程序之后,协议栈还需要找到合适的时机向发送方发送窗口更新。

断开连接

通信双方谁都可以先断开连接,以服务器方断开为例子:
服务器一方的应用程序会调用 Socket 库的 close 程序。然后,服务器的协议栈会生成包含断开信息的 TCP 头部,具体来说就是将控制位中的 FIN 比特设为 1。接下来,协议栈会委托 IP 模块向客户端发送数据。服务器的套接字中也会记录下断开操作的相关信息。
而对客户端,当收到服务器发来的 FIN 为 1 的 TCP 头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态。然后,为了告知服务器已收到 FIN 为 1 的包,客户端会向服务器返回一个 ACK 号。这些操作完成后,协议栈就可以等待应用程序来取数据了,过了一会委托协议栈收发的应用程序可能就申请read。应用程序有可能在收到 FIN 为 1 的包之前就来读取数据,这时读取数据的操作会被挂起,等到 FIN 包到达再继续执行。只要客户端收到服务器返回的所有数据,就会调用close结束收发,这时客户端的协议栈也会和服务器一样,生成一个 FIN 比特为 1 的 TCP 包,然后委托 IP 模块发送给服务器,过一会服务器返回一个ACK,就全部结束了。

而套接字的删除还要再等一段时间,这是为了防止一些误操作。例如,服务器没有收到用户的ACK号,就会重新发送一个FIN消息,而此时若用户的socket已经没了,那就要一直等下去,更糟糕的是用户新创建了一个socket,并将前一个端口号分配出去了,那么这个通信就要被断开了。

推荐阅读