【网络编程】 TCP 协议栈的知识汇总
文章目录
- 前言
- TCP 协议栈
- TCP 状态与 TCP 状态迁移图
- TCP 报文的各个组份
- TCP 报文的内部结构与 TCP 的可靠传输机制
- SEQ 和 ACK 与数据包乱序的处理
- SEQ 和 ACK 与重传机制处理丢包情况
- 拥塞控制与丢包情况的统计
- 慢启动
- 滑动窗口机制
- 总结——套接字与 TCB 控制块
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
前言
我是零声学院的小学徒,我虽然不是计算机科班出身,但我是学数学的,我认为对于一个我不熟悉的东西是需要作知识汇总的,尽量让自己理解清楚事情的全貌,并且对知识作出恰当的比喻和类比,以达本意。
本篇文章中,我将尝试像列文虎克的显微镜一样,从外到内,一步一步地放大的 TCP 的内部结构,从其数据结构层面解释整个 TCP 的数据传输过程。我在之前的文章中,通过介绍 P2P 的简易代码,并执行程序,通过 Wireshark 去抓包,记录整一个过程,发现了三次握手、数据传输和四次挥手,都是有各自的数据包传输,对应着若干篇的 TCP 报文。(原文链接 在此)
三次握手
数据传输过程
四次挥手的报文
现在,我们对 TCP 协议仅有直观上的认知,并无系统的认知。
TCP 协议栈
什么是 TCP 协议栈?它与 UDP 协议栈一样都是位于网络协议栈中的应用层的正下方——传输层。
所谓的应用层,即应用程序将自身的业务信息按照某种应用协议包装起来的一个程序层次。而所谓的传输层就是系统将应用层的信息再包装起来的一个层级。但是,我们在编程的时候,却没怎么需要我们亲自去编写 TCP 报文。这是因为,我们在编程的时候使用了底层的头文件,使用了底层的 POSIX-API。POSIX(Portable Operating System Interface,可移植操作系统接口)是一套由 IEEE(电气和电子工程师协会)制定的操作系统服务接口标准。POSIX 标准定义了操作系统应该提供的应用程序接口(API)以及相关的命令行工具和库函数,旨在使软件能够在不同的 UNIX 和类 UNIX 操作系统上更容易地移植和运行。
具体来说,我们在编程的时候,使用了以下的头文件。
#include <unistd.h> // close 函数
#include <sys/socket.h> // 创建和管理套接字。绑定地址、监听连接和接受连接。发送和接收数据。设置和获取套接字选项。 socket()、connect()、sendto()、recvfrom()、accept()
我们使用了 send
、recv
、accept
、connect
和 socket
等底层 I/O 函数,这使得 TCP 报文的编写有系统内核为我们代劳了。我们所常用的函数其实底层是相当复杂的,功能很完善。TCP 的信息传输过程如下,从三次握手建立连接,再到若干次的数据传输,最后是断开连接的四次挥手。该图里面展示了用户在调用 POSIX-API 函数的同时是如何影响 TCP 的传输进程。
这幅图里面还有 TCP 状态(比如 SYN_SENT 等),TCP 报文头部的某些信息(比如 ACK 标志、seq 序列号等等)。我会在下文分别展示这两者。
TCP 状态与 TCP 状态迁移图
TCP 状态其实是一个程序概念,它对应着一个状态常量枚举
typedef enum {CLOSED = 1, // 关闭LISTEN = 2, // 监听SYN_SENT = 3, // SYN发送SYN_RCVD = 4, // SYN接收ESTAB = 5, // 已建立FIN_WAIT1 = 6, // FIN等待1FIN_WAIT2 = 7, // FIN等待2CLOSE_WAIT = 8, // 关闭等待CLOSING = 9, // 同时关闭LAST_ACK = 10, // 最后确认TIME_WAIT = 11, // 等待关闭
} MIB_TCP_STATE;
我们区分服务器和客户端,描述双方在通信过程中(当然也是使用了 POSIX-API 的 I/O 函数,诸如 recv
、accept
、close
和 send
函数)各自程序中的状态变化。
连接建立 (三次握手):
- Server:
CLOSED
->LISTEN
(bind/listen) ->SYN_RCVD
(rcv SYN, snd SYN-ACK) ->ESTABLISHED
(rcv ACK) - Client:
CLOSED
->SYN_SENT
(connect/snd SYN) ->ESTABLISHED
(rcv SYN-ACK, snd ACK)
数据传输:
Client & Server: ESTABLISHED
(send/recv data)
连接终止 (四次挥手 - Client 主动关闭):
-
Client (主动关闭方):
ESTABLISHED
->FIN_WAIT_1
(close/snd FIN) ->FIN_WAIT_2
(rcv ACK for FIN) ->TIME_WAIT
(rcv FIN, snd ACK) -> [等待 2MSL] ->CLOSED
-
可能路径:
ESTABLISHED
->FIN_WAIT_1
->TIME_WAIT
(同时 rcv ACK & FIN) ->CLOSED
-
罕见路径(交叉挥手):
ESTABLISHED
->FIN_WAIT_1
->CLOSING
(rcv FIN before ACK) ->TIME_WAIT
(rcv ACK) ->CLOSED
-
-
Server (被动关闭方):
ESTABLISHED
->CLOSE_WAIT
(rcv FIN, snd ACK) ->LAST_ACK
(close/snd FIN) ->CLOSED
(rcv ACK for FIN)
我们使用一个图去总结上述的 TCP 状态变化。蓝色代表“客户端” 的状态变化、红色代表服务器的状态。
TCP 报文的各个组份
TCP 报文的具体字节安排如下,报文的对齐长度是 4 个字节,报文的长度是 4 的整数倍,报文长度至少 20 个字节
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 <--对齐长度为 4 个字节(32 位)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |E|C|E|U|A|P|R|S|F| |
| Offset|Reser|C|W|C|R|C|S|S|Y|I| Window |
| |-ved |N|R|E|G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer | <-- 基础长度 20 个字节
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if Data Offset > 5) | <-- 可变长度选项字段
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| DATA Payload | <-- 可变长度的数据荷载
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
首选先是源端口和目的端口,它们共占 4 个字节,一个端口占 2 个字节。有点常识的都知道,一个操作系统最多有 65536 个端口(0 ~ 65535),一个字节有 8 位,能表达 0 ~ 255 个的数字
65536=256×25665536=256 × 25665536=256×256
那不就刚好 2 个字节表达服务器客户端各自的端口。其余 TCP 报文的内容如下,
-
序列号(Sequence Number) - 可靠传输的基石
位置:第 4-7 字节
作用:标识发送数据的第一个字节的编号,解决乱序重组问题 -
确认号(Acknowledgment Number) - 确认机制的核心
位置:第 8-11 字节
作用:累积确认,表示接收方期望收到的下一个字节序号 -
Data Offset(数据偏移或头部长度)
位置:第 12 个字节起,往后的 4 位
含义:Data Offset 是一个 4 位的字段,它指示了 TCP 头部的长度,或者说,它指示了从 TCP 头部的开始到 TCP 数据(payload)的开始之间的距离。由于 Data Offset 是以 4 字节(32 位)为单位的,因此 TCP 头部的长度可以是 20 字节到 60 字节不等(即 5 到 15 个 4 字节单位)。
作用:确定 TCP 数据的位置:Data Offset 允许接收方知道从哪里开始读取 TCP 数据。支持可变长度的 TCP 头部。由于 TCP 头部可能包含可选字段(如窗口缩放、时间戳等),Data Offset 使得 TCP 头部可以灵活地扩展,以包含这些可选字段。 -
Reserved(保留位)
位置:第 12 个字节的第 4 位到第 6 位(4、5、6)
含义:Reserved 是一个 3 位的字段,它在 TCP 头部中被保留供将来使用。在 TCP 协议的当前版本中,这些位没有特定的用途,必须设置为 0。
作用:兼容性,Reserved 字段的存在是为了确保与未来版本的 TCP 协议兼容。如果将来需要引入新的功能或信息,这些位可以被重新定义和使用。在当前的 TCP 协议实现中,Reserved 字段的值在发送时应该设置为 0,接收方在处理 TCP 头部时通常会忽略这些位。 -
标志位(Flags) - 协议状态的控制开关
位置:自第 12 字节的最后一位,到第 13 个字节的整体,共 9 位。
关键标志:
标志位 | 名称 | 作用 | 可靠传输功能 |
---|---|---|---|
ECN | Explicit Congestion Notification 显式拥塞通知 | 支持 ECN 友好的拥塞控制算法 | 旨在减少在拥塞发生时数据包的丢失率 |
CWR | Congestion Window Reduced 拥塞窗口削减 | 告知接收方发送方已经因为网络拥塞而减小了其拥塞窗口 | 网络拥塞的响应 |
ECE | ECN-Echo | 作为“回声”(Echo)信号,由 TCP 接收方设置并发送回 TCP 发送方。 | 这通知发送方网络中的某个点已经检测到拥塞并标记了数据包 |
ACK | Acknowledgment | 确认号有效 | 启用确认机制 |
SYN | Synchronize | 建立连接同步序列号 | 初始序列号协商 |
FIN | Finish | 释放连接 | 有序关闭通道 |
RST | Reset | 强制断开连接 | 异常终止可靠传输 |
PSH | Push | 立即推送数据到应用层 | 减少传输延迟 |
-
窗口大小(Window Size) - 流量控制的载体
位置:第 14-15 字节
作用:通告接收方剩余缓冲区大小(单位:字节),动态控制发送速率(滑动窗口机制) -
校验和(Checksum) - 数据完整性的保障
位置:第 16-17 字节
作用:检测头部和数据的传输错误 -
紧急指针(Urgent Pointer) - 带外数据传输
位置:第 18-19 字节
配合标志位 URG=1 使用
作用:标记紧急数据结束位置
可靠传输扩展:确保关键数据优先处理 -
选项字段(Options)增强的可靠传输
-
数据荷载
现在 TCP 报文的内部结构已经清晰了,我们可以再次从 TCP 报文的内容角度去观察 TCP 传输过程中的行为。
ISN 代表“初始序列号”(Initial Sequence Number)。在 TCP(传输控制协议)中,ISN 是一个重要的概念,用于数据传输的可靠性和顺序性保证。
ISN 的作用:
- 确保数据顺序: TCP 使用序列号来标识它发送的每个字节的数据。ISN 就是这个序列号的起始值,它确保了每个 TCP 连接中数据传输的顺序性。
- 数据完整性: 通过使用 ISN 和后续的序列号,TCP 能够检测到数据包的丢失和重复,从而保证数据的完整性。
- 同步连接: 在建立 TCP 连接的过程中(三次握手),ISN 被用来同步两个通信端的序列号,确保双方都能按照正确的顺序接收和发送数据。
ISN 的生成,SN 通常在 TCP 连接建立时生成,其值是动态的,并不是固定的。有多种方法可以生成 ISN:
- 基于时间: 一种常见的方法是使用当前时间的一个函数来生成 ISN,这样可以保证即使在短时间内创建多个连接,ISN 值也不太可能重复。
- 随机生成: 为了提高安全性,避免潜在的序列号预测攻击,许多现代系统采用随机或伪随机数生成器来产生 ISN。
TCP 报文的内部结构与 TCP 的可靠传输机制
要平稳的地实现以上三次握手、数据传输和四次挥手,必须要设置一些可靠传输的机制。下面我会联系 TCP 报文内容和 TCP 可靠传输机制,来具体深入了解 TCP 报文。
SEQ 和 ACK 与数据包乱序的处理
序列号:TCP 为发送的每个字节分配一个序列号。发送方在发送数据时为每个字节分配一个唯一的序列号,接收方使用这些序列号来重新排序接收到的数据。
确认号:接收方在发送确认(ACK)时,会包含下一个期望接收的字节的序列号。例如,如果接收方正确接收到序列号为 100 的数据字节,它会发送一个确认号为 101 的 ACK,表示它已经接收到序列号 100 的数据,并期望下一个序列号为 101 的数据。
- step1、接收方缓存乱序数据:当接收方接收到乱序的数据包时,它会将这些数据包缓存起来,而不是立即交给上层应用。接收方会根据序列号重新排序这些数据包。
- step2、请求重传:如果接收方发现数据包丢失(例如,由于没有收到期望的下一个序列号的数据包),它会发送重复的 ACK,请求发送方重传丢失的数据包。
- step3、确认号指导重排序:接收方通过发送确认号来指导发送方哪些数据已经成功接收。发送方根据这些确认号来调整其发送窗口和重传策略。
示例:
假设发送方发送了三个数据包,序列号分别为 100、101 和 102,每个数据包包含 100 字节的数据:
1、发送方发送数据:
- 数据包 1:序列号 100,数据 “ABCDEFGHIJ”
- 数据包 2:序列号 101,数据 “JKLMNOPQRS”
- 数据包 3:序列号 102,数据 “TUVWXYZ”
2、数据包乱序到达:
- 接收方先收到数据包 2 和 3,序列号分别为 101 和 102。
- 接收方缓存这两个数据包,因为它还在等待序列号为 100 的数据包。
3、接收方请求重传:
- 接收方向发送方发送重复的 ACK,确认号为 101,表示它已经接收到序列号 100 之后的数据,并期望下一个序列号为 101 的数据。
4、发送方重传:
- 发送方收到重复的 ACK 后,重传序列号为 100 的数据包。
5、接收方重组数据:
- 接收方收到序列号为 100 的数据包后,将所有数据包按序列号顺序重组,并将重组后的数据交给上层应用。
SEQ 和 ACK 与重传机制处理丢包情况
- 超时重传(Retransmission Timeout, RTO):如果发送方在一定时间内没有收到某个数据包的确认 ACK 包,它会假设该数据包丢失,并进行重传。检测丢包并自动重传,动态 RTO 计算:基于 RTT(往返时间)自适应调整。
RTO=α×SRTT+β×RTTVAR(α=1,β=4)RTO=α×SRTT+β×RTTVAR \ (α=1,β=4)RTO=α×SRTT+β×RTTVAR (α=1,β=4) - 快速重传:如果发送方在短时间内收到三个或更多的重复 ACK 包,它会立即重传丢失的数据包,而不必等待超时计时器到期。
拥塞控制与丢包情况的统计
网络的流畅与否是不由自己所控制的,一旦网络拥塞就有可能会丢包。我们要控制拥塞,减少自身发出的流量。同时还要,通过丢包的情况识别出当前网络的拥塞情况。
1、窗口大小 (Window Size - 16 bits):
- 作用: 这是接收方通告给发送方的接收窗口 (rwnd)。它表示接收方当前还有多少可用的缓冲区空间来接收数据。
- 与拥塞控制的联系:
- 流量控制的基础: 发送方发送的数据量不能超过接收方通告的窗口大小,这防止了接收方被淹没(流量控制)。
- 拥塞窗口的上限: 发送方实际能发送的数据量还受限于它自己计算出的拥塞窗口 (
cwnd
)。发送方的有效发送窗口 =min(cwnd, rwnd)
。拥塞控制算法(如慢启动、拥塞避免)的核心就是动态调整cwnd
。接收窗口rwnd
是 cwnd 调整的一个硬性上限,确保发送速率不会超过接收方的处理能力。没有这个字段,发送方就不知道接收方的处理能力,拥塞控制就失去了一个重要的约束。
2、确认号 (Acknowledgment Number - 32 bits):
- 作用: 表示接收方期望收到的下一个字节的序号。确认了该序号之前的所有字节都已正确接收。
- 与拥塞控制的联系:
- ACK 驱动: TCP 是基于确认的协议。拥塞控制算法(尤其是基于丢包的算法)严重依赖 ACK 的到达。
- 测量
RTT
: 通过记录发送数据包的时间戳和接收到对应 ACK 的时间戳,可以计算往返时间 (RTT) 及其变化(RTT 方差)。RTT 是计算重传超时 (RTO) 的关键参数。RTO 是判断数据包是否丢失(网络可能拥塞)的主要依据。头部本身不存时间戳(标准头部),但 ACK 号关联了发送和接收时间(通常在 TCP 选项或操作系统内核中实现时间戳)。 - 检测丢包:
-
超时重传: 如果在一个 RTO 内没有收到某个数据段的 ACK,发送方认为该段丢失(网络拥塞的强烈信号),触发拥塞控制算法(如将 cwnd 重置为 1 MSS,进入慢启动)。
-
重复 ACK: 当接收方收到失序的数据段时,它会重复发送对最后一个按序到达字节的 ACK(即重复 ACK)。发送方收到一定数量(通常是 3 个)的重复 ACK 时,推断中间有数据包丢失(可能是轻微拥塞),触发快速重传,并进入快速恢复阶段调整 cwnd。ACK 号是识别重复 ACK 的关键。
-
3、序列号 (Sequence Number - 32 bits):
- 作用: 标识该 TCP 段中第一个数据字节的序号。
- 与拥塞控制的联系:
- 数据包标识: 序列号与 ACK 号配合,才能唯一确定哪些数据已被确认,哪些数据可能丢失需要重传。拥塞控制算法对丢包的反应(如调整 cwnd)就是基于这些丢失事件。
- 计算已发送未确认数据量: 发送方需要跟踪序列号范围,以确定当前有多少数据在传输中(在途字节数)。这必须小于或等于 min(cwnd, rwnd)。拥塞控制算法通过调整 cwnd 来限制这个在途数据量。
4、ECE (ECN-Echo ) 和 CWR (Congestion Window Reduced ):
- 作用: 这两个标志位用于支持显式拥塞通知 (ECN)。
- 与拥塞控制的联系:
- 支持 ECN 的路由器在即将发生拥塞(但还未丢包)时,会在 IP 头部标记数据包。
- 支持 ECN 的接收方在收到被标记的数据包后,会在发回给发送方的 ACK 包中设置 ECE 标志位。
- 发送方收到带有 ECE 标志的 ACK 后,知道网络即将拥塞(显式信号),会像处理轻微丢包一样(例如,将 cwnd 减半),并设置下一个数据包的 CWR 标志位,告知接收方它已降低发送速率。这避免了实际丢包,是一种更主动、高效的拥塞控制信号传递机制。
- ACK:
- 作用: 表示该报文段中的确认号字段有效。
- 与拥塞控制的联系: 所有携带有效确认信息的报文段(即大多数报文段)都会设置此标志。它是 ACK 机制的基础,而 ACK 机制是驱动拥塞控制(测量 RTT、检测丢包、增加 cwnd)的核心动力源。
简单比喻:想象一条繁忙的公路(网络)和送货卡车(TCP 数据包)。
- 序列号/确认号: 像送货单号和签收单号,用来确认哪些货物(数据)送到了,哪些可能丢了。
- 窗口大小: 像仓库管理员(接收方)告诉发货商(发送方):“我的仓库还能放多少货”。发货商自己还有个“道路通畅度评估员”(拥塞控制算法)。
- 拥塞窗口: 评估员根据路况(丢包、延迟、ECE 信号)决定:“这次最多只能发这么多车出去,不然路要堵了”。
- ECE/CWR: 像路边的电子指示牌(路由器)告诉司机:“前方拥堵,请减速!”,司机(接收方)把消息带回给发货商(发送方),发货商(发送方)就减少发车量,并回复说“知道了,已减速”。
- ACK 标志: 签收单本身。
慢启动
当我们启动拥塞控制时候,我们会主动降低传输速度,以适应网络拥塞情况。但一直低速传输也不是办法,我们需要一点一点把速度提升上去。
慢启动的核心思想是:在连接建立之初或从拥塞中恢复时,发送方从一个很小的拥塞窗口 (cwnd) 开始(通常为 1 个 MSS),然后每收到一个有效的 ACK,就将 cwnd 增加 1 个 MSS。这导致 cwnd(以及发送速率)在每个往返时间 (RTT) 内大约翻倍,从而实现指数级增长。但是,窗口大小的增长也是有一个限度的。
1、建立连接 (SYN/SYN-ACK) 的阶段 - 确定 MSS:
- 头部作用: 在 TCP 三次握手阶段(SYN 和 SYN-ACK 报文),双方在 TCP 头部的选项字段中协商 最大段大小 (MSS)。MSS 表示本端愿意接收的最大 TCP 数据段长度(不包括 IP 和 TCP 头部)。
- 与慢启动的关系: MSS 是慢启动的基本单位。初始 cwnd 通常设置为 1 * MSS 或 2 * MSS(现代实现如 Linux 常用 10 * MSS)。慢启动过程中 cwnd 的增加量也是以 MSS 为单位。没有 MSS 的协商,慢启动的初始窗口大小和增长步长就无法确定。
2、ACK 的驱动作用 (ACK Flag + Acknowledgment Number):
- 头部作用: 接收方发送的 ACK 报文会设置 ACK 标志位,并在 Acknowledgment Number (确认号) 字段中指示期望收到的下一个字节序号,确认了之前所有数据的成功接收。
- 与慢启动的核心关系: 这是慢启动增长机制的生命线。
- 触发增长: 慢启动算法规定:每收到一个有效的 ACK(即确认了新数据的 ACK,非重复 ACK),cwnd 就增加 1 个 MSS。ACK 报文头部的存在和内容(确认号)直接触发了 cwnd 的增长。
- 计算增长量: 发送方根据收到的 ACK 确认号,知道哪些数据已被确认,从而计算出有多少新的“信用”可用(即 cwnd 增加了多少),并据此发送新的数据段。没有 ACK 报文头部的驱动,慢启动的指数增长就无从谈起。
- 测量 RTT (间接): 虽然标准 TCP 头部不直接包含时间戳,但发送方通过记录数据包发送时间和对应 ACK 的到达时间(由 ACK 号关联),可以估算 RTT。RTT 决定了 ACK 返回的速率,从而影响了 cwnd 增长的实际速度(例如,RTT 短,ACK 回得快,cwnd 增长就快)。
3、接收窗口的限制 (Window Size):
- 头部作用: 接收方在每个 ACK 报文(或数据报文)的 Window Size (窗口大小) 字段中,通告其当前的接收窗口 (rwnd),表示剩余的接收缓冲区大小。
- 与慢启动的关系: 发送方的实际发送窗口是 min(cwnd, rwnd)。在慢启动阶段:
- 初始上限: 即使慢启动算法允许 cwnd 快速增长,发送的数据量也不能超过 rwnd。如果接收方通告的 rwnd 很小(例如,接收方处理能力有限或缓冲区小),它会限制慢启动阶段能达到的最大发送速率,即使网络带宽很充裕。
- 动态约束: 接收方可能随着处理数据动态调整 rwnd。慢启动过程中 cwnd 的增长必须时刻遵守 rwnd 这个硬性上限。
4、慢启动的结束与拥塞避免的开始:
- 头部作用: 慢启动何时结束?它要么增长到 ssthresh (慢启动阈值),要么检测到拥塞(丢包)。
- 丢包检测 (序列号 + ACK 号 + 超时/重复ACK): 丢包是拥塞的主要信号。慢启动会因此结束并进入恢复状态。
- 超时重传: 如果发送方在超时时间 (RTO,基于 RTT 估算) 内没有收到某个数据段的 ACK,它会认为该段丢失。这通常会导致 ssthresh 设置为当前 cwnd 的一半,cwnd 重置为 1 MSS,并重新开始慢启动。
- 重复ACK: 收到 3 个重复 ACK (对同一个序列号的重复确认) 会触发快速重传和快速恢复。在快速恢复中,cwnd 会被减半(或做其他调整)并加 3 MSS,然后进入拥塞避免阶段。慢启动暂停。
- 到达 ssthresh: ssthresh 是发送方维护的一个状态变量(不在头部传输)。当 cwnd 增长到 ssthresh 时,慢启动结束,进入拥塞避免阶段(线性增长)。
- 丢包检测 (序列号 + ACK 号 + 超时/重复ACK): 丢包是拥塞的主要信号。慢启动会因此结束并进入恢复状态。
5、显式拥塞通知 (ECE Flag) - 可选但相关:
- 头部作用: 如果支持 ECN,路由器在即将拥塞时会标记 IP 包。接收方在 ACK 中设置 ECE (ECN-Echo) 标志位通知发送方。
- 与慢启动的关系: 收到带有 ECE 标志的 ACK 会被视为一个拥塞信号。发送方会将 ssthresh 设置为当前 cwnd 的一半(或类似比例),并将 cwnd 设置为 ssthresh(或减半),然后直接进入拥塞避免阶段。这相当于在没有发生实际丢包的情况下提前结束了慢启动,是一种更主动的拥塞控制。
简单来说:想象慢启动是一个需要不断加油(增长 cwnd)的发动机。
- ACK 报文(及其头部) 就是燃油。每一个有效的 ACK 到达,就给发动机加一次油(cwnd += MSS)。
- 窗口大小字段 是限速器,防止发动机转速(发送速率)超过接收方车道的承受能力。
- 序列号/确认号 是油量表和控制单元,用来精确计量加了多少油、哪些油已经消耗(数据已确认)。
- MSS 选项 定义了每桶油的大小(增长单位)。
- 丢包/重复ACK/ECE(通过头部信息推断或显式传递)是过热警报,告诉发动机需要立即降速并改变运行模式(结束慢启动)。
滑动窗口机制
滑动窗口机制旨在 解决发送方和接收方处理速度不匹配的问题(流量控制),同时允许发送方连续发送多个报文段而不必等待每个报文的确认(提高效率,而且不会乱序),并确保数据的按序交付(可靠性)。
核心组件:
-
发送窗口: 位于发送方,表示当前允许发送的、未被确认的字节序列范围。其大小受两个因素限制:
发送窗口=min(拥塞窗口, 接收方通告窗口)
。 -
接收窗口: 位于接收方,表示接收缓冲区中当前可用于存储新数据的空间大小。接收方通过 TCP 头部的
Window Size
字段将这个值通告给发送方,这就是通告窗口。 -
窗口边界: 由序列号定义。
SND.UNA
:发送未确认。最早发送但尚未收到 ACK 的字节序列号。发送窗口的左边界。SND.NXT
:下一个要发送。下一个将要发送的字节序列号。位于发送窗口内。RCV.NXT
:下一个期望接收。接收方期望收到的下一个字节序列号。接收窗口的左边界。RCV.NXT + RCV.WND
:接收窗口的右边界(RCV.WND 是当前接收窗口大小)。
TCP 头部与滑动窗口的关系:
1、序列号 SEQ:
- 作用: 在数据报文中,Sequence Number 标识该报文段中第一个数据字节的序号。
- 与滑动窗口的关系: 发送方使用序列号填充发送窗口内的数据段。接收方根据序列号判断数据段是否在接收窗口内 (RCV.NXT <= seq < RCV.NXT + RCV.WND),是否按序到达,以及如何重组数据。序列号是界定窗口内数据位置的基础坐标。
2、确认号 ACK:
- 作用: 在 ACK 报文中,Acknowledgment Number 表示接收方期望收到的下一个字节的序号(即 RCV.NXT),确认了该序号之前的所有数据都已正确按序接收。
- 与滑动窗口的关系: 这是推动发送窗口滑动的核心动力!
- 当发送方收到一个有效的 ACK(确认了新数据),它知道 Acknowledgment Number 之前的数据已被接收方确认。
- 发送方将 SND.UNA 更新为收到的 Acknowledgment Number。
- 发送窗口向右滑动: 发送窗口的左边界 (SND.UNA) 移动到新的确认号位置,释放了已被确认数据的空间,允许发送新的数据(只要还在 SND.UNA + min(cwnd, 接收通告窗口) 范围内)。
- 同时,确认号也告知了发送方接收方的 RCV.NXT 位置(即接收窗口的左边界)。
3、窗口大小:
- 作用: 在任何包含 ACK 标志的报文段(数据段或纯 ACK 段)中,Window Size 字段(16 位)都携带了接收方当前的通告窗口大小 (RCV.WND)。
- 与滑动窗口的关系: 这是流量控制的直接体现。
- 接收方通过这个字段动态告知发送方其当前的接收能力(剩余缓冲区大小)。
- 发送方收到包含新 Window Size 的报文后,立即更新其对接收方通告窗口大小的认知。
- 发送窗口的大小被重新计算: 发送窗口 = min(拥塞窗口, 新通告窗口)。
- 如果新通告窗口变小:
- 可能限制发送方发送新数据 (SND.NXT 不能超过 SND.UNA + 新发送窗口)。
- 如果 SND.NXT 已经超出了新的发送窗口边界,发送方必须停止发送新数据(零窗口)。
- 如果新通告窗口变大(或从零变为非零):
- 发送窗口变大(或从零窗口恢复),允许发送方发送更多新的数据(或解除阻塞)。
- 这个字段直接决定了发送窗口的右边界 (SND.UNA + min(cwnd, 通告窗口))。
4、ACK 标志:
- 作用: 表示报文中的 Acknowledgment Number 字段有效。
- 与滑动窗口的关系: 绝大多数携带数据的报文和所有纯 ACK 报文都设置此标志。它是接收方反馈接收状态(确认号、窗口大小)的前提。没有 ACK 标志,接收方就无法告知发送方其接收进度和可用空间,滑动窗口就无法滑动和调整大小。
滑动窗口机制如何影响 TCP 报文的发送和接收:
- 发送方行为:
- 只能发送序列号落在当前发送窗口 (SND.UNA 到 SND.UNA + min(cwnd, 通告窗口)) 内的数据。
- 维护一个定时器,为发送窗口内最早未确认的段计时(超时重传)。
- 收到 ACK 后,滑动发送窗口(更新 SND.UNA),可能释放空间发送新数据。
- 收到新的 Window Size 后,重新计算发送窗口大小,可能停止发送或开始发送。
- 接收方行为:
- 只接收序列号落在当前接收窗口 (RCV.NXT 到 RCV.NXT + RCV.WND - 1) 内的数据。
- 对按序到达的数据,交付给应用层,更新 RCV.NXT,向右滑动接收窗口。
- 对失序到达但在窗口内的数据,缓存起来。
- 对落在窗口外的数据(旧重复包或未来包),通常丢弃或发送重复 ACK(取决于实现)。
- 在发送 ACK 时(可能随数据或单独发送):
- 将 Acknowledgment Number 设置为当前的 RCV.NXT(期望的下一个字节)。
- 将 Window Size 设置为当前的 RCV.WND(可用接收缓冲区大小)。
- 这个 ACK 报文就是接收方控制发送方滑动窗口(流量控制)的“遥控器”。
简单比喻:
想象一个传送带(字节流)连接发送方和接收方仓库。
- 发送窗口: 发送方传送带上允许放置但尚未签收的货物区域。
- 接收窗口: 接收方仓库当前空余的货架空间大小。
- TCP 报文(数据): 传送带上的货物箱。每个箱子有唯一编号(序列号)。
- TCP 报文(ACK): 接收方发回的签收单和仓库容量更新单。
- 确认号: 签收单上写:“编号 X 之前的货都收到了,下次请从 X 号开始发”。这告诉发送方可以把传送带(发送窗口)向前滚动到 X 的位置。
- 窗口大小: 更新单上写:“我仓库现在还能放 Y 箱货”。这告诉发送方传送带上最多能同时放多少箱新货(发送窗口大小)。
- 滑动:
- 收到签收单(ACK 确认号)-> 传送带向前滚动(发送窗口左边界右移)。
- 收到仓库容量更新单(窗口大小)-> 调整传送带允许放置新货的区域长度(发送窗口大小变化)。
因此,TCP 报文(特别是其头部字段)是滑动窗口机制得以在网络上实现动态、可靠、高效数据传输的基石。 双方通过报文交换窗口状态信息(确认进度、可用空间),驱动窗口的滑动和大小调整,最终实现端到端的流量控制。具体图示如下
发送方 (Sender) 接收方 (Receiver)
+---------------------------------------------------+ +---------------------------------------------------+
| **发送缓冲区 (Send Buffer)** | | **接收缓冲区 (Recv Buffer)** |
| +-----------------------------------------------+ | | +-----------------------------------------------+ |
| | 已发送 **已确认** (Delivered & Acked) | | | | 已接收 **已交付应用** (Delivered to App) | |
| | [SND.UNA 之前的数据] | | | | [RCV.NXT 之前的数据] | |
| | | | | | | |
| +-----------------------------------------------+ | | +-----------------------------------------------+ |
| | 已发送 **未确认** (In Flight / Outstanding) | | ----> | | 已接收 **待交付** (Received, Not Delivered) | |
| | [**发送窗口 (Sending Window)** 的核心区域] | | 数据包 | | [**接收窗口 (Receiving Window)** 可用空间前部]| |
| | * SND.UNA (发送未确认起点) | | ----> | | * RCV.NXT (期望接收起点) | |
| | * SND.NXT (下一个发送位置) --> | | | | | |
| | | | | | | |
| +-----------------------------------------------+ | | +-----------------------------------------------+ |
| | **允许发送但尚未发送** (Sendable) | | | | **可用接收空间** (Available Space) | |
| | [发送窗口内,SND.NXT 之后的部分] | | | | [**接收窗口 (Receiving Window)** 核心] | |
| | * 发送窗口右边界 = SND.UNA + Win | | | | * 窗口大小 = RCV.WND | |
| | Win = min(拥塞窗口 cwnd, 接收方通告窗口 rwnd) | | | | * 右边界 = RCV.NXT + RCV.WND | |
| +-----------------------------------------------+ | | +-----------------------------------------------+ |
| | **不允许发送** (Not Sendable) | | | | **不可用空间** (Not Available) | |
| | [超出当前发送窗口] | | | | [超出当前接收窗口] | |
| +-----------------------------------------------+ | | +-----------------------------------------------+ |
+---------------------------------------------------+ +---------------------------------------------------+
关键图示要点总结:
1、窗口是“滑动”的: 左边界 (SND.UNA / RCV.NXT) 随着数据的确认/交付而持续向右移动。
2、窗口大小是动态的:
- 发送窗口大小 (Win) 受 min(cwnd, rwnd) 约束。
- 接收窗口大小 (RCV.WND) 随应用读取速度和接收缓冲区管理而变化。
3、ACK 是滑动的驱动力: ACK 携带的 Acknowledgment Number 推动发送窗口左边界滑动;Window Size 更新发送窗口右边界。
4、报文头部是信息载体: Seq Number, Ack Number, Window Size 这三个字段承载了维护滑动窗口状态所需的所有关键信息。
5、流量控制: 通过接收方通告 Window Size (rwnd) 并限制发送方的发送窗口 (Win <= rwnd) 来实现。
总结——套接字与 TCB 控制块
我们所用的工具是集成式的,底层有太多东西被方便的工具所隐藏好了。
Socket 套接字和文件描述符(File Descriptor)在 Unix/Linux 系统中密切相关但存在本质区别,以下是它们的核心差异对比:
特性 | 文件描述符 (FD) | Socket 套接字 |
---|---|---|
核心概念 | I/O 资源的抽象句柄 | 网络通信端点的抽象 |
代表对象 | 文件、管道、设备等 | 网络连接(TCP/UDP/ICMP 等) |
创建方式 | open() / pipe() / dup() 等 | socket() 系统调用 |
操作目标 | 本地数据(磁盘/内存) | 远程主机(通过 IP + 端口) |
数据流特性 | 通常为单向或双向字节流 | 支持结构化数据包(如 UDP 数据报) |
关键操作 | read() / write() / lseek() | send() / recv() / bind() / connect() |
关联的数据结构 | 关联到文件的 inode、磁盘位置等元数据。 | 关联到五元组和协议栈状态: |
内核路径 | VFS → 文件系统/设备驱动 | Socket API → 协议栈 → 网卡驱动 |
复用兼容性 | 支持 epoll/select | 同样支持 |
也就是说 socket 套接字除了本身是一个符号编号之外(这和文件描述符一样),它勾连着一个 TCB (TCP control block 控制块)。TCP 控制块(TCP Control Block,TCB)的作用:
1、协议栈核心:存储 TCP 连接的所有协议状态信息。
2、连接管理:处理握手、数据传输、挥手等 TCP 状态机逻辑。
3、数据缓冲:管理发送/接收缓冲区、序列号、窗口大小等。
组件 | 作用 | 用户可见性 | 生命周期 |
---|---|---|---|
套接字 socket | 用户操作 Socket 的句柄 | ✅ 直接可见 | 从 socket() 到 close() |
TCB | 内核协议栈维护连接状态的核心数据结构 | ❌ 隐藏 | 从 connect()/bind() 到 TIME_WAIT 结束 |