1. TCP数据迁移

在上篇文章的例子中,我们使用read/write函数来读写套接字上传输的数据。那么在调用这两个API的过程中,数据是怎么迁移的呢?我们先看下面这张图:
image.png
我们以默认属性的套接字为例,当我们在申请套接字的时候,操作系统会为该套接字创建两个缓冲区:输入缓冲、输出缓冲。当我们调用write函数时,系统会将数据从应用程序的缓冲区转移到内核中的缓冲区,当数据拷贝完成后,write函数调用返回,注意:write函数并不会执行发送数据操作。除此之外,当要写入的数据大于输出缓冲区的大小时,write函数会阻塞,直到全部数据拷贝完成才会返回。
同理,当我们调用read函数时,实际上会将输入缓冲的数据拷贝到应用程序缓冲区中,并不真正执行接收数据操作。当输入缓冲区中无数据时,read函数会阻塞,当对方断开连接时,read函数会返回0。


除此之外,还需要注意的是:系统内核在发送数据后,并不会立即删除数据,当收到该数据被成功接收的ack后,才会删除该数据。

2. TCP流控制

2.1 TCP首部

20190401145313308.png
序号(seq)用来标识TCP连接中数据发端向收端发送的字节流,如果将字节流看作是两个应用程序间的单向流动,那么序号就起到对每个字节标记的作用。
序号为无符号4字节整数,最大值为232 - 1。当序号达到最大值后,会从0重新开始计数。
当新连接建立时,SYN字段被置为1,同时本机会选择一个数作为该连接的起始序号(ISN),因为序号部分会包含这一多出来的序号,因此对该数据包进行确认时应+1,也就是说:应答包的ack=seq+1
TCP为应用提供全双工服务,TCP连接上的两个方向均应该使用序号标识自己的数据字节流。

2.1 三次握手过程

TCP协议是一种面向连接的,可靠的协议,因此会有连接的过程:客户端与服务端会进行三次数据交互后成功建立连接,也就是我们常说的“三次握手”。下面是”三次握手”示意图:
image.png
详细解释如下:

  • 第一次握手:第一次握手通常由客户端发起,其中seq=10的意思为:现在发送的10号数据包,如果接收无误,请允许我发送seq=11的数据包。
  • 第二次握手:第二次握手通常由服务端发送回应,其中ack=11的意思为:seq=10的数据包我已成功接收,seq=20的意思与第一次握手时一致
  • 第三次握手:第三次握手由客户端发起,意为对服务器应答的回应。seq=1的意思为:现在发送的时1号数据包。

序号 方向 seq ack SYN ACK
1 A➡B 100(ISN) 0 1 0
2 B➡A 200(ISN) 101 1 1
3 A➡B 200 201 0 1
  1. 由上述表格可以看出,主机会为TCP连接的两个方向选出不同的初始序号(ISN),当然我们平时见到的通常为0

2.3 传输过程

连接建立后,就可以开始传输数据了。传输数据的示意图如下所示:
image.png
具体解释如下:

  1. seq=100意为:当前发送的是100号数据包,即该数据包第一个数据在字节流中的位置为100。ack=x的意思为序号为x的数据包已成功接收到,可以发送下一个数据包了
  2. B收到A发送的数据包后,以ack填充seq字段,同时以seq+接收到的数据长度作为ack对A端发送的数据包进行回应。
  3. A端收到B端发送的数据后,通过ack与上个数据包的seq对比,可以知道B端到底接收到多少字节数据,因此其继续传输时就会以该位置(也就是上个包的ack值)作为起始位置(seq值)继续传输。

序号 方向 seq ack 数据长度 数据包长度
1 A➡B 300 700 1460 1514
2 B➡A 700 300+1460=1740 0 54
3 A➡B 1740 700 1460 1514
4 B➡A 700 1740+1460=3200 0 54
  1. 因此,`应答包ack=``seq+数据长度`。除此之外,数据包首部并不参与这一计算中。

2.4 连接断开

当传输完成后,就可以断开连接。当连接断开时,TCP首部中的FIN标志位置为1,当客户端与服务端都发送过FIN数据包后,连接成功断开。连接断开过程具体如下:
image.png
具体解释如下:

  1. A端发送FIN数据包,告诉B端我这边没有数据要发送了,想要关闭连接
  2. B端收到A端的FIN数据包,先返回回应包:我知道了,但我目前不想关闭,因为还有数据没有传送完。
  3. B端数据传送完成之后,会向A端发送FIN数据包告知:我数据已经传送完成了,咱们可以一起关闭连接了。(b、c之间可能会有很多ack=101的数据包,这是在传输剩余数据)
  4. A端收到B端的FIN数据包,会关闭连接。(实际上并不会,而是会进入IIME_WAIT状态,等待2MSL(数据包在网络存货最大时间)后,才会彻底关闭)。

那么为什么需要等待2MSL的时间呢,有以下两个原因:

  1. 保证连接能够可靠的关闭:

假设A端最后一次的ACK数据包丢失了,此时B端超时之后会重发FIN数据包,数据包到达A端之后,A端会发出ACK数据包并重新计时,一直如此循环直到连接被正确关闭。若是A端并未进入TIME_WAIT状态而是直接关闭,B端超时之后会重新发送FIN数据包,此时A端会发送RST复位数据包,此时B端会认为是错误连接,显然这一关闭流程并不可靠。

  1. 保证链路残留数据包能够正确、彻底的消失

假设A端没有进入TIME_WAIT状态,而是直接关闭,此时A端发起一个新的连接(可能会出现IP、端口均与关闭连接相同的情况),若是连接建立请求先到B端,A端已关闭连接残留的数据包后到B端,此时这些前一连接的数据会被认为是新连接的,从而导致处理出错。


序号 方向 seq ack FIN ACK
1 A➡B 100 200 1 1
2 B➡A 200 101 0 1
3 B➡A 200 101 1 1
4 A➡B 101 201 0 1