网络是怎样连接的-收发数据(上)
2.3 收发数据
2.3.1 将 HTTP 请求消息交给协议栈
当控制流程从 connect 回到应用程序之后,接下来就进入数据收发阶段了。
数据收发操作是从应用程序调用 write 将要发送的数据交给协议栈开始的,协议栈收到
数据后执行发送操作,这一操作包含如下要点。
协议栈并不关心应用程序传来的数据是什么内容
应用程序在调用 write 时会指定发送数据的长度,在协议栈看来,要发送的数据就是一定长度的二进制字节序列而已。
协议栈并不是一收到数据就马上发送出去
会将数据存放在内部的发送缓冲区中,并等待应用程序的下一段数据。
一次将多少数据交给协议栈是由应用程序自行决定的,协议栈并不能控制这一行为。
如果一收到数据就马上发送出去,就可能会发送大量的小包,导致网络效率下降,因此需要在数据积累到一定量时再发送出去。
至于要积累多少数据才能发送,不同种类和版本的操作系统会有所不同,不能一概而论,但都是根据下面几个要素来判断的。
判断积累多少数据的两个要素
每个网络包能容纳的数据长度
协议栈会根据一个叫作 MTU 的参数来进行判断。
MTU 表示一个网络包的最大长度,在以太网中一般是 1500 字节(图 2.5)。
从 MTU 减去头部的长度,得到的长度就是一个网络包中所能容纳的最大数据长度,这一长度叫作 MSS 。
当从应用程序收到的数据长度超过或者接近 MSS 时再发送出去,就可以避免发送大量小包的问题了。
时间
当应用程序发送数据的频率不高的时候,如果每次都等到长度接近 MSS 时再发送,可能会因为等待时间太长而造成发送延迟。
这种情况下,即便缓冲区中的数据长度没有达到 MSS,也应该果断发送出去。
为此,协议栈的内部有一个计时器,当经过一定时间之后,就会把网络包发送出去。
协议栈允许开发者自己选择倾向哪个要素
在进行发送操作时需要综合考虑这两个要素以达到平衡。实际如何判断是由协议栈的开发者来决定的。
如果仅靠协议栈来判断发送的时机可能会带来一些问题,因此协议栈也给应用程序保留了控制发送时机的余地。
应用程序在发送数据时可以指定一些选项,比如如果指定“不等待填满缓冲区直接发送”,则协议栈就会按照要求直接发送数据。
像浏览器这种会话型的应用程序在向服务器发送数据时,等待填满缓冲区导致延迟会产生很大影响,因此一般会使用直接发送的选项。
2.3.2 对较大的数据进行拆分
HTTP 请求消息一般不会很长,一个网络包就能装得下,但如果其中要提交表单数据,长度就可能超过一个网络包所能容纳的数据量。比如在博客或者论坛上发表一篇长文就属于这种情况。
这种情况下,发送缓冲区中的数据就会超过 MSS 的长度,这时我们当然不需要继续等待后面的数据了。
发送缓冲区中的数据会被以 MSS 长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中。
根据发送缓冲区中的数据拆分的情况,当判断需要发送这些数据时,就在每一块数据前面加上 TCP 头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号,然后交给 IP 模块来执行发送数据的操作(图 2.6)。
IP 模块会在网络包前面添加 IP 头部和以太网的 MAC 头部后发送网络包,这些操作我们将稍后讲解。
2.3.3 使用 ACK 号确认网络包已收到
到这里,网络包已经装好数据并发往服务器了,但数据发送操作还没有结束。
TCP 具备确认对方是否成功收到网络包,以及当对方没收到时进行重发的功能,因此在发送网络包之后,接下来还需要进行确认操作。
确认的原理
计算网络包的长度
首先,TCP 模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节,接下来在发送这一块数据时,将算好的字节数写在 TCP 头部中,“序号”字段就是派在这个用场上的。
然后,发送数据的长度也需要告知接收方,不过这个并不是放在TCP 头部里面的,因为用整个网络包的长度减去头部的长度就可以得到数据的长度,所以接收方可以用这种方法来进行计算。
有了上面两个数值,我们就可以知道发送的数据是从第几个字节开始,长度是多少了。
将计算结果存入TCP头部中的ACK号
通过这些信息,接收方还能够检查收到的网络包有没有遗漏。
假设上次接收到第 1460 字节,如果收到序号为 1461 的包,说明中间没有遗漏。
但如果收到的包序号为 2921,那就说明中间有包遗漏了。
如果确认没有遗漏,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入 TCP头部的 ACK 号中发送给发送方。
这个返回 ACK 号的操作被称为确认响应,通过这样的方式,发送方就能够确认对方到底收到
了多少数据。
ACK比特
返回 ACK 号时,除了要设置 ACK 号的值以外,还需要将控制位中的ACK 比特设为 1。这代表 ACK 号字段有效,接收方也就可以知道这个网络包是用来告知 ACK 号的。
序号不是从1开始的
在实际的通信中,序号并不是从 1 开始的,而是需要用随机数计算出一个初始值。
因为如果序号都从 1 开始,通信过程就会非常容易预测,有人会利用这一点来发动攻击。
但是如果初始值是随机的,那么对方就搞不清楚序号到底是从多少开始计算的,因此需要在开始收发数据之前将初始值告知通信对象。
连接操作过程中客户端和服务器计算序号并将序号初始值传给对方
连接过程中,有一个将 SYN 控制位设为1 并发送给服务器的操作,就是在这一步将序号的初始值告知对方的。
实际上,在将 SYN 设为 1 的同时,还需要同时设置序号字段的值,而这里的值就代表序号的初始值。
刚刚只考虑了单向的数据传输,TCP 数据收发是双向的,在客户端向服务器发送数据的同时,服务器也会向客户端发送数据。
首先客户端先计算出一个序号,然后将序号和数据一起发送给服务器,服务器收到之后会计算 ACK 号并返回给客户端。
相反地,服务器也需要先计算出另一个序号,然后将序号和数据一起发送给客户端,客户端收到之后计算 ACK 号并返回给服务器。
此外,如图所示,客户端和服务器双方都需要各自计算序号,因此双方需要在连接过程中互相告知自己计算的序号初始值。
实际的工作过程
明白原理之后我们来看一下实际的工作过程,如图2.9所示。
客户端计算序号初始值并发送给服务器
首先,客户端在连接时需要计算出与从客户端到服务器方向通信相关的序号初始值,并将这个值发送给服务器(图 2.9 ①)。
服务器根据该初始值计算出ACK并和服务器的序号初始值一起返回给客户端
接下来,服务器会通过这个初始值计算出 ACK 号并返回给客户端(图 2.9 ②)。
初始值有可能在通信过程中丢失,因此当服务器收到初始值后需要返回 ACK 号作为确认。
同时,服务器也需要计算出与从服务器到客户端方向通信相关的序号初始值,并将这个值发送给客户端(图 2.9 ②)。
客户端根据该初始值计算出ACK号并返回给服务器
接下来像刚才一样,客户端也需要根据服务器发来的初始值计算出 ACK 号并返回给服务器(图 2.9 ③)。
客户端将序号和数据一起发送给服务器
序号和 ACK 号都已经准备完成了,进入数据收发阶段。
数据收发操作本身是可以双向同时进行的,但 Web 中是先由客户端向服务器发送请求,序号也会跟随数据一起发送(图 2.9 ④)。
服务器收到数据后再返回ACK号
然后,服务器收到数据后再返回 ACK 号(图 2.9 ⑤)。
从服务器向客户端发送数据的过程则正好相反(图 2.9 ⑥⑦)。
TCP 采用这样的方式确认对方是否收到了数据,在得到对方确认之前,发送过的包都会保存在发送缓冲区中。
如果对方没有返回某些包对应的 ACK 号,那么就重新发送这些包。
这一机制非常强大。通过这一机制,我们可以确认接收方有没有收到某个包,如果没有收到则重新发送,这样一来,无论网络中发生任何错误,我们都可以发现并采取补救措施(重传网络包)。
反过来说,有了这一机制,我们就不需要在其他地方对错误进行补救了。
因此,网卡、集线器、路由器都没有错误补偿机制,一旦检测到错误就直接丢弃相应的包。
应用程序也是一样,因为采用 TCP 传输,即便发生一些错误对方最终也能够收到正确的数据,所以应用程序只管自顾自地发送这些数据就好了。
不过,如果发生网络中断、服务器宕机等问题,那么无论 TCP 怎样重传都不管用。这种情况下,无论如何尝试都是徒劳,因此 TCP 会在尝试几次重传无效之后强制结束通信,并向应用程序报错。