跳转至

TCP

TCP(传输控制协议)是一个面向连接的协议,主要用于在网络中可靠地传输数据。

  • 面向连接:通信前必须建立连接(三次握手),通信结束要释放连接(四次挥手)
  • 可靠传输:指的是TCP协议能保证 接收到的所有字节都与发送的字节 相同有序
  • 面向字节流:把应用层数据看作字节流,根据MSS(最大报文段长度)划分数据段,不保留应用层的报文边界。
  • 流量控制:滑动窗口机制
  • 拥塞控制:慢启动、拥塞避免、快重传、快恢复
  • 全双工通信:数据可以同时双向传输

TCP 报文格式

 0                   1                   2                   3   
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |           |U|A|P|R|S|F|                               |
| Offset| Reserved  |R|C|S|S|Y|I|            Window             |
|       |           |G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             data                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 源端口(16位):存储发送应用程序的端口号,用于确定数据传输的来源应用。
  • 目标端口(16位):包含接收应用程序的端口号,确保数据能被发送到正确的应用程序。
  • 序列号(32位):通过有序分段和在接收端重组,确保数据按正确顺序接收。
  • 确认号(32位):包含下一个预期的序列号,用于确认已接收到的数据。
  • 数据偏移(4位):表示TCP数据负载的起始点,同时存储TCP头部的大小。
  • 控制标志(9位):TCP使用多个控制标志来管理通信。主要标志包括:
    • SYN(同步):负责建立发送方和接收方之间的连接
    • ACK(确认):用于传输对数据接收情况的确认
    • FIN(结束):表明TCP连接是否终止
    • RST(重置):主要用于在发生错误时重置连接
  • 窗口大小(16位):指定发送方接收窗口的大小。
  • 校验和(16位):用于检测传输过程中数据是否损坏。
  • 紧急指针(16位):指向紧急数据的第一个字节。
  • 可选选项(可变长度):表示不同的TCP选项。
  • 数据负载:包含实际传输的应用程序数据。

对比 UDP 的报文格式,不难发现TCP的报文中添加了很多字段,用来支持TCP独有的特性,具体的说:

  • 面向连接:控制标志帮助标记连接建立过程的状态。
  • 可靠传输:利用ACK控制标志确认应答,利用序列号、确认号和超时重传来保证数据传输有序,校验和来保证数据完整性。
  • 面向字节流:利用序列号和确认号确定字节流的位置。
  • 流量控制:利用窗口大小来协商发送速率。

还有一些没有提到的增加的头部字段:

  • 由于头部长度可变,所以有数据偏移字段。
  • 紧急指针用的比较少,不是所有的TCP协议的实现都支持。更多是依赖应用层的优先级机制来处理紧急数据。
  • 可选选项:常用的有
    • MSS:这个选项用于告知对方能够接收的最大报文段长度,通常设置为MTU减去TCP和IP头部的长度,以避免IP分片。对于以太网,MSS通常为1460字节(1500 - 40)。
    • Selective ACK:支持选择确认机制。该选项允许接收方告知发送方哪些数据包已经成功接收,哪些数据包丢失,从而减少重传的数量,提高网络效率。
    • Timestamp:时间戳信息,这有助于计算往返时间(RTT)。
    • NOP:用于扩展TCP窗口大小,以支持更大的接收缓冲区,适应高带宽延迟的网络环境。

在下文还会重新提到这些机制和字段,不用在此处完全理解。

TCP 有限状态机

有限状态机在网络领域是描述一个协议的常用手段,TCP协议的状态机如下:

TCP State Machine

建立连接 - 三次握手

为了建立连接 TCP 连接,通信双方必须从对方了解如下信息:

  1. 对方报文发送的开始序号。
  2. 对方发送数据的缓冲区大小。
  3. 能被接收的最大报文段长度 MSS。
  4. 被支持的 TCP 选项。

三次握手(如果一切顺利)的过程:

  1. 建立连接时,客户端发送 SYN 包到服务器,并进入 SYN_SENT 状态,等待服务器确认。
    • SYN = 1,表示请求建立连接。
    • seq = x,作为客户端发送的初识序号。
  2. 服务器收到 SYN 包,必须确认客户的 SYNack=j+1),同时自己也发送一个 SYN 包(seq=k),即 SYN+ACK 包,此时服务器进入 SYN_RECEIVED 状态。 理论上,在这个时候,服务器要为该连接分配资源,但是这意味着 SYN 泛洪攻击可能发生。
    • SYN = 1,表示同意建立连接。
    • seq = y,作为服务器发送的初识序号。
    • ACK = 1,表示收到用户发送的 SYN 包。实际上,之后发送的每一个包的 ACK 都为 1。
    • ack = x+1,表示请求的下一个字节是 x+1(表示 x 之前的字节都已经收到)
  3. 客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),此包发送完毕,客户端进入 ESTABLISHED(TCP 连接成功)状态。
    • ACK = 1,表示确认收到了服务器的 SYN+ACK 包。
    • ack = y+1,表示请求的下一个字节是 y+1(表示 y 之前的字节都已经收到)
    • seq = x+1,此时可以开始携带数据了,开始的序号是 x+1
  4. 服务器收到客户端的ACK包,进入ESTABLISHED状态。三次握手完成。

一些特殊情况:

断开连接 - 四次挥手

