网络通信之TCP协议

发布于:2025-07-20 ⋅ 阅读:(16) ⋅ 点赞:(0)

  

目录

一、TCP协议概述

1.1 TCP 协议简介

1.2 传输控制协议

1.2.1 控制逻辑源自内核协议栈

1.2.2 为什么说 TCP 在“控制传输”?

1.3 TCP 的字节流传输模型

1.3.1 什么叫“面向字节流”?

1.3.2 为什么 TCP 设计为字节流?

1.3.3 两次 send() 会发生什么?

1.3.4 粘包与拆包问题的根本原因

1.3.5 如何解决粘包/拆包?

二、 TCP 报文段结构解析

2.1 TCP首部长度(HLEN)

2.2 如何解析 TCP 报文中的数据?

2.3 TCP 的窗口机制与流量控制

2.4 TCP 的确认应答机制

2.5 TCP 为什么需要确认应答?

2.6 ACK 应答机制的三大特性

2.7 序列号与确认号的作用

2.8 TCP 报文中的 6 个控制标志位

三、TCP 的可靠传输策略

3.1 TCP 的超时重传与快速重传机制

3.2 TCP 的连接建立与断开机制

3.2.1 TCP 三次握手建立连接的过程

3.2.2 TCP 四次挥手断开连接的过程


一、TCP协议概述

1.1 TCP 协议简介

        TCP(Transmission Control Protocol,传输控制协议) 是一种面向连接、可靠传输、基于字节流的传输层协议。它运行在 IP 协议之上,构成了现代网络通信的核心组成—TCP/IP 协议栈


CP 主要用于对数据完整性、顺序和传输可靠性要求较高的应用场景,如:

  • Web 应用:HTTP / HTTPS

  • 邮件服务:SMTP、POP3、IMAP

  • 远程登录:SSH、Telnet

  • 文件传输:FTP(命令通道)

特点包括:

  • 可靠传输: 有确认应答、超时重传、数据校验等机制。

  • 面向连接: 通过“三次握手”建立连接,“四次挥手”断开连接。

  • 顺序保证: 接收方按发送顺序组装数据(通过序列号)。

  • 全双工通信: 双方可同时发送数据。

  • 流量控制和拥塞控制: 动态调节发送速率,防止丢包与拥塞。

1.2 传输控制协议

1.2.1 控制逻辑源自内核协议栈

        当我们在程序中通过 socket() 创建一个 TCP 套接字时,操作系统会为其分配一个内部的数据结构,称为 Socket File Control Block(socket 文件控制块)。该控制块中维护了多个关键信息,其中就包括:

  • 发送缓冲区

  • 接收缓冲区

  • 连接状态、序列号、窗口、重传定时器等控制字段


        用户层的数据发送和接收,其实只是完成了数据拷贝的动作真正的数据传输过程完全由 TCP 协议在内核中自动控制

✅ 数据的“发送”其实只是拷贝

  • 调用 send() / write() 时,数据被从 应用层用户空间 拷贝到 内核 TCP 发送缓冲区

  • TCP 是否立即发送该数据,由协议栈根据网络状态自行决定(例如可能启用 Nagle 算法合并发送)。

✅ 数据的“接收”也是拷贝

  • 调用 recv() / read() ,是从 内核的 TCP 接收缓冲区 拷贝数据到 应用层用户空间

  • 实际的数据接收早已在内核中完成,用户只是“取用”。

1.2.2 为什么说 TCP 在“控制传输”?

        因为用户只负责把数据交给 TCP,而不是直接操作网络。以下这些核心问题都由 TCP 控制协议自动完成:

  • 何时发送数据?(可能立即,也可能合并多个包延迟发送)

  • 发送多少字节?(受窗口大小、拥塞控制等限制)

  • 如果丢包怎么办?(是否重传,何时重传,采用何种策略)

  • 数据是否按顺序交付?

  • 是否需要通知对端 ACK?是否需要快速重传?

        所有这些传输细节都不是应用层代码能控制的,而是由 TCP 协议栈(即传输控制逻辑)自动完成的。这正是 “Transmission Control Protocol” 名称的核心含义——TCP 不只是传输协议,更是一个智能调度数据传输的“控制器”

1.3 TCP 的字节流传输模型

1.3.1 什么叫“面向字节流”?

✅ 定义说明

        所谓面向字节流(byte-stream-oriented),指的是:TCP 将应用层传来的数据视为一个连续、无结构的字节序列进行可靠传输,不保留每次 send()/write() 的边界信息

        与之相对的是 UDP是面向报文(message-oriented)的协议,天然保留每次 sendto() 发送的“消息边界”。


