TCP(传输控制协议)核心机制与底层原理

一、 TCP/IP 分层模型与基础通信结构

1. 网络分层参考模型

  • 应用层:负责应用程序的网络访问(如 DNS, HTTP, FTP, 序列化/反序列化)。

  • 表示层/会话层:在 TCP/IP 模型中通常合并入应用层。

  • 传输层:操作系统内核实现,负责端到端的数据传输控制(包含 TCP, UDP 等)。TCP 全称为"传输控制协议",意为对数据的传输进行详细的控制。

  • 网络层 (互联网层):负责 IP 寻址与路由选择(ARP, IP, ICMP)。

  • 数据链路层与物理层:网卡层驱动与硬件设备。

2. 底层通信机制与缓冲区

  • 双缓冲区模型 :主机间的通信本质是应用层通过系统调用(如 read/writesend/recv),将数据在用户区与 OS内核的 TCP 缓冲区(发送缓冲区 / 接收缓冲区) 之间进行拷贝

  • 字节流管理 :发送端往发送缓冲区写入的数据,TCP 会天然地为每一个字节进行编号,这就是序列号 (Sequence Number) 的底层来源。


二、 TCP 首部格式与流量控制

1. TCP 首部结构与核心字段

TCP 报头标准长度为 20 字节,最大为 60 字节(计算范围:4×15=604 \times 15 = 604×15=60 字节,由4位首部长度决定)。

  • 源/目的端口号 (16位):表示数据从哪个进程来,到哪个进程去。

  • 序号 (32位) 与确认序号 (32位):用于保证数据的有序性和可靠性。

  • 首部长度 (4位) 与 保留位 (6位)

  • 标志位 (Flags, 6位) :URG, ACK, PSH, RST, SYN, FIN。本质是报头中的比特位(Linux内核 struct tcphdr 中定义)。

  • 窗口大小 (16位) :用于流量控制

  • 校验和 (16位):发送端填充,包含 TCP 首部和数据部分(CRC校验)。接收端校验不通过则丢弃。

  • 紧急指针 (16位):标识紧急数据的范围。

2. 流量控制模型

  • 核心逻辑:发送数据一定要上报,必须进行合理的流控以保证效率。如果对方接收缓冲太小,发送极速会导致丢包。

  • 窗口机制 :根据客观事实,发送端必须尽早知道对方的接收能力。"16位窗口大小"填的就是自己(发送该报文一端)接收缓冲区剩余空间的大小。双方通过报文交互,实时协商接收能力。


三、 TCP 标志位解析 (Flags)

接收方收到的 TCP 报文有不同的类型,必须根据标志位采取不同的处理方式。

1. 连接管理标志 (SYN, FIN, ACK)

  • SYN (同步标志位) :请求建立连接。前两次握手只有 TCP 报头,不能携带数据(因为连接尚未完成),但已经可以进行双方接收能力的协商。

  • FIN (结束标志位):通知对方本端要关闭连接。

  • ACK (确认标志位):确认号是否有效。常规应答报文中 ACK 标志位几乎都设为 1(可以是纯应答,也可以是应答+数据)。

2. 数据与异常控制标志 (PSH, RST, URG)

  • PSH (推标志):提示接收端应用程序立刻从 TCP 缓冲区把数据读走。

  • RST (复位标志) :异常重置报文。通信过程中连接出现任何认知不一致或异常(如浏览器 ERR_CONNECTION_RESET),对方会要求重新建立连接。

  • URG (紧急标志) :表示紧急指针有效。用于处理带外数据 (MSG_OOB)。紧急数据通常只有一个字节,不占常规空间,绕过常规字节流接收队列,允许应用程序"插队"优先读取处理。


四、 序号机制、可靠性与超时重传

1. 序列号 (Seq) 与确认应答 (ACK)

  • 编号与分割 :TCP 将每个字节的数据都进行了编号。例如发送 1~1000 字节,下一个报文发送 1001~2000 字节。

  • 确认应答 :每一个 ACK 都带有对应的确认序列号,含义是:"我已经收到了哪些数据,下一次请从哪里开始发" (例如收到 1~1000,确认序号就是 1001)。

2. 丢包判定与去重机制

  • 无法 100% 保证到达:发送方无法保证数据一定能到达对方,也无法确认是"数据丢了"还是"应答丢了"。

  • 判定标准 :只要在特定的时间间隔内收不到应答(超时),就缺省判定为报文丢失,触发重传。

  • 去重机制 :如果是 ACK 丢失导致发送方重传,接收方会收到重复数据。此时依靠序列号进行去重。

