TCP协议详解

发布时间 2024-01-11 17:36:23作者: Flamesky

本文围绕网络模型、TCP头部、TCP API交互流程、TCP超时重传、TCP滑动窗口、TCP拥塞控制以及Nagle算法这些模块展开讲解。

网络模型

网络模型有OSI(Open System Interconnection)七层模型、TCP/IP五层模型、TCP/IP四层模型,前两种模型都是学术上的概念,TCP/IP四层模型在实际中得到实践和应用。

三种网络模型的对应关系以及TCP/IP四层模型每层对应的协议如下图所示:

网络模型

说明:
RIP协议(Routing Information Protocol,路由信息协议):负责数据的包装、寻址和路由。
ICMP协议(Internet Control Message Protocol,网间控制报文协议):用来提供网络诊断信息,ping工具就应用了这个协议。
ARP协议(Address Resolution Protocol,地址解析协议):根据IP地址获取物理地址。
RARP协议(Reverse Address Resolution Protocol,反向地址转换协议):允许局域网的物理机器从网关服务器的 ARP 表或者缓存上请求其 IP 地址。


TCP头部

TCP头部
  • 16位端口,告知主机该报文段来自哪里(源端口)以及传给哪个上层协议或应用层序(目的端口)的。
  • 32位序号(sequence number, SYN),解决乱序问题。一次TCP通讯(三次握手到四次挥手)过程中,某一个传输方向上的字节流的每个字节的编号。值是随机初始值ISN(Initial Sequence Number,初始序号值)加上报文段中第一个字节在整个字节流中的偏移。
  • 32位确认号(acknowledgement number, ACK):用作于对另一方发送来的TCP报文段的响应,解决丢包问题。值是收到的TCP报文段的序号加一。
  • 4位头部长度:标识该TCP报文段有多少个32bit(4Byte)。因为4位最大能标识15,所以TCP头部最长是60Byte,前面固定长度是20Byte,所以TCP头部最短是20Byte
  • 6位TCP flag包含如下几项:
    URG标志,表示紧急指针是否有效。
    ACK标志,表示确认号是否有效。一般称携带ACK标志的报文段是确认报文段。
    PSH标志,提示接收端应用程序立即从TCP接受缓冲区读走数据。
    RST标志,表示要求对方重新建立连接。称携带RST标志的TCP报文段为复位报文段。
    SYN标志,表示请求建立一个连接。称携带SYN标志的TCP报文段为同步报文段。
    FIN标志,表示通知对方本端将关闭连接。称携带FIN标志的TCP报文段为结束报文段。
  • 16位窗口大小:是TCP流量控制的一个手段。这里说的窗口指的是接收通告窗口。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
  • 16位校验和,由发送端填充,接收端对TCP报文段执行CRC算法以效验TCP报文段在传输过程中是否损坏(包括TCP头部和数据部分)。这也是TCP可靠传输的一个重要保障
  • 16位紧急指针,是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。即这个字段是紧急指针相对当前序号的偏移,为紧急偏移。以让接收端迅速接受到紧急数据。TCP的紧急指针是发送端向接收端发送紧急数据的方法。

综上,需要注意的点:

  • TCP的包没有IP地址,那是网间层的工作;
  • sequence number解决乱序问题;
  • acknowledgement number解决丢包问题;
  • 16位窗口大小用于解决流控问题,使用的是著名的滑动窗口机制;
  • TCP flag,也就是包的类型,主要用于操控TCP状态流转;
  • 5元组(5-tuple),通常指由源IP,源端口,目标IP,目标端口,4层通信协议 (the layer 4 protocol)等5个字段来表示一个会话。

TCP状态流转

我们知道TCP是有连接的,UDP是无连接的。TCP的“连接”实际是通信的双方维护一个“连接状态”,让它看上去是好像有连接一样。

下图是三次握手和三次挥手的过程,其中客户端倒数第二个印错了,应该是FIN_WAIT_2。

三次握手与四次挥手