✅ 面向字节流的具体表现

1)发送端:

  • 多次 send() / write() 的数据可能被合并成一个或多个 TCP 报文段;

  • 合并行为受 TCP 堆栈算法(如 Nagle 算法)和拥塞控制机制影响。
  • TCP 并不会在每次 send() / write() 调用之间插入边界标识。

2)接收端:

  • 每次 recv()read() 接收到的数据长度和边界无法预测

  • 一次 recv() 可能读取不到一个完整的消息,也可能包含多个消息。

1.3.2 为什么 TCP 设计为字节流?

        TCP 被设计为面向字节流的传输协议,是为了适应复杂和动态的网络环境,实现高效、可靠的传输。

        其本质是:仅负责传输一个无边界的、有序的字节流,如何切分消息由应用层决定。


✅ 技术动机

1)动态调整数据发送粒度

  • TCP 根据 发送窗口 和 拥塞窗口 实时调整发送速率;

  • 发送数据的分段行为是基于网络状态控制的,而非应用层调用的边界。

2)合并应用层数据(如 Nagle 算法)

  • TCP 可能将多个小块 send() 的数据缓冲合并,减少报文数量,提高带宽利用率。

  • 这意味着多次 send() 调用可能合并进一个 TCP 报文中发送。

3)底层网络层支持分片与重组

  • IP 层支持对数据包进行分片;

  • TCP 在传输层也能将大段应用数据拆成多个报文段发送,再在接收端重新组装。

4)接收端读取粒度灵活

  • recv() / read()的读取长度是由接收应用决定的;

  • 与发送端 send() 的边界没有任何必然关系;

1.3.3 两次 send() 会发生什么?

假设发送端代码如下:

send(sockfd, "Hi.", 3, 0);
send(sockfd, "I am XiaoHua", 13, 0);

应用层的两条消息为:

[Hi.] 和 [I am XiaoHua]

实际网络中的 TCP 报文可能出现以下几种情况:

✅ 情况一:两条消息被合并到一个 TCP 报文中(粘包)

[TCP segment 1] → "Hi.I am Xiaolin"

✅ 情况二:第一条完整,第二条部分被发送(拆包)

[TCP segment 1] → "Hi.I am "
[TCP segment 2] → "Xiaolin"

✅ 情况三:第一条也被拆了,第二条拼接进来(粘 + 拆)

[TCP segment 1] → "Hi"
[TCP segment 2] → ".I am Xiaolin"

✅ 情况四:极端情况,多个 TCP 报文包含碎片数据

[TCP segment 1] → "H"
[TCP segment 2] → "i."
[TCP segment 3] → "I am X"
[TCP segment 4] → "iaolin"

1.3.4 粘包与拆包问题的根本原因

        由于 TCP 是面向字节流的协议,数据在传输过程中没有天然边界,再加上传输受下列因素影响:

  • 网络拥塞、IP 分片;

  • 接收缓冲区限制;

  • Nagle 算法合并;

  • 系统调度延迟等。

因此,TCP 不能保证每次发送和接收是一一对应的消息单位,从而引发两个典型问题:

✅ 粘包

  • 多个应用层消息在发送端被合并为一个 TCP 报文段

  • 接收端调用一次 recv() 就读到了多个消息的拼接内容;

  • 导致消息边界丢失

✅ 拆包

  • 一个完整的应用层消息在网络层或 TCP 层被拆分为多个报文段

  • 接收端需要多次调用 recv() 才能拼出完整消息

1.3.5 如何解决粘包/拆包?

因为 TCP 不提供边界,消息划分必须由应用层协议自行定义。

常见策略包括:

方法 描述
固定长度消息 每条消息固定 N 字节
特殊分隔符 用特殊字符标识消息结尾(如 \n, \0, \r\n
前置长度字段 在消息前添加长度字段(如 4 字节)
TLV 格式协议 使用 Type-Length-Value 格式描述数据结构

二、 TCP 报文段结构解析

        TCP 报文段由固定报头(20字节)+ 可选字段 + 数据部分组成。TCP 报文头要求按 32 位对齐(与 IP 层协同)

字段名 长度 说明
源端口 16位 发送方进程绑定的端口号
目的端口 16位 接收方进程绑定的端口号(用于上层交付)
序列号 32位 本段数据第一个字节的编号
确认号 32位 期望接收的下一个字节序号(仅在 ACK 位=1 时有效)
首部长度 HL 4位 单位为 4 字节,用于解析 TCP 报头长度(含选项)
保留位 6位 全部为 0,保留给以后使用
标志位 Flags 6位 SYN/ACK/FIN/RST/PSH/URG 控制连接行为
窗口大小 16位 告诉对端:我还能接收多少字节(流量控制)
校验和 16位 TCP 的完整性验证(伪首部 + TCP 头 + 数据)
紧急指针 16位 标记紧急数据(仅当 URG=1 时有效)
选项字段 可变 如 MSS、窗口扩大因子、时间戳等(32位对齐)
数据部分 可变 实际发送的应用层数据

2.1 TCP首部长度(HLEN)

        HLEN 字段占 TCP 头部的 4 位,单位为 4 字节。所以TCP 报头的长度范围为 0~15,即报头长度范围是 0~60 字节。

具体说明:

  • 标准 TCP 报头固定长度为 20 字节,对应 HLEN = 5

  • 如果报头中包含选项字段,报头长度会大于 20 字节。

  • 接收方通过 HLEN 字段确定 TCP 报头结束的位置,即数据部分的起始偏移。

举例:[若 HLEN = 6]  →  [报头 = 6 * 4 = 24 字节 ] →  [数据部分从偏移 24 字节开始]

2.2 如何解析 TCP 报文中的数据?

1) 读取首部长度字段(HLEN)

  • 读取此字段即可得到 TCP 报头的总长度:报头长度=HLEN×4 字

2) 提取固定报头部分(20 字节)

  • 包含源端口、目的端口、序列号、确认号、标志位、窗口大小等核心信息。
  • 这部分是 TCP 报文的基本结构,必须解析。

3) 提取选项字段(如果有)

  • 选项字段长度为:选项长度=(HLEN×4)−20 字节

4) 提取有效载荷数据

  • 剩余部分即为 TCP 数据段的有效载荷(Payload),传递给上层应用或协议处理。

2.3 TCP 的窗口机制与流量控制

TCP 为了防止接收方被过快的数据流压垮,引入了基于窗口的流量控制机制。

✅ 核心概念

  • 接收窗口(Receive Window,rwnd):由接收方通告给发送方,表示接收方当前还能接收的缓冲区大小(以字节为单位)。

  • 发送窗口(Send Window):发送方根据接收方的窗口值决定最大发送字节的范围。

  • 滑动窗口:随着数据的确认接收,窗口向前滑动,允许新的数据继续发送。

✅ 工作机制

  1. 每个 TCP 报文头中包含一个 16 位的窗口字段 Window Size,用来传递接收方的接收窗口大小;

  2. 发送方只能发送序号在区间 [SND.UNA, SND.UNA + rwnd) 内的数据,其中 SND.UNA 表示“未确认的最小序号”。

  3. 接收方根据自身接收缓存的使用情况动态调整窗口大小,并反馈给发送方,防止接收方因缓存不足而被数据流压垮。

2.4 TCP 的确认应答机制

确认应答机制是 TCP 实现可靠传输的核心机制之一。

✅ 机制概述

  • 每个 TCP 报文都可以携带 ACK 标志位 和 确认号字段;

  • 确认号表示望接收的下一个字节序列号,即接收方已成功收到的上一次数据。

  • TCP 默认使用累计确认机制确认号表示“连续收到的最后字节的下一个序号”。

✅ 举例说明

  • 发送方发送序列号范围为 100~199 的数据段;

  • 接收方成功接收后,发送确认号为 200,表示:“我已成功收到序列号小于 200 的所有字节,请从 200 开始发送下一批数据”。

2.5 TCP 为什么需要确认应答?

✅ 原因一:确保可靠传输

  • IP 协议本身是不可靠的,数据包可能会出现丢失、乱序、重复等情况。

  • TCP 通过确认应答机制,能够检测数据是否成功到达对端,保证数据传输的可靠性。

✅ 原因二:驱动数据传输窗口前进

  • TCP 发送方依赖收到的确认号(ACK)反馈,来滑动发送窗口,继续发送后续数据。

  • 如果没有确认反馈,发送窗口将停滞,导致数据无法继续发送,影响传输效率。

✅ 原因三:支持重传机制

  • 通过确认号,发送方能够判断哪些数据段未被接收方确认。

  • 在超时或收到重复 ACK 的情况下,发送方能够触发重传,保障数据不丢失。