3. 动态超时重传时间 (RTO)

  • 时间设定难题:网络状态是变化的(RTT 往返时间不同)。设得太长影响效率,设得太短导致频繁发重复包。

  • 动态计算策略 :Linux 中以 500ms 为一个单位进行控制。如果重发得不到应答,等待时间按指数形式递增 (2×5002 \times 5002×500ms -> 4×5004 \times 5004×500ms -> ...)。累计到一定次数仍无响应,则强制关闭连接。


五、 TCP 连接管理与状态机

建立连接是有时间与空间成本的(OS 需要用 struct Link 等数据结构来管理连接)。

5.1 三次握手 (建立连接)

  • 过程 :客户端 connect 发起,OS 自动完成握手。服务端 accept 不参与底层握手过程,只负责从内核队列中取出建立好的连接。

  • 本质:原本是 4 次握手(双方各发起一次 SYN 和 ACK),但服务端的 SYN 和 ACK 合并为了一次报文,从而变成了 3 次握手(稍带应答)。

  • 为什么是三次?(必须是奇数次) :以最小成本,100% 验证双方通信信道通畅,确认网络能够支持全双工通信。(类比男女朋友结为夫妻,需要双方意愿且外部条件满足)。

  • 安全隐患:由于服务端必须无脑接受客户端的连接请求,因此容易引发资源消耗或安全问题(如 SYN 泛洪攻击)。

5.2 四次挥手(断开连接)

  • 概念解释

    • 全双工通信 (Full-Duplex):指数据在通信双方之间可以同时进行双向传输的工作模式。TCP的本质即是建立并维护一个可靠的全双工通信。

    • 四次挥手:TCP断开连接的过程。断开连接的本质是"建立双方断开连接的共识"。

  • 详细笔记

    • TCP 状态流转标识CLOSED, LISTEN, SYN_SENT, SYN_RCVD, ESTABLISHED, FIN_WAIT_1, FIN_WAIT_2, TIME_WAIT, CLOSE_WAIT, LAST_ACK

    • 断开连接(四次挥手)过程

      1. C->S:客户端表示"我要发的数据已经发完了,我要和你断开"(发送 FIN)。

      2. S->C:服务端表示"我也收到断开连接请求了"(返回 ACK)。此时服务端可能还有数据要发送。

      3. S->C:服务端表示"我也发完了,我也要断开连接"(发送 FIN)。

      4. C->S:客户端表示"收到,确认断开"(返回 ACK)。

    • 主动断开连接方 :主动断开连接的一方,最终会进入 TIME_WAIT 状态,等待最后一次挥手彻底完成。

5.3 TIME_WAIT 与 CLOSE_WAIT

  • 概念解释

    • 句柄泄漏 (Handle Leak):指程序在申请了文件描述符(句柄)后,由于逻辑错误未正确释放(未调用 close),导致系统可用的文件描述符逐渐耗尽的问题。

    • MSL (Maximum Segment Lifetime):报文在网络中的最大生存时间。

  • 详细笔记

    • CLOSE_WAIT 产生原因与危害

      • 如果客户端(C)已经退出或关闭,而服务器端(S)没有调用 close() 关闭 Socket,服务端就会一直卡在 CLOSE_WAIT 状态。

      • 在此状态下,连接并未真正释放,只是停止了文件句柄的读写。这会导致句柄泄漏问题,是一个严重的BUG。

      • 修复方案 :必须在服务端逻辑中加上对应的 close() 调用(或修正代码中遗漏的 new_sock.Close()),确保四次挥手正确完成。

    • TIME_WAIT 状态的作用(2MSL机制)

      • 主动关闭方在发送最后一个 ACK 后,必须处于 TIME_WAIT 状态等待 2MSL 的时间(CentOS7/Ubuntu 默认 60s,可通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看)。

      • 为什么是 2MSL?

        1. 保证两个传输方向上尚未接收或迟到的报文段都已经消失,防止服务器立刻重启后收到上一个进程的错误/迟到数据。

        2. 理论上保证最后一个报文可靠到达。如果最后一个 ACK 丢失,服务端会重发 FIN,此时处于 TIME_WAIT 的客户端仍可重发 LAST_ACK。

    • TIME_WAIT 引起的 Bind 失败与解决

      • 服务端主动断开连接时,会产生大量 TIME_WAIT。若此时重启服务端程序(如 ./server),会报 bind error: Address already in use

      • 因为 TIME_WAIT 占用了五元组,相同端口暂不可用。

      • 解决方法 :使用 setsockopt() 设置 SO_REUSEADDR 选项。