TCP为一个连接定义了11种状态,分别是
连接建立前:CLOSED, LISTEN, SYN_SENT, SYN_RCVD
数据发送中: ESTABLISHED
关闭连接: 被动关闭 CLOSE_WAIT(接收到FIN) LAST_WAIT(发送FIN,等待其ACK)
主动关闭 FIN_WAIT_1(主动发送FIN) FIN_WAIT_2(接收到被动关闭套接字发来的ACK) TIME_WAIT(接收到被动套接字发来的FIN,并发给其ACK,等待2MSL后回到CLOSED状态)
同时关闭 CLOSING

TCP状态流转图

需要注意的点:

  • TIME_WAIT状态会等待2MSL(Max Segment Lifetime)时间,之后状态回到CLOSED。等待2MSL期间可以重启服务器,但是要等2MSL才能重新连接。
  • TCP是全双工的连接,必须时必须关闭传和送两个方向上的连接
  • CLOSE_WAIT状态下,需要考虑还要不要发送数据给对方,没有了再变LAST_ACK。
  • FIN_WAIT_2状态下,不能再发送数据,只能接收数据。所以有可能会导致一方一直处于FIN_WAIT_2状态而另一方一直处于CLOSE_WAIT状态,直到应用层来决定关闭这个状态。
  • RST其实是TCP包头里的一个标志位,目的是为了在异常情况下关闭连接。RST是另一种关闭连接的方式,应用程序可以判断RST的真实性,即是否为异常中止。而同时打开和同时关闭则是两种特殊的TCP状态,发生的概率很小。RST一般出现于异常情况,归类为 对端的端口不可用 和 socket提前关闭。比如服务器crash了,CLOSED后收到TCP包返回RST。

TCP连接为什么要三次握手?
因为通信双方要互相通知对方自己的初始化SYN。

TCP断开连接为什么要四次挥手?
因为TCP是全双工连接,所以发送和接收方都需要FIN和ACK,有一方是被动的,所以看上去就成了四次挥手。
如果双方同时断开连接,也就是同时发送FIN,在收到对方的FIN后就会进入到CLOSING状态,收到ACK后就变成TIME_WAIT状态。换种说法就是FIN_WAIT_1状态在收到FIN请求后就会变CLOSING状态

为什么又TIME_WAIT状态?
假设最终的ACK丢失,服务端将重发FIN,客户端必须维护TCP状态信息以便可以重发最终的ACK,否则会发送RST。


API交互流程

这节没什么讲的,直接上图

API交互流程图

TCP超时重传

TCP传输有下面四种情况:

  1. 数据包顺利到达对端,对端顺利响应ACK
  2. 数据包丢失
  3. 数据包顺利到达,ACK报文中途丢失
  4. 数据包到达对端,但是对端异常未响应ACK或被对端丢弃

只有第一种是正常的,后面三种就属于异常情况,TCP就会超时重传。
TCP每发送一个报文段,就对这个报文段设置一次计时器。只要计时器设置的重传时间到了,但还没有收到确认,就要重传这一报文段,这个就叫做“超时重传”。
可以用telnet和tcpdump工具来看TCP是怎么超时重传的。

影响超时重传机制协议效率的一个关键参数是RTO(Retransmission TimeOut,超时重传时间)。
RTO的设置对于重传非常重要:

  • 设置长了,重发就慢,没有效率,性能差;
  • 设置短了,重发就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。

那么该怎么设置RTO呢?
RFC793定义的经典算法是先采样RTT(传输往返时间),然后对RTT做平滑计算,就可以求得RTO。
但是这个算法在在计算RTT时有多义性问题。往返时间的“返”时间点是收到ACK的时间点,“往”的时间该算第一次的还是算重传的,这对结果有影响。

Karn/Partridge Algorithm:Karn算法用了一个取巧的方式——只要一发生重传,就对现有的RTO翻倍。比如RTO为1s、2s、4s、8s...


TCP滑动窗口