2.6 ACK 应答机制的三大特性

✅ 特性一:累计确认(Cumulative ACK)

含义:TCP 默认使用 累计确认机制

  • 只确认序号连续的数据段的最后一个字节

举例:

  • 若接收方收到字节序列号为 0~499,则发回 ACK = 500

  • 如果 500~599 丢失,接收方收到 600~699 也不会确认,只会继续发送 ACK = 500

注:可选 SACK(Selective ACK)

  • TCP 的扩展机制,允许接收方告诉发送方具体哪些数据段已经成功接收

  • 提升在网络拥堵或乱序环境中的重传效率,避免不必要的重复发送


✅ 特性二:延迟确认(Delayed ACK)

含义:允许 不立即发送 ACK,而是略微延迟(典型值如 40~200ms)后再发送,以期望:

  • 在延迟窗口内有更多的数据可一并确认,从而减少 ACK 报文数量,提高链路利用率。

举例:

  • TCP 可能会等待一小段时间,看是否可以合并多个 ACK 或数据与 ACK 一起发送;

  • 若超时无数据到达,也会强制发送 ACK。

:为防止死锁,RFC 要求: 延迟确认时间不得超过 500ms。


✅ 特性三:快速重复确认(用于快速重传)

含义:当接收方收到乱序数据段,即前面的数据段丢失,后续却已到达,会连续发送相同的 ACK 确认号

  •   表示“我还在等待之前那个序列号的数据”。

举例:

  • 已收到 0~499,丢了 500~599,却先收到了 600~699

  • 接收方会反复发送 ACK = 500

  • 发送方一旦收到 3 个重复 ACK,就会触发 快速重传机制,无需等待超时。

2.7 序列号与确认号的作用

✅ 序列号(Sequence Number)

  • 表示 TCP 数据流中每个字节的唯一编号。

  • 每个 TCP 报文段携带其首字节的序列号,用于标识数据的位置。

  • 在 TCP 连接建立时,客户端和服务端各自生成一个随机的初始序列号,用于防止旧连接报文的干扰。

✅ 确认号(Acknowledgment Number)

  • 表示接收方期望收到的下一个字节序号;

  • 用于接收方向发送方确认已收到数据;

  • 只有当 TCP 报文中的 ACK 标志位为 1 时,确认号字段才有效。

2.8 TCP 报文中的 6 个控制标志位

标志位 含义 用途
SYN 建立连接请求 三次握手中使用
ACK 确认应答标志 大多数数据包都设置此位
FIN 主动关闭连接请求 四次挥手中使用
RST 复位连接 非法请求或异常时终止连接
PSH 推送功能 告知对方立即处理数据
URG 紧急指针有效 基本已废弃,极少使用

三、TCP 的可靠传输策略

        TCP 为了实现“面向连接、可靠传输”的目标,设计了一系列机制,保障数据在复杂网络环境下依然能顺序到达、不重复、不丢失、不乱序

3.1 TCP 的超时重传与快速重传机制

✅ 超时重传(Timeout Retransmission)

  • TCP 为每个已发送但未被确认的数据段设置一个重传定时器;

  • 如果定时器超时仍未收到 ACK,则该段会被重新发送。

📌 关键参数

  • RTO(Retransmission Timeout):重传超时时间,动态计算:

            RTO = SRTT + 4 * RTTVAR

    其中:

    • SRTT:平滑往返时间,对最近 RTT 进行指数加权平均

    • RTTVAR:RTT(往返时间)偏差,估计 RTT 的变异程度

✅ 快速重传(Fast Retransmit)

  • 当接收端收到乱序数据段时,会发送对最后一个连续正确接收数据段的确认,即重复 ACK

  • 发送端一旦接收到连续 3 个相同的重复 ACK,即认为数据段丢失,无需等待超时,立即重传该丢失的数据段

为什么重新发送数据段N+1,确认号ACK=N+3,而不是+1变为N+2?
        TCP 确认号始终指示“已经连续收到的数据的下一个字节”,而不是“最后一个收到数据的序号”。
        当接收方收到丢失的 N+1 数据段后,现在,接收方已经收到 N、N+1、和之前乱序收到的 N+2。数据已经是连续的,从 N 到 N+2 都收到完整。因此,接收方确认下一个期待字节序号是 N+3。

3.2 TCP 的连接建立与断开机制

        TCP 是面向连接的协议,通信前需要建立连接,结束后需显式关闭连接。