5.4 涉及的函数

  • 函数名shutdown

  • 函数原型

c 复制代码
  #include <sys/socket.h>
  int shutdown(int sockfd, int how);
  • 功能与参数说明 :主动关闭全双工连接的全部或部分。how 参数:SHUT_RD (禁止后续接收),SHUT_WR (禁止后续发送),SHUT_RDWR (禁止后续收发)。

  • 函数名setsockopt

  • 函数原型

c 复制代码
  #include <sys/socket.h>
  int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • 功能与参数说明 :用于设置 Socket 选项。常用于解决 TIME_WAIT 导致的 bind 失败,设置 SO_REUSEADDR 为 1。

六、 数据流转、面向字节流与滑动窗口

6.1 数据流转与面向字节流

  • 概念解释

    • 序列化/反序列化 (Serialization/Deserialization):将应用层结构化数据转换为连续字节流(序列化)以便网络传输,并在接收端恢复原始结构(反序列化)的过程。

    • 面向字节流 (Byte Stream):TCP将数据视为无边界的连续字节序列。读写次数不需要一一对应。

    • 粘包问题 (Sticky Packet):由于TCP面向字节流且无报文边界,接收方应用层可能将多个连续发送的数据包当作一个包读取,或者读到"半个包"的现象。

  • 详细笔记

    • 数据流转链路:应用层 (Message) -> 序列化 -> 系统调用 (write) -> OS Kernel (发送缓冲区) -> TCP层 -> 网络层 -> 传输层 -> 本地发送网卡 -> 网络 -> 主机B本地接收网卡 -> 接收缓冲区 -> read -> 反序列化。

    • 面向字节流的特性

      • 存在发送缓冲区和接收缓冲区。数据写入发送缓冲区后,长包拆分,短包等待(积累到合适大小或时机发送)。

      • 读写不需要匹配:写100字节可以1次写完,也可以分100次写;读也可以随意组合,互不影响。

    • 粘包问题解析与解决

      • "包"指的是应用层数据包。TCP有序号机制保证字节按序到达,但不提供应用层边界。

      • 相比之下,UDP不存在粘包问题,因为UDP交付是按单个完整报文进行的(要么全收,要么不收)。

      • 解决粘包归根结底是"明确两个包之间的边界":

        1. 定长包 :每次按固定大小读取(如 sizeof(Request))。

        2. 变长包(包头约定长度):在包头增加总长度字段。

        3. 变长包(分隔符):包与包之间使用明确的分隔符(确保正文不与分隔符冲突)。

6.2 滑动窗口机制

  • 概念解释

    • 滑动窗口 (Sliding Window) :发送方无需等待确认应答 (ACK) 而可以连续发送数据的最大量。本质是流量控制的具体实现方案
  • 详细笔记

    • 窗口的作用:一发一收性能差。一次发送多条数据(将等待ACK的时间重叠)可极大提高性能与网络吞吐率。

    • 窗口大小的决定 :滑动窗口大小由"对方接收窗口大小"和"我的发送缓冲区"共同决定(取短板),即由对方的接收能力决定

    • 窗口的内部机制

      • 接收窗口是发送缓冲区的一部分。

      • 发送缓冲区区域划分:已发送已确认 | 可以直接发(滑动窗口内) | 待发送。

      • 计算公式start = 确认序号end = start + win。大小 = end - start。

      • 滑动窗口移动的本质:start 和 end 向右增加。随着确认数据的到来,窗口不断右移,已确认的数据被冲刷掉。

      • 序号在缓冲区中逻辑上无限大,窗口基本只向右滑动。

    • 关于滑动窗口的问题思考

      • 可以向左滑动吗?不会!

      • 可以变大/小/不变/为零吗?可以,完全由对方的接收能力决定。

      • 会不会越过报文进行应答?不会,由确认序号的定义(要求连续确认)决定。

      • 接收窗口一直向右会不会溢出?(环形缓冲区设计可解决)。


七、 TCP 异常处理、重传机制与网络控制

