本文主要是介绍Linux高性能服务器编程 总结索引 | 第3章:TCP协议详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
和IP协议相比,TCP协议 更靠近应用层,因此在应用程序中具有 更强的可操作性。一些重要的socket选项都和TCP协议相关
- TCP头部信息。TCP头部信息出现在 每个TCP报文段中,用于指定 通信的源端口号、目的端口号,管理TCP连接,控制两个方向的数据流
- TCP状态转移过程。TCP连接的任意一端 都是一个状态机。在TCP连接 从建立到断开的整个过程中,连接两端的状态机将经历不同的状态变迁
- TCP数据流。通过分析TCP数据流,就可以从网络应用程序外部 来了解应用层协议 和 通信双方交换的应用程序数据。这一部分将讨论 两种类型的TCP数据流:交互数据流 和 成块数据流。TCP数据流中有一种特殊的数据,称为紧急数据
- TCP数据流的控制。为了保证可靠传输 和 提高网络通信质量,内核需要对TCP数据流进行控制。这一部分讨论TCP数据流控制的两个方面:超时重传 和 拥塞控制
1、TCP服务的特点
1、传输层协议 主要有两个:TCP 协议和 UDP 协议。TCP 协议相对于 UDP 协议的特点是:面向连接、字节流和可靠传输
使用 TCP 协议通信的双方 必须先建立连接,然后 才能开始数据的读写。双方都必须 为该连接分配必要的内核资源,以管理连接的状态 和 连接上数据的传输。TCP 连接是全双工的,即双方的数据读写 可以通过一个连接进行。完成数据交换之后,通信双方都必须 断开连接以释放系统资源
TCP 协议的这种连接是 一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序 不能使用 TCP 服务。而无连接协议 UDP 则非常适合于广播和多播
2、字节流服务 和 数据报服务的区别:体现为 通信双方是否必须执行相同次数的读、写操作。当发送端应用程序 连续执行多次写操作时,TCP 模块先将这些数据放入 TCP 发送缓冲区中。当 TCP 模块真正开始发送数据时,发送缓冲区中这些等待发送的数据可能被封装成 一个或多个 TCP 报文段发出。因此,TCP 模块发送出的 TCP 报文段的个数 和 应用程序执行的写操作次数之间没有固定的数量关系
当接收端 收到一个或多个TCP报文段后,TCP模块 将它们携带的应用程序数据 按照TCP报文段的序号 依次放入 TCP接收缓冲区中,并通知应用程序读取数据。接收端应用程序 可以一次性将TCP接收缓冲区中的数据全部读出,也可以分多次读取,这取决于 用户指定的应用程序读缓冲区的大小。因此,应用程序执行的读操作次数 和 TCP模块接收到的TCP报文段个数之间 也没有固定的数量关系
综上所述,发送端执行的写操作次数 和 接收端执行的读操作次数 之间没有任何数量关系,这就是字节流的概念:应用程序对数据的发送和接收 是没有边界限制的
UDP则不然。发送端应用程序 每执行一次写操作,UDP模块 就将其封装成 一个UDP数据报并发送之。接收端必须及时 针对每一个UDP数据报执行 读操作(通过recvfrom系统调用),否则就会丢包。并且,如果用户没有指定足够的应用程序缓冲区 来读取UDP数据,则UDP数据将被截断
TCP 传输是可靠的
首先,TCP 协议 采用发送应答机制,即发送端 发送的每个 TCP 报文段 都必须得到接收方的应答,才认为这个 TCP 报文段传输成功
其次,TCP 协议 采用超时重传机制,发送端 在发送出一个 TCP 报文段之后 启动定时器,如果在定时时间内 未收到应答,它将重发该报文段
最后,因为 TCP 报文段最终是以 IP 数据报发送的,而 IP 数据报 到达接收端可能乱序、重复,所以 TCP 协议还会对接收到的 TCP 报文段重排、整理,再交付给应用层
2、TCP 头部结构
2.1 TCP 固定头部结构
其中的诸多字段 为管理 TCP 连接和控制数据流 提供了足够的信息
16 位端口号:告知主机 该报文段是来自哪里(源端口)以及 传给哪个上层协议或应用程序(目的端口)的。进行 TCP 通信时,客户端 通常使用 系统自动选择的临时端口号,而服务器则使用 知名服务端口号
32 位序号:一次 TCP 通信(从 TCP 连接建立到断开)过程中 某一个传输方向上的字节流的每个字节的编号。假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个 TCP 报文段中,序号值 被系统初始化为某个随机值 ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中 序号值将被系统设置成 ISN 加上 该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个 TCP 报文段传送的数据是 字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN+1025。另一个传输方向(从 B 到 A)的 TCP 报文段的序号值也具有相同的含义
32 位确认号:用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值加 1。假设主机 A 和主机 B 进行 TCP 通信,那么 A 发送出的 TCP 报文段 不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号
4 位头部长度:标识该 TCP 头部有多少个 32bit 字(4 字节)。因为 4 位最大能表示 15,所以 TCP 头部最长是 60 字节
6 位标志位包含如下几项:
- URG 标志,表示紧急指针 是否有效
- ACK 标志,表示确认号是否有效。称携带 ACK 标志的 TCP 报文段为确认报文段
- PSH 标志,提示接收端应用程序 应该立即从 TCP 接收缓冲区中 读走数据,为接收后续数据 腾出空间(如果应用程序 不将接收到的数据读走,就会一直停留在 TCP 接收缓冲区中)
- RST 标志,表示要求对方重新建立连接。称携带 RST 标志的 TCP 报文段为复位报文段
- SYN 标志,表示请求建立一个连接。称携带 SYN 标志的 TCP 报文段为同步报文段
- FIN 标志,表示通知对方本端要关闭连接了。称携带 FIN 标志的 TCP 报文段为结束报文段
16 位窗口大小:是 TCP 流量控制的一个手段。这里说的窗口,指的是 接收通告窗口。它告诉对方本端的 TCP 接收缓冲区 还能容纳多少字节的数据,这样对方 就可以控制发送数据的速度
16 位校验和:由发送端填充,接收端对 TCP 报文段执行 CRC 算法以检验 TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障
16 位紧急指针:是一个正的偏移量。它和序号字段的值相加 表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是 紧急指针相对当前序号的偏移,不妨称之为 紧急偏移。TCP 的紧急指针 是发送端向接收端发送紧急数据的方法
2.2 TCP 头部选项
TCP 头部的最后一个选项字段 是可变长的可选信息。这部分最多包含 40 字节,因为 TCP 头部最长是 60 字节(其中还包含前面讨论的 20 字节的固定部分)
选项的第一个字段 kind 说明选项的类型。有的 TCP 选项 没有后面两个字段,仅包含 1 字节的 kind 字段
第二个字段 length(如果有的话)指定该选项的总长度,该长度包括 kind 字段和 length 字段占据的 2 字节
第三个字段 info(如果有的话)是选项的具体信息
常见的 TCP 选项有 7 种
kind=0 是选项表结束选项
kind=1 是空操作(nop)选项,没有特殊含义,一般用于将 TCP 选项的总长度填充为 4 字节的整数倍
kind=2 是 最大报文段长度选项。TCP 连接初始化时,通信双方使用该选项来协商 最大报文段长度(Max Segment Size,MSS)。TCP 模块通常将 MSS 设置为(MTU - 40)字节(减掉的这 40 字节 包括 20 字节的 TCP 头部和 20 字节的 IP 头部)。这样携带 TCP 报文段的 IP 数据报的长度就不会超过 MTU(假设 TCP 头部 和 IP 头部都不包含选项字段,并且这也是一般情况),从而避免本机发生 IP 分片。对以太网而言,MSS 值是 1460(1500-40)字节
MTU 为 最大传输单元。它是指 数据链路层上 每一帧中可以传输的最大数据负载。简单来说,就是在物理链路上 能够无拆分地传输的最大数据包的大小(MTU只包括传输的有效负载部分,不包括帧的头部信息)
对于以太网而言,标准MTU通常为1500字节。这1500字节 用于承载IP数据包(包括IP头部和数据部分),但不包括 以太网帧头部和尾部
kind=3 是窗口扩大因子选项。TCP 连接初始化时,通信双方使用该选项 来协商 接收通告窗口的扩大因子。在 TCP 的头部中,接收通告窗口 大小是用 16 位表示的,故最大为 65535 字节,但实际上 TCP 模块允许的接收通告窗口大小 远不止这个数(为了提高 TCP 通信的吞吐量)。窗口扩大因子 解决了这个问题
假设 TCP 头部中的接收通告窗口大小是 N,窗口扩大因子(移位数)是 M,那么 TCP 报文段的实际接收通告窗口大小是 N 乘 2M,或者说 N 左移 M 位。注意,M 的取值范围是 0~14(占1个字节,但不是 0 ~ 65535)。可以通过修改 /proc/sys/net/ipv4/tcp_window_scaling 内核变量来 启用或关闭窗口扩大因子选项
和 MSS 选项一样,窗口扩大因子选项 只能出现在同步报文段中,否则将被忽略。但同步报段本身 不执行窗口扩大操作,即同步报文段头部的 接收通告窗口大小 就是该 TCP 报文段的实际接收通告窗口大小(因为窗口扩大因子 还没有被协商完成,在SYN报文段阶段 它只是被提议的一个值)。当连接建立好之后,每个数据传输方向的窗口扩大因子 就固定不变了(数据传输阶段的每一个TCP报文段中,接收通告窗口大小(N)都会使用窗口扩大因子进行扩展计算。即实际接收窗口大小为 N * 2M)
kind=4 是选择性确认(SACK)选项。TCP 通信时,如果某个 TCP 报文段丢失,则 TCP 模块会重传 最后被确认的 TCP 报文段后续的所有报文段,这样原先已经正确传输的 TCP 报文段 也可能重复发送,从而降低了 TCP 性能。SACK 技术 正是为改善这种情况而产生的,它使 TCP 模块 只重新发送丢失的 TCP 报文段,不用发送所有未被确认的 TCP 报文段。选择性确认选项 用在连接初始化时,表示是否支持 SACK 技术。可以通过修改 /proc/sys/net/ipv4/tcp_sack 内核变量来启用或关闭选择性确认选项
kind=5 是 SACK 实际工作的选项。该选项的参数 告诉发送方本端 已经收到并缓存的不连续的数据块,从而让发送端可以据此检查 并重发丢失的数据块。每个块边沿参数 包含一个 4 字节的序号。其中 块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿 和 块右边沿)之间的数据是没有收到的。因为 一个块信息占用 8 字节,所以 TCP 头部选项中 实际上最多可以包含 4 个这样的不连续数据块(考虑选项类型和长度占用的 2 字节:4 * 8 + 2 < 40)
kind=8 是时间戳选项。该选项提供了 较为准确的计算通信双方之间的回路时间(Round Trip Time, RTT)的方法,从而为 TCP 流量控制 提供重要信息。可以通过修改 /proc/sys/net/ipv4/tcp_timestamps 内核变量来 启用或关闭时间戳选项
2.3 使用 tcpdump 观察 TCP 头部信息
利用 tcpdump 抓取了一个数据包 并分析了其中的 IP 头部信息,本节分析 其中与 TCP 协议相关的部分
tcpdump 输出 Flags [S],表示该 TCP 报文段包含 SYN 标志,因此 它是一个同步报文段。如果 TCP 报文段 包含其他标志,则 tcpdump 也会将该标志的首字母 显示在 “Flags” 后的方括号中
seq 是序号值。因为该同步报文段是从 127.0.0.1.41621(客户端 IP 地址 和 端口号)到 127.0.0.1.23(服务器 IP 地址 和 端口号)这个传输方向上的 第一个 TCP 报文段,所以这个序号值 也就是此次通信过程中 该传输方向的 ISN 值。并且,因为这是整个通信过程中的第一个 TCP 报文段,所以它没有 针对对方发送来的 TCP 报文段的确认值(尚未收到任何对方发送来的 TCP 报文段)。
win 是 接收通告窗口的大小。因为 这是一个同步报文段,所以 win 值反映的是 实际的接收通告窗口大小
options 是 TCP 选项,其具体内容列在方括号中。mss 是发送端(客户端)通告的最大报文段长度。通过 ifconfig 命令查看回路接口的 MTU 为 16436 字节,因此可以预想到 TCP 报文段的 MSS 为 16396(16436-40)字节。sackOK 表示发送端支持并同意使用 SACK 选项。TS val 是发送端的时间戳。ecr 是时间戳回显应答。因为这是一次 TCP 通信的第一个 TCP 报文段,所以它针对对方的时间戳的应答为 0(尚未收到对方的时间戳)。紧接着的 nop 是一个空操作选项。wscale 指出发送端使用的 窗口扩大因子为 6。
接下来 分析 tcpdump 输出的字节码中 TCP 头部对应的信息,它从第 21 字节开始(去掉 IP 头部)
3、TCP 连接的建立和关闭
3.1 使用 tcpdump 观察 TCP 连接的建立和关闭
首先从 ernest-laptop 上执行 telnet 命令登录 Kongming20 的 80 端口,然后抓取这一过程中 客户端和服务器交换的 TCP 报文段
$ sudo tcpdump -i eth0 -nt '(src 192.168.1.109 and dst 192.168.1.108) or (src 192.168.1.108 and dst 192.168.1.109)'
$ telnet 192.168.1.109 80
192.168.1.109...
Trying 192.168.1.109...
Connected to 192.168.1.109.
Escape character is '^]'.
^]telnet> quit
Connection closed.
当执行 telnet 命令 并 在两台通信主机之间建立 TCP 连接后(telnet 输出 “Connected to 192.168.1.109”),输入 Ctrl+] 以调出 telnet 程序的命令提示符,然后在 telnet 命令提示符后 输入 quit 以退出 telnet 客户端程序,从而结束 TCP 连接(省略了大部分 TCP 头部信息)
因为整个过程 并没有发生 应用层数据数据的交换,所以 TCP 报文段的数据部分部分的长度(length)总是 0
第 1 个 TCP 报文段 包含 SYN 标志,因此 它是一个同步报文段,即 ernest-laptop(客户端)向 Kongming20(服务器)发起连接请求。同时,该同步报文段 包含一个 ISN 值为 535734930 的序号(初始序列号是一个32位的数字,用于标识TCP报文段中的数据 在整个数据流中的位置)。第 2 个 TCP 报文段 也是同步报文段,表示 Kongming20 同意与 ernest-laptop 建立连接。同时它发送自己的 ISN 值为 2159701207 的序号,并对第 1 个同步报文段进行确认。确认值是 535734931,即第 1 个同步报文段的序号值加 1
序号值是用来标识 TCP 数据流中的每一字节的。但同步报文段 比较特殊,即使它并没有携带任何应用层数据,它也要占用一个序号值。第 3 个 TCP 报文段是 ernest-laptop 对第 2 个同步报文段的确认。至此,TCP 连接就建立起来了。建立 TCP 连接的这 3 个步骤被称为 TCP 三次握手
从第 3 个 TCP 报文段开始,tcpdump 输出的序号值和确认值 都是相对初始 ISN 值的偏移(序号从0开始,表示相对位置,图中就是 ack = 1, seq = 1)。当然,可以开启 tcpdump 的 -S 选项来选择打印序号的绝对值
即使一个TCP报文段只携带确认信息(ACK),也需要携带序列号(seq),保持对连接中序列号的正确追踪。如果确认报文段(ACK)本身不携带任何数据,序列号的值 将与之前报文段的序列号相同,也就是保持不变。
后面 4 个 TCP 报文段是 关闭连接的过程。第 4 个 TCP 报文段包含 FIN 标志,因此 它是一个结束报文段,即 ernest-laptop 要求关闭连接。结束报文段 和同步报文段一样,也要 占用一个序号值。Kongming20 用 TCP 报文段 5 来确认该结束报文段。紧接着 Kongming20 发送 自己的结束报文段 6,ernest-laptop 则用 TCP 报文段 7 给予确认。实际上,仅用于确认目的的 确认报文段 5 是可以省略的,因为结束报文段 6 也携带了该确认信息。确认报文段 5 是否 出现在连接断开的过程中,取决于 TCP 的延迟确认特性
在连接的关闭过程中,因为 ernest-laptop 先发送结束报文段(telnet 客户端程序主动退出),故称 ernest-laptop 执行主动关闭,而称 Kongming20 执行被动关闭。
一般而言,TCP 连接是由客户端发起,并通过三次握手建立(特殊情况是 所谓同时打开的,还是双方都要三次握手)。TCP 连接的关闭过程 相对复杂一些。可能是 客户端执行主动关闭,比如前面的例子;也可能是 服务器执行主动关闭,比如服务器程序被中断 而强制关闭连接;还可能是同时关闭(和同时打开一样,非常少见)
3.2 半关闭状态
TCP 连接是 全双工的,所以它允许两个方向的数据传输 被独立关闭。换言之,通信的一端 可以发送结束报文段给对方,告诉它本端已经完成了数据的发送,但允许 继续接收来自对方的数据,直到对方也发送结束报文段 以关闭连接。TCP 连接的这种状态称为半关闭状态(很少见)
服务器和客户端应用程序 判断对方是否已经关闭连接的方法是:read 系统调用返回 0(收到结束报文段)
3.3 连接超时
前面 讨论的是很快建立连接的情况。如果客户端访问一个距离它很远的服务器,或者 由于网络繁忙,导致服务器 对于客户端发送出的同步报文段没有应答,对于提供可靠服务的 TCP 来说,它必然是 先进行重连(可能执行多次),如果重连仍然无效,则通知 应用程序连接超时
为了观察连接超时,模拟一个繁忙的服务器环境,在 ernest-laptop 上执行下面的操作:
$ sudo iptables -F # 清空当前所有的防火墙规则,-F 是 --flush 的缩写$ sudo iptables -I INPUT -p tcp --syn -i eth0 -j DROP
# 在 INPUT 链的最前面插入一条规则,阻止来自特定接口(eth0)的SYN包
-I INPUT:表示在 INPUT 链中插入规则。-I 的全称是 --insert,它将规则插入到链的最前面(即优先级最高)。
-p tcp:指定规则只适用于TCP协议的报文。
--syn:表示匹配带有SYN标志的TCP报文段(即用于发起新连接的TCP报文段)。这是用来检测TCP连接的发起阶段。
-i eth0:指定入站流量的接口为 eth0,即该规则只对通过网络接口 eth0 的流量生效。
-j DROP:表示将符合条件的报文丢弃。-j 表示跳转(jump),DROP表示不允许这些报文通过
iptable 命令 用于过滤数据包,这里 利用它来丢弃所有接收到的连接请求(丢弃所有同步报文段,这样客户端 就无法得到任何确认报文段)
从 Kongming20 上执行 telnet 命令登录 ernest-laptop,并用 tcpdump 抓取这个过程中双方交换的 TCP 报文段
保留了 tcpdump 输出的时间戳(不使用其 -t 选项)
一共抓取到 6 个 TCP 报文段,都是同步报文段,并且具有相同的序号值,这说明后面 5 个同步报文段 都是超时重连报文段
TCP 模块一共执行了 5 次重连操作,这是由 /proc/sys/net/ipv4/tcp_syn_retries 内核变量所定义的。每次重连的超时时间都增加一倍。在 5 次重连均失败的情况下,TCP 模块放弃连接 并通知应用程序
在应用程序中,可以修改连接超时时间
4、TCP 状态转移
TCP 连接的任意一端 在任一时刻都处于某种状态,当前状态 可以通过 netstat 命令查看。本节 要讨论的是 TCP 连接从建立到关闭的整个过程中 通信两端状态的变化
4.1 TCP 状态转移总图
1、先讨论服务器的 典型状态转移过程,此时 说的连接状态都是指 该连接的服务器端的状态
服务器 通过 listen 系统调用进入 LISTEN 状态,被动等待客户端连接,因此执行的是 被动打开。服务器一旦监听到 某个连接请求(收到同步报文段),就将该连接 放入内核等待队列中,并向客户端发送带 SYN 标志的确报文段。此时 该连接转移到 ESTABLISHED 状态。ESTABLISHED 状态是 连接双方能够进行双向数据传输的状态
当客户端主动关闭连接时(通过 close 或 shutdown 系统调用 向服务器发送结束报文段),服务器通过返回确认报文段 使连接进入 CLOSE_WAIT 状态。这个状态的含义很明确:等待服务器应用程序关闭连接。通常,服务器检测到客户端关闭连接后,也会立即 给客户端发送一个结束报文段来关闭连接。这将使连接转移到 LAST_ACK 状态,以等待客户端对结束报文段的最后一次确认。一旦确认完成,连接就彻底关闭了
2、讨论 客户端的典型状态转移过程,此时 说的连接状态都是指 该连接的客户端的状态
客户端通过 connect 系统调用 主动与服务器建立连接。connect 系统调用 首先给服务器发送一个同步报文段,使连接转移到 SYN_SENT 状态。此后,connect 系统调用 可能因为如下两个原因失败返回:
- 如果 connect 连接的目标端口号不存在(未被任何进程监听),或者 该端口仍被处于 TIME_WAIT 状态的连接所占用,则服务器 将给客户端发送一个复位报文段,connect 调用失败
- 如果 目标端口号存在,但 connect 在超时时间内 未收到服务器的确认报文段,则 connect 调用失败
connect 调用失败 将使连接立即返回到初始的 CLOSED 状态。如果客户端 成功收到服务器的同步报文段和确认,则 connect 调用成功返回,连接转移至 ESTABLISHED 状态
当客户端 执行主动关闭时,它将向服务器发送 一个结束报文段,同时连接进入 FIN_WAIT_1 状态。若此时 客户端收到服务器专门用于确认目的的 确认报文段(TCP 报文段 5),则连接转移至 FIN_WAIT_2 状态。当客户端处于 FIN_WAIT_2 状态时,服务器处于 CLOSE_WAIT 状态,这一对状态是可能发生半关闭的状态。此时如果服务器也关闭连接(发送结束报文段),则客户端将给予确认并进入 TIME_WAIT 状态
客户端从 FIN_WAIT_1 状态直接进入 TIME_WAIT 状态的一条线路(不经过 FIN_WAIT_2 状态),前提是处于 FIN_WAIT_1 状态的服务器 直接收到带确认信息的结束报文段(而不是先收到确认报文段,再收到结束报文段)。这种情况对应于 服务器不发送 TCP 报文段 5
3、处于 FIN_WAIT_2 状态的客户端 需要等待服务器发送结束报文段,才能转移至 TIME_WAIT 状态,否则 它将一直停留在这个状态。如果不是 为了在半关闭状态下继续接收数据,连接长时间地停留在 FIN_WAIT_2 状态并无益处。连接停留在 FIN_WAIT_2 状态的情况可能发生在:客户端执行半关闭后,未等服务器关闭连接就强行退出了。此时客户端连接 由内核来接管,可称之为孤儿连接(和孤儿进程类似)。Linux 为了防止 孤儿连接 长时间存留在内核中,定义了两个内核变量:/proc/sys/net/ipv4/tcp_max_orphans 和 /proc/sys/net/ipv4/tcp_fin_timeout。前者 指定内核能接管的孤儿连接数目,后者 指定孤儿连接 在内核中生存的时间
孤儿连接 是指在网络通信中,一端关闭连接 但另一端仍然认为连接存在的情况
例如:
客户端与服务器之间 建立了TCP连接
客户端 由于某种原因断开了连接(例如,客户端突然宕机或网络断开)
服务器端 没有及时收到断开通知,继续等待客户端的数据
孤儿进程 是指一个进程的父进程(一个进程是由另一个进程创建的,创建进程称为父进程,被创建的进程称为子进程)已经终止,而该进程仍在运行的状态,操作系统 会将这些孤儿进程的父进程重置为 init进程
4.2 TIME_WAIT 状态
客户端连接 在收到服务器的结束报文段(TCP 报文段 6)之后,并没有直接进入 CLOSED 状态,而是转移到 TIME_WAIT 状态。在这个状态,客户端连接 要等待一段长为 2MSL(Maximum Segment Life,报文段最大生存时间)的时间,才能完全关闭
TIME_WAIT 状态存在的原因有两点:
- 可靠地终止 TCP 连接
- 保证让迟来的 TCP 报文段有足够的时间被识别并丢弃
对于 第一点,用于 确认服务器结束报文段 6 的 TCP 报文段 7 丢失,那么 服务器将重发结束报文段。因此 客户端需要停留在某个状态 以处理 重复收到的结束报文段(即 向服务器发送确认报文段)。否则,客户端 将以复位报文段来回应服务器,服务器则认为这是一个错误,因为它期望的是 一个像 TCP 报文段 7 那样的确认报文段
对于 第二点,在 Linux 系统上,一个 TCP 端口 不能被同时打开多次(两次及以上)。当一个 TCP 连接处于 TIME_WAIT 状态时,将无法立即将占用着的端口 来建立一个新连接
反过来思考,如果没有 TIME_WAIT 状态,则应用程序能够 立即建立一个和刚关闭的连接相似的连接(这里说的相似,是指它们具有相同的 IP 地址 和 端口号)。这个新的、和原来相似的连接被称为 原来的连接的化身。新的化身 可能接收到属于原来的连接的、携带应用程序数据的 TCP 报文段(迟到的报文段)
因为 TCP 报文段的最大生存时间是 MSL,所以坚持 2MSL 时间的 TIME_WAIT 状态能够 确保网络上两个传输方向上尚未被接收到的、迟到的 TCP 报文都已经消失(被中转路由器丢弃)。因此,一个连接的新化身 可以在 2MSL 时间之后安全地建立,而绝对 不会接收到属于原来连接的应用程序数据
有时希望避免 TIME_WAIT 状态,因为 当程序退出后,希望能够立即重启它。但由于 处在 TIME_WAIT 状态的连接还占着端口,程序将无法启动(直到 2MSL 超时时间结束)
一个例子:在测试机器 ernest-laptop 上以客户端方式运行 nc(用于创建网络连接的工具)命令,登录本机的 Web 服务,并明确指定客户端使用 12345 端口 与服务器通信。然后从终端输入 Ctrl+C 终止客户端程序,接着又立即重启 nc 程序,以完全相同的方式再次连接本机的 Web 服务
使用 netstat 命令查看连接的状态。其输出显示,客户端程序被中断后,连接进入 TIME_WAIT 状态,12345 端口仍被占用,所以客户端重启失败
对客户端程序来说,通常不用担心 上面描述的重启问题。因为 客户端一般使用 系统自动分配的临时端口号 来建立连接,而由于随机性,临时端口号 一般和程序上一次使用的端口号(还处于 TIME_WAIT 状态的那个连接使用的端口号)不同,所以客户端程序一般可以立即重启。上面的例子仅仅是为了说明问题,强制客户端使用 12345 端口,这才导致立即重启客户端程序失败
5、复位报文段
在某些特殊条件下,TCP 连接的一端 会向另一端发送携带 RST 标志的报文段,即 复位报文段,以通知对方关闭连接 或 重新建立连接。讨论产生复位报文段的 3 种情况
5.1 访问不存在的端口
当客户端程序 访问一个不存在的端口时,目标主机 将给它发送一个复位报文段
考虑从 Kongming20 上执行 telnet 命令登录 ernest-laptop 上一个不存在的 54321 端口,并用 tcpdump 抓取该过程中两台主机交换的 TCP 报文段
$ sudo tcpdump -nt -i eth0 port 54321 #仅抓取发至和来自 54321 端口的 TCP 报文段
$ telnet 192.168.1.108 54321
Trying 192.168.1.108...
telnet: connect to address 192.168.1.108: Connection refused
telnet 程序的输出 显示连接被拒绝了,因为这个端口不存在。tcpdump 抓取到的 TCP 报文段内容如下:
1.IP 192.168.1.109.42001 > 192.168.1.108.54321: Flags [S], seq 21621375, win 14600, length 0
2.IP 192.168.1.108.54321 > 192.168.1.109.42001: Flags [R.], seq 0, ack 21621376, win 0, length 0
ernest-laptop 针对 Kongming20 的连接请求(同步报文段)回应了一个复位报文段(tcpdump 输出 R 标志)。因为复位报文段的 接收通告窗口大小为 0,所以可以预见:收到复位报文段的一端 应该关闭连接或者重新连接,而不能回应这个复位报文段
当客户端程序 向服务器的某个端口发起连接,而该端口仍被处于 TIME_WAIT 状态的连接所占用时,客户端程序也将收到复位报文段
5.2 异常终止连接
前面讨论的连接终止方式 都是正常的终止方式:数据交换完成之后,一方给另一方发送结束报文段。TCP 提供了异常终止一个连接的方法,即给对方发送一个复位报文段。一旦 发送了复位报文段,发送端 所有排队等待发送的数据都将被丢弃
5.3 处理半打开连接
服务器(或客户端)关闭 或者 异常 终止了连接,而对方 没有收到结束报文段(比如 发生了网络故障),此时,客户端(或服务器)还维持着 原来的连接,而服务器(或客户端)即使重启,也已经没有 该连接的任何信息了。将这种状态 称为半打开状态,处于这种状态的连接 称为半打开连接。如果客户端(或服务器)往处于 半打开状态的连接写入数据,则对方将回应一个复位报文段
在 Kongming20 上使用 nc 命令 模拟一个服务器程序,使之监听 12345 端口,然后从 ernest-laptop 运行 telnet 命令 登录到该端口上,接着拔掉 ernest-laptop 的网线,并在 Kongming20 上中断服务器程序。显然,此时 ernest-laptop 上运行的 telnet 客户端程序维持着 一个半打开连接。然后接上 ernest-laptop 的网线,并从客户端程序 往半打开连接写入 1 字节的数据 “a”
同时,运行 tcpdump 程序抓取整个过程中 telnet 客户端和 nc 服务器交换的 TCP 报文段
$ nc -l 12345 #在Kongming20上运行服务器程序
$ sudo tcpdump -nt -i eth0 port 12345
$ telnet 192.168.1.109 12345 #在ernest-laptop上运行客户端程序
Trying 192.168.1.109...
Connected to 192.168.1.109.
Escape character is '^]'. #此时断开ernest-laptop的网线,并重启服务器
a(回车) #向半打开连接输入字符a
Connection closed by foreign host.
tcpdump 抓取到的 TCP 报文段内容如下:
前 3 个 TCP 报文段是 正常建立 TCP 连接的 3 次握手的过程。第 4 个 TCP 报文段 由客户端发送给服务器,它携带着 3 字节的应用程序数据,这 3 字节依次是:字母 “a”、回车符 “\r” 和 换行符 “\n”。不过 因为服务器程序已经被中断,所以 Kongming20 对客户端发送的数据 回应了一个复位报文段 5
6、TCP 交互数据流
1、前面讨论了 TCP 连接及其状态,从本节开始 讨论通过 TCP 连接交换的应用程序数据
2、TCP 报文段所携带的应用程序数据 按照长度分为两种:交互数据 和 成块数据
交互数据 仅包含很少的字节。使用交互数据的应用程序(或协议)对实时性要求高,比如 telnet、ssh 等
成块数据的长度 则通常为 TCP 报文段允许的最大数据长度。使用成块数据的应用程序(或协议)对传输效率要求高,比如 ftp
在 ernest-laptop 上执行 telnet 命令登录到本机,然后在 shell 命令提示符后执行 ls 命令,同时用 tcpdump 抓取这一过程中 telnet 客户端和 telnet 服务器交换的 TCP 报文段
$ tcpdump -nt -i lo port 23 # 通过本地回环接口(lo,通常是 localhost 或 127.0.0.1)的端口 23 的网络数据包
$ telnet 127.0.0.1
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is ']'.
Ubuntu 9.10
ernest-laptop login: ernest (回车) #输入用户名
Password: (回车) #输入密码ernest@ernest-laptop:~$ ls (回车)
TCP 报文段 1 由客户端发送给服务器,它携带 1 字节的应用程序数据,即字母 “l”
TCP 报文段 2 是服务器对 TCP 报文段 1 的确认,同时回显字母 “l”(回显(echo)是指服务器接收到客户端发送的应用程序数据后,将该数据原封不动地返回给客户端,让客户端知道服务器 成功接收并处理了它的请求,回显 通常用于 简单的通信测试 或 某些协议的实现,如 Telnet 或 Echo)。TCP 报文段 3 是客户端对 TCP 报文段 2 的确认
第 4 ~ 6 个 TCP 报文段是针对字母 “s” 的上述过程
TCP 报文段 7 传送的 2 字节数据分别是:客户端键入的回车符和流结束符(EOF,本例中是 0x00)。TCP 报文段 8 携带服务器返回的客户查询的目录的内容(ls 命令的输出),包括该目录下文件的文件名 及 其显示控制参数。TCP 报文段 9 是客户端对 TCP 报文段 8 的确认。TCP 报文段 10 携带的 也是服务器返回给客户端的数据,包括一个回车符、一个换行符、客户端登录用户的 PS1 环境变量(第一级命令提示符,PS1环境变量代表命令提示符的格式,它显示在终端上,提示用户可以输入命令)。TCP 报文段 11 是客户端对 TCP 报文段 10 的确认
客户端针对服务器返回的数据 所发送的确认报文段(TCP 报文段 6、9 和 11)都不携带任何应用程序数据(长度为 0),而服务器 每次发送的确认报文段(TCP 报文段 2、5、8 和 10)都包含 它需要发送的应用程序数据。服务器的这种处理方式 称为延迟确认,即它不马上确认 上次收到的数据,而是在一段延迟时间后 查看本端是否有数据需要发送,如果有,则和确认信息一起发出。因为服务器对客户请求处理得很快,所以它发送确认报文段的时候总是有数据一起发送。延迟确认可以减少发送 TCP 报文段的数量。而由于用户的输入速度 明显慢于客户端程序的处理速度,所以客户端的确认报文段 总是不携带任何应用程序数据。在 TCP 连接的建立和断开过程中,也可能发生延迟确认
上例是 在本地回路运行的结果,在局域网中 也能得到基本相同的结果,但在广域网上就未必如此。广域网上的交互数据流 可能经受很大的延迟,并且,携带交互数据的微小 TCP 报文段数量一般很多(一个按键输入 就导致一个 TCP 报文段),这些因素都可能导致拥塞发生
解决该问题的一个简单有效的方法是使用 Nagle 算法
3、Nagle 算法 要求一个 TCP 连接的通信双方 在任意时刻都 最多只能发送一个未被确认的 TCP 报文段,在该 TCP 报文段的确认到达之前 不能发送其他 TCP 报文段。另一方面,发送方在等待确认的同时 收集本端需要发送的微量数据,并在确认到来时 以一个 TCP 报文段将它们全部发出。这样就极大地减少了网络上的微小 TCP 报文段的数量。该算法的另一个优点在于 其自适应性:确认到达得越快,数据也就发送得越快
7、TCP 成块数据数据流
考虑用 FTP 协议传输一个大文件。在 ernest-laptop 上启动一个 vsftpd 服务器程序,并执行 ftp 命令登录该服务器上,然后在 ftp 命令提示符后输入 get 命令,从服务器下载一个几百兆的大文件。同时用 tcpdump 抓取这一个过程中 ftp 客户端和 vsftpd 服务器交换的 TCP 报文段
$ sudo tcpdump -nt -i eth0 port 20 # vsftpd 服务器程序使用端口号 20
$ ftp 127.0.0.1
Connected to 127.0.0.1.
220 vsFTPd 2.3.0
Name (127.0.0.1:ernest): ernest (回车) # 输入用户名并回车
331 Please specify the password.
Password: (回车) # 输入密码并回车
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> get bigfile (回车) # 获取大文件 bigfile
seq 20583041:20589425:这个序列号范围是 不包含末尾的,类似数学中的区间记法 [start:end)
客户端发送的最后两个 TCP 报文段 17 和 18,它们分别是对 TCP 报文段 2 和 16 的确认(从序号值和确认值来判断)。由此可见,当传输 大量大块数据的时候,发送方 会连续发送多个 TCP 报文段,接收方 可以一次确认所有这些报文段。发送方在收到上一次确认后,能连续发送多少个 TCP 报文段 是由接收通告窗口(还需要考虑拥塞窗口)的大小决定的。TCP 报文段 17 说明客户端还能接收 30084×64 字节(本例中窗口扩大因子为 6,这意味着需要将窗口大小值乘以 26 = 64),即 192576 字节的数据
而在 TCP 报文段 18 中,接收通告窗口大小为 1748288 字节,即客户端能接收的数据量变小了。这表明客户端的 TCP 接收缓冲区有更多的数据未被应用程序读取而停留在其中,这些数据都来自 TCP 报文段 3~16 中的一部分。服务器收到 TCP 报文段 18 后,它至少(因为接收通告窗口可能扩大)还能连续发送的未被确认的报文段数量是 174828/16384 个,即 106 个(但一般不会连续发送这么多)。其中,16384 是成块数据的长度(见 TCP 报文段 1~16 的 length 值),很显然它小于但接近 MSS 规定的 16396 字节
服务器每发送 4 个 TCP 报文段 就传送一个 PSH 标志(tcpdump 输出标志 P)给客户端,以通知客户端的应用程序 尽快读取数据。不过这对服务器来说显然不是必需的,因为它知道客户端的 TCP 接收缓冲区中 还有空闲空间(接收通告窗口大小不为 0)
8、带外数据
有些传输层协议 具有带外数据的概念,用于 迅速通告对方 本端发生的重要事件。因此,带外数据 比普通数据(也称为带内数据)有更高的优先级,它应该 总是立即被发送,而不论发送缓冲区中是否有 排队等待发送的普通数据。带外数据的传输 可以使用一条独立的传输层连接,也可以映射到 传输普通数据的连接中。实际应用中,带外数据的使用 很少见,已知的仅有 telnet、ftp 等远程非活跃程序(在远程服务器或设备上 运行的程序或进程,在一定时间内 没有任何活动或用户交互)
UDP 没有实现带外数据传输,TCP 也没有真正的带外数据。不过 TCP 利用其头部中的紧急指针标志 和 紧急指针两个字段,给应用程序 提供了一种紧急方式。TCP 的紧急方式 利用传输普通数据的连接 来传输紧急数据。这种紧急数据的含义 和带外数据类似,因此后文也将 TCP 紧急数据 称为带外数据
介绍 TCP 发送带外数据的过程。假设一个进程 已经往某个 TCP 连接的发送缓冲区中 写入了 N 字节的普通数据,并等待其发送。在数据被发送前,该进程又向这个连接 写入了 3 字节的带外数据 “abc”。此时,待发送的 TCP 报文段的头部将被设置 URG 标志,并且紧急指针 被设置为指向最后一个带外数据的下一字节(进一步减去当前 TCP 报文段的序号值 得到 其头部中的紧急偏移值)
发送端 一次发送的多字节的带外数据中 只有最后一字节被当作带外数据(字母 c),而其他数据(字母 a 和 b)被当成了普通数据。如果 TCP 模块以多个 TCP 报文段 来发送图 3-10 所示 TCP 发送缓冲区中的内容,则每个 TCP 报文段 都将设置 URG 标志,并且它们的紧急指针 指向同一个位置(数据流中带外数据的下一个位置,所以 只有紧急指针指出的数据范围的最后一个字节被视为带外数据),但只有一个 TCP 报文段真正携带带外数据
考虑 TCP 接收带外数据的过程。TCP 接收端 只有在接收到紧急指针标志时 才检查紧急指针,然后根据紧急指针所指的位置 确定带外数据的位置,并将它读入一个特殊的缓存中。这个缓存只有 1 字节,称为 带外缓存。如果上层应用程序 没有及时将带外数据从带外缓存中读出,则后续的带外数据(如果有的话)将覆盖它
前面讨论的带外数据的接收过程 是 TCP 模块接收带外数据的默认方式。如果 给 TCP 连接设置了 SO_OOBINLINE 选项,则带外数据将 和普通数据一样被 TCP 模块存放在 TCP 接收缓冲区中。此时应用程序 需要像读取普通数据一样来读取带外数据
如何区分带外数据和普通数据呢?显然,紧急指针 可以用来指出带外数据的位置,socket 编程接口 也提供了系统调用来识别带外数据
9、TCP 超时重传
3.6 节~ 3.8 节中,讲述了 TCP 在正常网络情况下的数据流。从本节开始,讨论异常网络状况下(开始出现超时或丢包),TCP 如何控制数据传输 以保证其承诺的可靠服务
TCP 服务 必须能够重传超时时间内 未收到确认的 TCP 报文段。为此,TCP 模块 为每个 TCP 报文段都维护一个重传定时器,该定时器在 TCP 报文段第一次被发送时启动。如果超时时间内 未收到接收方的应答,TCP 模块将重传 TCP 报文段 并重置定时器。至于下次重传的超时时间如何选择,以及最多执行多少次重传,就是 TCP 的重传策略
在 ernest-laptop 上启动 iperf 服务器程序,然后从 Kongming20 上执行 telnet 命令登录该服务器程序。接下来,从 telnet 客户端发送一些数据(此处是“1234”)给服务器,然后断开服务器的网线 并再次从客户端发送一些数据给服务器(此处是“12”)。同时,用 tcpdump 抓取 这一过程中客户端和服务器交换的 TCP 报文段
iperf 是一个测量网络状况的工具,-s 选项表示 将其作为服务器运行。iperf 默认监听 5001 端口,并丢弃该端口上 接收到的所有数据,相当于一个 discard 服务器
TCP 报文段 1 ~ 3 是三次握手建立连接的过程,TCP 报文段 4 ~ 5 是客户端发送数据 “1234”(应用程序数据长度为 6,包括回车、换行两个字符,后同)及服务器确认的过程。TCP 报文段 6 是客户端第一次发送数据“12”的过程。因为服务器的网线被断开,所以客户端 无法收到 TCP 报文段 6 的确认报文段。此后,客户端对 TCP 报文段 6 执行了 5 次重传,它们是 TCP 报文段 7 ~ 11,这可以从每个 TCP 报文段的序号得知。此后,数据包 12 ~ 23 都是 ARP 模块的输出内容,即 Kongming20 查询 ernest-laptop 的 MAC 地址
保留了 tcpdump 输出的时间戳,以便推理 TCP 的超时重传策略。观察 TCP 报文段 6 ~ 11 被发送的时间间隔,它们分别为 0.2 s、0.4 s、0.8 s、1.6 s 和 3.2 s。由此可见,TCP 一共执行 5 次重传,每次重传超时时间 都增加一倍(因此,和 TCP 超时重连的策略相似)。在 5 次重传均失败的情况下,底层的 IP 和 ARP 开始接管,直到 telnet 客户端放弃连接为止
Linux 有两个重要的内核参数与 TCP 超时重传相关:/proc/sys/net/ipv4/tcp_retries1 和 /proc/sys/net/ipv4/tcp_retries2。前者指定在底层 IP 接管之前 TCP 最少执行的重传次数,默认值是 3。后者指定连接放弃前 TCP 最多可以执行的重传次数,默认值是 15 (一般对应 13 ~ 30 min)。在实例中,TCP 超时重传发生了 5 次,连接坚持的时间是 15 min(可以用 date 命令来测量)
虽然超时 会导致 TCP 报文段重传,但 TCP 报文段的重传 可以在超时之前,即快速重传
10、拥塞控制
10.1 拥塞控制概述
TCP 模块还有一个重要的任务,就是 提高网络利用率,降低丢包率,并保证 网络资源对每条数据流的公平性。这就是所谓的拥塞控制
拥塞控制的四个部分:慢启动(slow start)、拥塞避免(congestion avoidance)、快速重传(fast retransmit)和快速恢复(fast recovery)
拥塞控制的最终受控变量是 发送端向网络一次连续写入(收到其中第一个数据的确认之前)的数据量,称为 SWND(Send Window,发送窗口)。不过,发送端最终以 TCP 报文段来发送数据,所以 SWND 限制了 发送端能连续发送的 TCP 报文段数量。这些 TCP 报文段的最大长度(仅指数据部分)称为 SMSS(Sender Maximum Segment Size,发送者最大段大小),其值一般等于 MSS
发送端 需要合理地选择 SWND 的大小。如果 SWND 太小,会引起明显的网络延迟;反之,如果 SWND 太大,则容易导致网络拥塞。前文提到,接收方可通过 其接收通告窗口(RWND)来控制发送端的 SWND。但这显然不够,所以发送端引入了一个称为拥塞窗口(Congestion Window,CWND)的状态变量。实际的 SWND 值是 RWND 和 CWND 中的较小者
图 3-11 显示了拥塞控制的输入和输出(可见,它是一个闭环反馈控制)
TCP通过监控RTT(Round Trip Time,往返时间)、丢包等指标来调整拥塞窗口的大小。
当RTT增大、丢包出现时,TCP会认为网络出现拥塞,于是缩小CWND,减少发送速率,避免加重网络拥塞。反之,如果网络状况良好,RTT较短、丢包率低,CWND会逐渐增大,增加发送速率
TCP发送端根据网络反馈(如RTT、丢包等)调整CWND,CWND控制发送的数据量(SWND),而网络的状况和接收端的反馈(RWND)反过来影响发送端的CWND调整
10.2 慢启动和拥塞避免
1、TCP 连接建立好之后,CWND 将被设置成初始值 IW(Initial Window),其大小为 2 ~ 4 个 SMSS。但新的 Linux 内核提高了该初始值,以减小传输滞后。此时发送端最多能发送 IW 字节的数据。此后发送端每收到收端的一个确认,其 CWND 就按照式(3-1)增加:
CWND += min (N, SMSS) (3-1)
N 是此次确认中包含的之前 未被确认的字节数。这样一来,CWND 将按照指数形式(如果一次发送的数据量是 CWND 个报文段,那么 每一个报文段都有一个ACK返回,返回的每一个ACK 都会使 CWND 增加 SMSS 的大小。因此,在一个 RTT 周期内,CWND 增加的总量是 与之前发送的报文段数量成比例的)扩大,这就是所谓的慢启动。慢启动算法的理由是,TCP 模块 刚开始发送数据时 并不知道网络的实际情况,需要用一种试探的方式 平滑地增加 CWND 的大小
2、如果 不施加其他手段,慢启动必然使得 CWND 很快膨胀(可见慢启动其实不慢)并最终导致网络拥塞。因此 TCP 拥塞控制中定义了另一个重要的状态变量:慢启动门限(slow start threshold size, ssthresh)。当 CWND 的大小超过该值时,TCP 拥塞控制 将进入拥塞避免阶段
拥塞避免算法 使得 CWND 按照线性方式增加,从而减缓其扩大
- 每个 RTT 时间内 按照式 (3-1) 计算新的 CWND,而不论该 RTT 时间内 发送端收到多少个确认
- 每收到 一个对新数据的确认报文段,就按照式 (3-2) 来更新 CWND
CWND += SMSS * SMSS / CWND (3-2)(每个RTT内增加值 CWND 加在一起正好 都增加一个 SMSS)
以 SMSS 为单位来显示 CWND(实际上它是 以字节为单位的),以次数为单位 来显示 RTT,这只是为了方便讨论问题。此外,假设当前的 ssthresh 是 16 SMSS 大小
上面讨论了 发送端 在未检测到拥塞时 所采用的积极避免拥塞的方法。接下来介绍 拥塞发生时(可能发生 慢启动阶段 或者 拥塞避免阶段)拥塞控制的行为。不过 要先搞清楚发送端是如何判断拥塞已经发生的。发送端判断拥塞发生的依据 有如下两个:
- 传输超时,或者说 TCP 重传定时器溢出
- 接收到重复的确报文段
对第一种情况 仍然使用 慢启动和拥塞避免。对第二种情况 则使用 快速重传和快速恢复(如果是真的发生拥塞的话)。注意,第二种情况 如果发生在重传定时器溢出之后,则也被拥塞控制 当成第一种情况来对待
如果 发送端检测到 拥塞发生是由传输超时,即上述第一种情况,那么它将执行重传 并做如下调整:
ssthresh = max(FlightSize / 2,2 * SMSS)
CWMD <= SMSS
(3.3)
其中 FlightSize 是已经发送 但未收到确认的字节数。这样调整之后,CWMD 将小于 SMSS,那么也必然小于新的慢启动门限值 ssthresh(因为 一定不小于 SMSS 的 2 倍),故而拥塞控制 再次进入慢启动阶段
10.3 快速重传和快速恢复
1、在很多情况下,发送端 都可能接收到重复的确认报文段,比如 TCP 报文段丢失,或者 接收端 收到乱序 TCP 报文段并重排之(当接收端收到乱序的TCP报文段时,它会进行乱序处理 并发送重复的确认报文段(duplicate ACKs)给发送端。这个过程是TCP的一部分,旨在通知 发送端数据包的传输出现问题,并帮助发送端做出调整。这是 TCP 的可靠传输机制的一部分,用于确保数据的有序到达和处理)等
2、拥塞控制算法 需要判断 当收到重复的确认报文段时,网络是否真的发生了拥塞,或者说 TCP 报文段是否真的丢失了。具体做法是:发送端 如果连续收到 3 个重复的确认报文段,就认为是 拥塞发生了。然后它启用 快速重传和快速恢复算法 来处理拥塞
过程如下:
1)当收到第 3 个重复的确认报文段时,按照式 ssthresh = max(FlightSize / 2,2 * SMSS) CWMD <= SMSS(3-3)
计算 ssthresh,然后立即重传丢失的报文段(快速重传),并按照式(3-4)设置 CWND
CWND = ssthresh + 3 * SMSS (3-4)
2)每次收到 1 个重复的确认时,设置 CWND = CWND + SMSS。此时发送端可以发送新的 TCP 报文段(如果新的 CWND 允许的话)
3)当收到新数据的确认时,设置 CWND = ssthresh(ssthresh 是新的慢启动门限值,由第一步计算得到)
快速重传和快速恢复 完成之后,拥塞控制 将恢复到拥塞避免阶段,这一点由第 3 步操作可知
快速重传:
快速重传是TCP的拥塞控制机制中的一种策略,它用于 快速检测丢包并重传丢失的数据包,而不需要 等待超时重传机制的触发
过程
1)发送端 发送一系列数据包,假设数据包 1 丢失,而数据包 2、3 等成功到达接收端
2)接收端 收到数据包 2 后,由于丢失了数据包 1,它发送一个重复的 ACK,指示需要数据包 1
3)如果发送端 收到三个相同的 ACK,它会立即重传丢失的数据包 1,而不需要等到超时
快速恢复:
快速恢复 是紧接着快速重传进行的,它的目的是 在重传丢失数据包后,避免完全触发拥塞控制的慢启动阶段,以更快的速度恢复到合适的传输速率
快速恢复机制 假设网络并未完全拥塞,丢包是 轻度拥塞 或 瞬时波动的结果,因此 不需要将传输速率降到极低,而是通过调节拥塞窗口,快速恢复到 接近正常的传输速度
过程
1)在发送端检测到丢包 并执行快速重传后,TCP 不会直接进入慢启动,而是 进入快速恢复状态。
2)发送端将 拥塞窗口(CWND)减半,而不是完全重置为1。减半是 为了减少网络负载,但不至于 过度减小传输速率
3)TCP 继续按照当前窗口大小 传输数据,直到收到新的确认信息(ACK),确认所有数据都已经成功送达
4)一旦确认恢复正常,拥塞窗口 就会恢复线性增长,进入拥塞避免阶段
3、通过一个具体的例子 来展示TCP拥塞控制中的慢启动、拥塞避免、快速重传 和 快速恢复 是如何工作的
设 有一个TCP连接,发送端 正在向接收端发送数据。初始的 拥塞窗口(CWND) 设为1个报文段大小,慢启动阈值(ssthresh) 初始为32个报文段大小。TCP使用 MSS(Maximum Segment Size,最大报文段大小)作为单位进行拥塞窗口和报文段的计算
1)慢启动阶段:
初始状态:发送端的CWND为1,因此它只能发送1个报文段
2)第一次传输:
发送端发送报文段1
接收端收到报文段1并返回ACK,确认收到数据
发送端将CWND加倍,CWND = 2(慢启动规则:每次ACK到达时CWND加倍)
3)第二次传输:
发送端发送2个报文段(报文段2、3)
接收端收到并分别返回ACK
发送端再次加倍CWND,CWND = 4
4)第三次传输:
发送端发送4个报文段(报文段4、5、6、7)
接收端返回ACK后,CWND加倍至8
这个过程继续下去,CWND在每个RTT内快速增长,直到CWND达到慢启动阈值(ssthresh = 32)
5)拥塞避免阶段:
达到ssthresh:当CWND达到32时,慢启动阶段结束,TCP进入拥塞避免阶段
在拥塞避免阶段,CWND的增长速度变慢,不再是指数增长,而是线性增长,即每经过一个RTT,CWND只增加1个MSS大小。
例如:CWND = 32,经过一个RTT(所有数据包被确认后),CWND将变为33,而不是像慢启动那样翻倍
6)丢包发生,快速重传:
假设在此时,发送端发送了报文段33、34、35、36,但由于网络问题,报文段34丢失
接收端收到报文段33和报文段35、36后,发现它缺失了报文段34,因此它会持续发送重复ACK,确认它只接收到了报文段33,并表明它在等待报文段34
发送端收到3个重复的ACK,这触发了快速重传机制
发送端立即重传丢失的报文段34,而不必等到超时重传触发
7)快速恢复:
由于发生了丢包,发送端进入快速恢复状态,但它不会立即进入慢启动,而是通过调整CWND来保持一定的发送速率。
发送端会将 ssthresh 设置为当前 CWND 的一半,比如现在 CWND = 32,ssthresh 将被设置为 16
同时,CWND 减半:即从32降到16(即 CWND = ssthresh)
发送端继续传输剩余的数据,而不是立即恢复慢启动。此时,拥塞窗口继续按照线性增长的方式增加
一旦所有丢失的数据得到确认,拥塞窗口将继续逐步增加,以尽量 恢复到之前的传输速率
8)恢复到拥塞避免阶段:
在快速恢复完成后,发送端会进入拥塞避免阶段,CWND 从16 开始线性增长,直到遇到下一个丢包或超时
这个过程会继续进行,直到发生新的拥塞或者网络状况发生变化
总结这个例子中的各个阶段:
慢启动:CWND 从1开始,随着每次ACK的到达,CWND 呈指数增长,直到 CWND 达到慢启动阈值(ssthresh)
拥塞避免:一旦 CWND 达到 ssthresh,增长变为线性,以避免过快地增加传输速率,造成网络拥塞
快速重传:在发生丢包时,接收端通过发送3个或更多的重复 ACK 触发快速重传,发送端会立即重传丢失的数据包,而不是等待超时
快速恢复:在重传丢失数据包后,发送端将 CWND 减半,并将 ssthresh 设置为当前 CWND 的一半,随后继续线性增长 CWND,以较快的速度恢复正常传输速率
这篇关于Linux高性能服务器编程 总结索引 | 第3章:TCP协议详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!