四次挥手(如果一切顺利)的过程(注意,这里的客户端和服务端都是相对的概念,不一定和上面的客户端和服务端相同):

  1. 客户端发送一个 FIN 包。此时客户端 关闭自己的发送通道,进入FIN WAIT 1状态。
    • FIN = 1,表示 我不会再向你发送数据了
    • seq = uu 是连着上次发送的包的。不过 FIN 报文即使不携带数据,也消耗一个序号。ACK = 1,上面强调过,除了一开始的 SYN 包,其他的 ACK 都为 1。这个和普通的包并没有什么不同。
  2. 服务端收到FIN包,进入CLOSE WAIT状态,发送一个 ACK。表示确认收到了 FIN 包。
    • ACK = 1, ack = u+1
  3. 服务器还可以接着发送数据,发送完成后,也发送 FIN 包表示我要关闭自己的发送通道了。进入LAST ACK状态。
    • FIN = 1,表示 我不会再向你发送数据了
    • ACK = 1, ack = u+1 seq =v
  4. 客户端收到FIN包,进入TIME WAIT状态,等待2MSL时间后关闭连接,进入CLOSE状态。发送ACK``ACK = 1, ack = v+1,表示确认收到服务器发送的断开请求。
    • 为什么需要 TIME_WAIT 状态呢?一个是让旧连接的包在网络上消失,防止影响新连接(如果新的 TCP 四元组和老的一模一样)。一个是如果 ACK 丢了,服务器重传 FIN 可以响应(不过实际上是响应 RST,不过作用差不多啦,都是把连接关掉)。
  5. 服务器收到ACK包,关闭连接,进入CLOSE状态。

Keepalive 机制

如果客户端意外死亡,没有发 FIN,难道这个连接就持续到服务器重启?按照上面TCP的状态机看,确实是这样的,显然,实践上是不可接受的。

Keepalive机制会定义一个时间段,如果在这个时间段内没有任何和连接相关的活动(数据包的交互),TCP 的 Keepalive 机制开始,每隔一个时间段,发送一个探测报文,如果 连续的若干个 探测报文没有得到响应,认为当前的 TCP 连接已经死亡。

Linux 实现了 Keepalive 机制。

可靠传输

TCP协议并不依赖可靠的传输协议(通常就是IP协议),来保证自身的可靠性。在底层协议会出错、乱序的情况下,如何保证数据传输不出错、不乱序?前者主要靠的是校验和,后者则靠超时重传机制。

超时重传

非常直观的理解超时重传机制,TCP发送的字节流,就是发送若干个TCP报文。由于底层协议不能保证顺序,所以给TCP报文编个号,如1、2、3、4、5(仅做举例,TCP的“编号”是字节流偏移)。接收者如果收到1、5、4、2,自己排成1、2、4、5,告诉发送方:“我在2之前的数据都收到了”。发送方久久等不到3之后的确认消息,就会重发3、4、5。

更严谨的说,TCP协议在发送数据的时候,设定一个定时器,当超过指定的时间后,没有收到对方的对应的 ACK 确认应答报文,就会重发该数据。没有收到对应ACK有可能是发送的报文丢失,也有可能是对应的 ACK 确认应答报文丢失。

这个指定的时间设置成多少合适呢?TCP 采用一种自适应算法,他记录一个报文的发出时间和相应收到确认的时间。这两个时间之差被称为报文段的 RTT(Round-Trip Time,往返时间)。TCP 保留了 RTT 的一个 加权平均 RTT,随着每次新测量的 RTT 变化。我们的超时重传时间略大于 RTT。

最基本的超时重传存在的问题是,超时周期可能较长。有什么办法可以加速呢?可以使用快速重传机制:TCP 规定当发送方接收到对同一个报文段的 3 个冗余 ACK 时,就可以确认丢失。例如,发送方发送了 1,2,3,4,5 的报文段,但是接受方只收到了 1,3,4,5,那么接收方就发送 3 个对 1 号报文的冗余 ACK,表示自己希望收到 2 号报文段。这种技术还用在 拥塞控制 中。

快速重传机制仍有问题。在上面的例子中,发送方在收到 3 个对 1 号报文的冗余 ACK,并不知道后续的3、4、5是否已经送达。最好是通过某种机制,让发送方只重传缺失的报文。这叫做选择确认机制,用到上面提到的Selective ACK 可选字段(也就意味着不是所有的TCP实现都支持,理论上TCP的实现在没收到2的情况下收到3、4、5,是可以把他们全部扔掉的)。仍以上面的例子举例,此时ACK=1,SACK=3-5,发送方就知道,只传2和后续的数据就可以了。

校验和

应用层往往使用哈希算法作为校验数据,如md5/sha-3。然而,他们的计算开销太大了。TCP的校验和使用异或。校验和的计算涉及以下几个步骤:

  1. 伪首部的构造:在计算校验和时,首先需要构造一个12字节的伪首部。伪首部包含源IP地址、目的IP地址、保留字节(置0)、传输层协议号(TCP为6)和TCP报文长度(包括TCP首部和数据)。
  2. 数据分组:将伪首部、TCP首部和TCP数据分为16位的字。如果总长度为奇数个字节,则在最后增加一个全为0的字节以形成16位字。
  3. 反码求和:将所有16位字进行反码相加,进位也要累加。计算时,校验和字段本身被置为0,以避免影响计算结果。
  4. 取反:最后,对计算结果取反,得到的值即为TCP的校验和,这个值会被填入TCP首部的校验和字段中。

这种方式可以检测出大多数传输错误,计算简单,开销小,但是也不能保证100%检测出所有错误。

流量控制和拥塞控制

由于网络的各层协议,在流量控制和拥塞控制上使用的思想是类似的,将他们统一放到流量控制和拥塞控制文章中。