7.1 丢包与重传机制

  • 概念解释

    • 超时重传 (Timeout Retransmission):当发送方在规定时间内未收到报文的ACK,认为包已丢失,主动重新发送该报文。

    • 快速重传 (Fast Retransmit / 高速重发控制):如果发送端连续收到3个相同的ACK(说明某个包丢失,但后续的包收到了),无需等待超时定时器,立刻重发丢失的报文段。

  • 详细笔记

    • 丢包场景分析与滑动窗口应对

      • ACK丢失:部分ACK丢失不要紧,因为TCP确认消息是连续确认的。后续的ACK(如收到"下一个是3001")会自动涵盖前面丢掉的ACK。

      • 最左侧数据包丢失 :发送端滑动窗口左侧不动。接收端持续返回相同的ACK(如重复返回3次"下一个是1001")。触发快速重传,补发1001-2000这段数据。接收端收到补发后,会直接返回之前已缓存数据的最大连续序号(如7001)。

      • 中间报文丢失 :必须触发重传,确认了才能向右滑动。超时重传和快速重传的配合就是滑动窗口的核心逻辑!

    • 报文暂存:TCP发包未收到应答时,报文必须暂存在发送缓冲区内,以便后期重传。

7.2 流量控制 (Flow Control)

  • 概念解释

    • 流量控制:TCP根据接收端的处理能力,动态决定发送端的发送速度,避免接收端缓冲区被打满导致丢包。

    • 窗口探测包 (Window Probe):当接收端窗口变为0时,发送端定期发送的微小数据段,用于探测接收端窗口是否恢复。

  • 详细笔记

    • 工作原理

      流量控制主要通过滑动窗口(Sliding Window)来实现。

    • 接收方在每次给发送方回复 ACK(确认报文)时,都会在 TCP 首部的 Window(窗口大小) 字段中填入一个值。

    • 这个值代表接收方目前剩余的缓冲区大小

    • 发送方根据这个通知,动态调整自己连续发送数据的上限。如果接收方通告窗口为 0,发送方就会停止发送,直到接收方重新通告一个非 0 窗口。

7.3 拥塞控制 (Congestion Control)

  • 概念解释

    • 拥塞控制:防止过多数据注入网络,避免网络负载过大。它是针对整个网络状态的控制,区别于流量控制(针对通信两端)。

    • 拥塞窗口 (cwnd):发送方维护的一个状态变量,控制同时发送到网络的数据量。

    • 慢启动 (Slow Start):在初始发送或网络拥塞后,从较小的窗口开始,按指数级快速探测网络可用带宽的算法。

    • 场景:你开着车(数据包)上高速公路。如果路上的车太多,收费站和主干道就会堵车(路由器排队、丢包)。交管部门(拥塞控制算法)会通过限制限行、调整红绿灯(降低发送速率)来缓解整条公路的拥堵。这不取决于你目的地那个人的车库有多大,而是取决于大马路堵不堵。

  • 详细笔记

    • 工作流程 :实际发送窗口 = min(拥塞窗口, 接收端窗口)

    • 慢启动:初始时拥塞窗口很小,但每收到一个 ACK,窗口大小就翻倍(指数级增长),用以快速探测网络带宽。

    • 拥塞避免:当窗口达到一个阈值(ssthresh)后,为了防止动作过大导致拥堵,窗口变为线性增长(每次加 1)。

    • 快重传:发送方只要连续收到 3 个重复的 ACK,就判定数据包丢了,不等超时定时器到期,立刻重传。

    • 快恢复:配合快重传,在网络出现丢包但还没彻底瘫痪时,直接将窗口减半,然后重新进入线性增长,而不是直接掉回初始的慢启动状态。

补充:网络出现丢包时的两种处理情况

当网络在"拥塞避免"阶段由于达到极限而开始丢包时,TCP 会根据以下两种不同情况来决定是启动"快恢复"还是"重置为 1":

  • 情况一:网络严重拥塞(触发超时重传)

    如果网络彻底堵死,发送方长时间收不到任何回应,导致超时定时器到期。此时 TCP 判定网络瘫痪:

    • 慢启动阈值 ssthresh 变为当前窗口的一半。

    • 拥塞窗口(cwnd)瞬间重置为 1

    • 重新开始慢启动过程。

  • 情况二:网络轻微拥塞(触发快重传与快恢复)

    如果网络只是个别丢包,发送方连续收到 3 个重复的 ACK。此时 TCP 判定网络大体通畅,只是轻微感冒:

    • 立即触发快重传补漏。

    • 随后触发快恢复 ,直接将窗口减半,然后重新进入线性增长(拥塞避免),而不掉回初始的慢启动状态。