3.2.1 TCP 三次握手建立连接的过程

✅ 目的

  • 双方协商初始序列号(ISN);

  • 确保双向通路可达;

  • 建立连接状态。

✅ 三次握手流程

第一次握手:客户端发送 SYN

客户端应用层动作

  • 调用:connect(sockfd, ...)

  • 结果:

    • 触发内核创建一个带有 SYN 标志的 TCP 报文段。

    • 该报文段包含客户端初始序列号(client_isn)。

    • 客户端进入 SYN_SENT 状态。

服务端内核行为

  • TCP 层监听 socket(通过 listen() 进入监听状态)。

  • 收到 SYN 报文后:

    • 自动生成半连接(SYN队列)。

    • 发送带有 SYN+ACK 标志的响应(第二次握手)。


第二次握手:服务端回应 SYN+ACK

✦ 服务端应用层动作

  • 调用顺序:

    • socket() 创建监听 socket;

    • bind() 绑定地址;

    • listen() 启用监听(此时服务端进入 LISTEN 状态);

    • accept() 等待连接(阻塞,直到三次握手完成)。

✦ 内核行为

  • 收到客户端的 SYN 后,回应 SYN+ACK 报文;

  • 服务端进入 SYN_RCVD 状态;

  • 报文中携带服务端自己的初始序列号 server_isn,并 ACK 客户端的 client_isn + 1


第三次握手:客户端回应 ACK

✦ 客户端内核行为

  • 收到服务端的 SYN+ACK 后,回复一个 ACK 报文:

    • 确认号为 server_isn + 1

    • 此时客户端进入 ESTABLISHED 状态;

  • connect() 系统调用 返回成功(0),应用层正式感知连接建立完成。

✦ 服务端内核行为

  • 收到客户端 ACK 报文后,连接完成,状态变为 ESTABLISHED

  • 将连接从半连接队列转到全连接队列,进入全连接队列(accept() 会从此队列中取出连接);

  • accept() 系统调用返回一个新的连接 socket,用于与客户端通信。

✅ 特性

  • 第三次握手可以携带数据(如 HTTP 请求),提高效率;

  • 如果第三次 ACK 丢失,服务端重发 SYN+ACK,客户端重传 ACK。

3.2.2 TCP 四次挥手断开连接的过程

✅ 原因:全双工连接,必须双方独立关闭方向

  • FIN 表示“我已经没有数据可发了”,但仍可接收对方数据;

  • TCP 使用四次挥手确保双方都关闭。

✅ 四次挥手流程

第一次挥手:主动关闭方发送 FIN

✦ 主动关闭方应用层动作

  • 调用:close(sockfd)

  • 结果:

    • 内核发送一个带有 FIN 标志的 TCP 报文段;

    • 主动关闭方进入 FIN_WAIT_1 状态。

✦ 被动关闭方内核行为

  • 收到 FIN 报文;

  • 发送 ACK 报文确认;

  • 被动关闭方进入 CLOSE_WAIT 状态;

  • 应用层被通知连接关闭请求(read() 会返回 0 或触发关闭事件)。


第二次挥手:被动关闭方回应 ACK

✦ 被动关闭方应用层动作

  • 应用程序在收到关闭通知后,调用 close(sockfd)(或其他关闭方式);

  • 准备关闭连接。

✦ 被动关闭方内核行为

  • 发送带有 FIN 标志的 TCP 报文段;

  • 被动关闭方进入 LAST_ACK 状态。


第三次挥手:主动关闭方收到 FIN,回应 ACK

✦ 主动关闭方内核行为

  • 收到被动关闭方的 FIN 报文;

  • 回复 ACK 报文确认;

  • 进入 TIME_WAIT 状态,等待足够时间确保对方收到确认。

✦ 主动关闭方应用层动作

  • 等待 2 倍最大报文生存时间(2MSL)后,内核释放连接资源;

  • 连接最终关闭,进入 CLOSED 状态。


第四次挥手:被动关闭方收到 ACK,连接关闭

✦ 被动关闭方内核行为

  • 收到主动关闭方的 ACK 报文;

  • 连接关闭,进入 CLOSED 状态;

  • 释放资源。

✅ TIME_WAIT 的作用

  • 保证最后一个 ACK 能到达对方;

  • 防止旧连接残留包干扰新连接(使用相同四元组);

  • 持续 2 倍最大报文生存时间(MSL,通常为 2 分钟)。


网站公告

今日签到

点亮在社区的每一天
去签到