TCP滑动窗口的作用:一是提供TCP的可靠性;二是提供TCP的流控特性。同时滑动窗口机制还体现了TCP面向字节流的设计思路。
TCP滑动窗口是一个16bit字段,代表的是窗口的容量,也就是TCP的标准窗口最大为65535个字节
TCP flag中还包含了一个TCP窗口扩大因子,option-kind为3,option-length为3,option-data取值范围是0~14。窗口扩大因子用来扩大TCP窗口,可把原来的16bit的窗口扩大为32bit。

发送方在发送缓存的数据可以分为4类:已经发送并得到对端ACK、已经发送未得到对端ACK、未发送但允许发送以及未发送不允许发送。
其中“已经发送未得到对端ACK”和“未发送但允许发送”这两部分数据称为发送窗口

对于接收方,接收缓存内存存在3种状态:已接收、未接收准备接收以及未接收未准备接收。其中“未接收准备接收”称为接收窗口

由于TCP是全双工连接,所以客户端和服务端双方都要维护一个“发送窗口”和一个“接收窗口”。要求发送窗口大小不能大于接收窗口

滑动窗口实现面向流的可靠性来源于“确认重传”机制:

  • 发送窗口要收到对端的ACK确认才会移动
  • 接收窗口只有在前面所有段都确认的情况下才会移动

TCP拥塞控制

TCP拥塞控制是防止过多数据注入网络,由四个核心算法组成:慢开始、拥塞避免、快速重传、快速恢复

慢开始和拥塞避免

发送维持一个拥塞窗口cwnd(congestion window)的状态变量。cwnd的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接收方的接收能力,发送窗口可能小于cwnd

慢开始的思路就是一开始不要发送大量的数据,先探测网络的拥塞程度,也就是由小到大逐渐增加拥塞窗口大小。慢开始的cwnd是指数增加,当cwnd大于慢开始门限ssthresh时,开始拥塞避免算法。拥塞避免的cwnd是线性增加

拥塞控制过程:

  • TCP连接初始化时,cwnd初始化为1;
  • 执行慢开始算法,cwnd指数增加,直到cwnd大于ssthresh执行拥塞避免算法,cwnd线性增加;
  • 当网络发生拥塞时,把ssthresh值设置为拥塞前的一半,cwnd设置为1。

快重传和快恢复

快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(目的是使发送方及早知道有报文段没有到达对方)。快重传算法规定,发送只要一连收到3个重复确认就应当立即重传对方尚未收到的报文段,而不用等待RTO。

快重传配合使用的还有快恢复:

  • 当发送方连续收到3个重复确认时,就对ssthresh执行乘法减少,直接减半,但是接下来并不执行慢开始算法。
  • 考虑到如果网络出现拥塞的话,就不会收到好几个重复确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是直接将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。

拥塞控制小结

拥塞控制有两套逻辑:
慢开始的cwnd是从1开始指数增加,拥塞避免的cwnd是线性增加。发生网络拥塞时,ssthresh减半,cwnd设置为1

收到对端连续三次重复确认时开始快重传和快恢复,快恢复策略是ssthresh减半,cwnd等于新的ssthresh。

拥塞控制

Nagle算法

RFC896 [Nagle 1984]中所建议的 Nagle 算法,默认是开启的,用户可以使用 TCP_NODELAY 选项来关闭Nagle算法

该算法要求一个 TCP 连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其他的小分组。相反,TCP 收集这些少量的分组,并在确认到来时以一个分组的方式发出去。该算法的优越之处在于它是自适应的:确认到达得越快,数据也就发送得越快。而在希望减少微小分组数目的低速广域网上,则会发送更少的分组.

Nagle发送规则

  • 如果包长度达到MSS,则允许发送;
  • 如果该包含有FIN,则允许发送;
  • 设置了TCP_NODELAY选项,则允许发送;
  • 若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;(前提是未设置TCP_CORK选项,和Nagle隐式地不发小包不同,TCP_CORK是显式地阻止小包发送)
  • 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。