7.4 提高效率的优化机制与异常情况

  • 概念解释

    • 延迟应答 (Delayed ACK) :接收端收到数据后不立即返回ACK,而是等待一小段时间。

    • 捎带应答 (Piggybacking) :ACK确认包搭乘应用层即将发送的响应数据包(顺风车)一起发往对端。

    • 保活机制 (Keep-Alive):TCP内置定时器,定期询问对方存活状态。

  • 详细笔记

    • 延迟应答:立刻应答可能导致返回的窗口较小。延迟一段时间(等待应用层消费掉部分缓冲区),可以返回更大的窗口,提升吞吐量。限制:数量限制(如每隔2个包应答)和时间限制(如最大延迟200ms)。

    • 捎带应答 :在)

      延迟应答基础上,一发一收的场景中,ACK 经常和业务响应(如 HTTP Response)合并发送,减少网络包数量。

    • TCP异常情况处理(容错能力强)

      • 进程终止:释放文件描述符,自动发送 FIN,执行正常四次挥手。

      • 机器重启:同进程终止。

      • 机器掉电/断网

        1. 若有写入操作,接收端发现连接不在,触发 Reset (RST)。

        2. 若无写入,TCP内置保活定时器(几十分钟级别)定期探活,失败则释放。通常保活机制会在应用层自己实现(如 HTTP长连接、QQ断线重连)。


八、 TCP 与 UDP 对比 & 基于 TCP 的协议

  • 详细笔记

    • TCP 的复杂性来源

      • 可靠性:校验和、序列号、确认应答、超时重发、连接管理、流量控制、拥塞控制。

      • 提高性能:滑动窗口、快速重传、延迟应答、捎带应答。

      • 其他:各类定时器(超时重传、保活、TIME_WAIT等)。

    • TCP vs UDP 场景对比

      • TCP 适用于可靠传输(文件传输、重要状态更新)。

      • UDP 适用于高速传输、实时性要求高、允许轻微丢包的场景(早期QQ、视频流媒体,支持广播)。

      • 经典面试题:用 UDP 实现可靠传输 -> 需在应用层引入序列号、确认应答、超时重传等机制。

    • 基于 TCP 的应用层协议:HTTP, HTTPS, SSH, Telnet, FTP, SMTP 等。


九、 Linux 内核 Socket 数据结构 (C语言中的面向对象/多态)

9.1 结构体嵌套与多态机制

  • 概念解释

    • 多态机制 (Polymorphism in C):Linux内核使用结构体嵌套(将被继承的结构体放在子结构体的第一个成员位置)来实现面向对象的继承和多态。指针强制转换后,可以直接操作父类属性。
  • 详细笔记

    • 进程与文件系统的关联task_struct 包含 files_struct 指针 -> fd_array[] -> 指向具体的 struct file -> file->private_data 指向网络层的 struct socket

    • 网络协议栈的继承链条

      • 最内层(基类)struct sock sk (包含收发队列 sk_receive_queuesk_write_queue)。

      • 第二层struct inet_sock (第一个成员必须是 struct sock sk)。

      • 第三层struct inet_connection_sock

      • 最外层(TCP派生类)struct tcp_sock / struct udp_sock

相关推荐
王老师青少年编程1 小时前
csp信奥赛C++高频考点专项训练之字符串 --【字符串综合】:遍历问题
c++·字符串·csp·高频考点·信奥赛
yoyo_zzm1 小时前
五大编程语言对比:PHP、C、C++、C#、易语言
c语言·c++·php
杰之行1 小时前
Fast-DDS Transport 层架构详解
c++·人工智能
运维全栈笔记1 小时前
Harbor生产级部署实战:PostgreSQL+Redis+MinIO全解耦架构详解
linux·运维·服务器·笔记·架构·kubernetes·k8s
W23035765731 小时前
Linux C++ 基于 timerfd + epoll 实现高性能定时器队列(完整源码 + 超详细解析)
linux·开发语言·c++·线程池
Lochor Lee1 小时前
C++学习笔记——输入输出的格式
c++·笔记·学习
wanhengidc1 小时前
云手机中虚拟技术的功能
运维·服务器·网络·安全·web安全·智能手机
皓月盈江1 小时前
Linux Ubuntu系统使用Docker搭建vulhub靶场环境
linux·ubuntu·docker·tomcat·vulhub·漏洞靶场
念恒123061 小时前
Docker基础--namespace空间隔离实战(包含部分指令)
linux·运维·服务器