万字长文:从三次握手的“为什么”到内核 sk_buff 的流动,彻底说透 UDP、TCP 与 HTTP

万字长文:从三次握手的“为什么”到内核 sk_buff 的流动,彻底说透 UDP、TCP 与 HTTP 前言你理解的“TCP可靠、UDP不可靠”可能只触及了冰山一角“TCP 是可靠的UDP 是不可靠的”——这句话几乎所有开发者都能脱口而出。但当你线上服务的接口突然超时飙高、下载速度莫名骤降、游戏画面频繁卡顿你真的能说清楚问题出在握手阶段、滑动窗口、拥塞控制还是零窗口探测吗大多数开发者的网络协议认知停留在这句“八股文”层面。遇到超时、丢包、拥塞时只能盲目地改改超时时间、调调缓冲区大小运气好解决了运气不好就陷入“重启试试”的玄学循环。真正的底层认知决定了你排查问题的效率上限。这篇文章我们从 UDP 的 8 字节首部一路拆到 TCP 的滑动窗口与拥塞控制再到 HTTP 从 1.1 到 3.0 的演进逻辑最后深入 Linux 内核看一眼数据包的全生命周期。读完你会理解为什么 UDP 没有发送缓冲区TCP 为什么是三次握手而不是两次TIME_WAIT 为什么要等 2MSLHTTP/3 为什么要“抛弃”TCP一、网络协议栈全景四层模型中HTTP、TCP、UDP 各司其职在深入拆解之前先建立全局视图。现代网络通信基于TCP/IP 四层模型层级协议示例核心职责应用层HTTP、DNS、RTP为用户提供具体服务定义数据格式与语义传输层TCP、UDP端到端通信提供端口寻址与传输控制网络层IP跨网络的数据包路由与寻址数据链路层Ethernet物理介质上的帧传输数据发送时应用层数据如 HTTP 报文逐层向下封装传输层加上 TCP/UDP 首部、网络层加上 IP 首部、数据链路层加上帧首部。接收时则反向逐层解封装。HTTP 是应用层协议它“坐”在 TCP 或 QUIC基于 UDP之上TCP 和 UDP 是传输层协议它们“坐”在 IP 之上。理解这个层次关系是后续一切分析的基础。二、UDP 协议深度拆解8 字节的极简主义2.1 核心特性UDPUser Datagram Protocol的设计目标是最小化开销。它的三个核心特性决定了它的“性格”无连接发送数据前不需要建立连接直接发——像寄信写完就投递不管对方在不在。不可靠不保证数据到达不保证顺序丢包了就丢了——没有重传没有确认。面向报文应用层交多少数据UDP 就原封不动发多少不会拆分也不会合并。2.2 8 字节固定首部结构UDP 的首部极其精简固定8 字节无选项字段text0 7 8 15 16 23 24 31 -------------------------------- | 源端口号 | 目的端口号 | -------------------------------- | UDP 长度 | UDP 校验和 | --------------------------------字段长度说明源端口号16 bit发送方端口0 表示不关心回复目的端口号16 bit接收方端口UDP 长度16 bit整个报文长度首部 8 字节 数据最小 8最大 65535UDP 校验和16 bit可选IPv6 下必须计算Linux 内核中用struct udphdr结构体描述这个首部。2.3 内核收发路径与“无发送缓冲区”UDP没有发送缓冲区但有接收缓冲区。这意味着发送方应用层调用sendto()后内核直接封装成 UDP 数据报交给 IP 层。如果网卡繁忙数据可能直接在协议栈中丢弃——没有排队重试机制。接收方接收缓冲区用于暂存数据等待应用层读取。但如果缓冲区满了新来的数据会被直接丢弃——这再次体现了 UDP 的“不可靠”。2.4 典型应用场景UDP 的极简设计让它成为实时性优先场景的首选实时音视频RTP宁可丢一帧画面也不能等重传造成卡顿DNS 查询一次查询一个响应简单高效在线游戏位置同步等高频小包延迟敏感QUICHTTP/3 的底层在 UDP 之上实现了可靠的流式传输三、TCP 协议深度拆解重头戏如果说 UDP 是“极简主义”那 TCP 就是“精密仪器”。TCP 的设计目标是在不可靠的 IP 网络之上提供可靠的字节流传输。3.1 20 字节固定首部 可变选项TCP 首部至少20 字节最多 60 字节带选项时text0 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 -------------------------------- | 源端口号 | 目的端口号 | -------------------------------- | 序列号 (32 bit) | -------------------------------- | 确认号 (32 bit) | -------------------------------- | 首部长度 | 保留 |U|A|P|R|S|F| 窗口大小 | -------------------------------- | 校验和 | 紧急指针 | -------------------------------- | 选项 (可选最多40字节) | --------------------------------核心字段解读序列号32 bit当前报文第一个字节的序号用于按序重组和丢包检测确认号32 bit期望收到的下一个字节序号告诉发送方“这之前的我都收到了”首部长度4 bit单位是 32-bit 字最小 5即 20 字节最大 1560 字节标志位6 bitURG、ACK、PSH、RST、SYN、FIN——连接管理的“指令集”窗口大小16 bit接收方通告的可用缓冲区大小最大 65535可通过窗口缩放选项扩展为什么 TCP 需要 20 字节而 UDP 只要 8 字节序列号、确认号、窗口、标志位——每一项都是 TCP 承诺“不丢数据、不乱顺序、不超对方承受能力”的代价。3.2 三次握手为什么不是两次TCP 建立连接需要三次握手第一次SYN客户端 → 服务器SYN1携带初始序列号client_isn第二次SYNACK服务器 → 客户端SYN1ACK1携带服务器的初始序列号server_isn确认号 client_isn 1第三次ACK客户端 → 服务器ACK1确认号 server_isn 1关键问题为什么不是两次握手两次握手无法解决历史连接问题。假设客户端发送的 SYN 报文在网络中滞留客户端超时重传后建立了新连接并完成了数据传输。此时旧 SYN 报文“迟到”到达服务器服务器以为这是一个新连接请求回复 SYNACK——如果只有两次握手服务器会认为连接已建立并开始分配资源但实际上客户端根本不认这个“幽灵连接”。第三次握手的核心作用就是让客户端有机会拒绝这种历史重复请求。初始序列号ISN不是从 0 开始的而是一个 32 位计数器每 4 微秒加 1约 4.55 小时循环一次——这是为了防止序列号重叠。3.3 四次挥手与 TIME_WAITTCP 断开连接需要四次挥手主动关闭方发送 FIN被动关闭方回复 ACK被动关闭方发送 FIN主动关闭方回复 ACK为什么挥手要四次而握手只要三次因为 TCP 是全双工的——握手时 SYN 和 ACK 可以合并SYNACK但挥手时被动关闭方收到 FIN 后可能还有数据要发送所以 ACK 和 FIN 必须分开发。TIME_WAIT 为什么是 2MSL主动关闭方收到对方的 FIN 并回复 ACK 后进入 TIME_WAIT 状态等待2MSLMaximum Segment Lifetime最大报文生存时间后才彻底关闭。两个原因保证最后的 ACK 能被对方收到如果 ACK 丢失对方会重传 FINTIME_WAIT 确保能收到并重发 ACK让旧连接的所有报文在网络中消失防止旧连接的“迟到报文”被新连接误认为是有效数据3.4 可靠传输机制TCP 的可靠性靠一套精密的机制保证。滑动窗口没有滑动窗口时每条数据都要等 ACK 才能发下一条效率极低。滑动窗口允许发送方在收到 ACK 之前连续发送窗口大小以内的数据发送窗口 已发送未确认的数据范围接收窗口 接收方可用的缓冲区大小收到一个 ACK窗口就“滑动”一次继续发送下一条数据窗口大小由接收方根据自身处理能力动态通告。RTO 与超时重传发送方为每个报文启动重传计时器超时未收到 ACK 则重传。RTORetransmission Timeout的计算至关重要太小 → 不必要的重传浪费带宽太大 → 增加延迟RTO 基于加权平均 RTT计算且每次重传后超时时间会翻倍指数退避。快速重传与 SACK如果只靠超时重传丢包检测太慢。快速重传优化了这一点发送方收到3 个重复的 ACK时立即重传丢失的报文不用等超时。但快速重传有个问题如果有多个报文丢失它不知道具体丢的是哪些。SACKSelective Acknowledgment选择性确认解决了这个问题——接收方在 ACK 中明确告知“我收到了哪些乱序数据块”发送方只重传真正丢失的部分。3.5 流量控制零窗口探测流量控制的核心是让发送方不要发得太快以免接收方来不及处理。接收方通过窗口大小字段通告自己的可用缓冲区。当接收方缓冲区满了窗口大小变为0发送方停止发送。但如果这个“零窗口”通告丢失了呢发送方等待窗口更新接收方等待数据——死锁TCP 用零窗口探测打破僵局发送方启动持续计时器超时后发送零窗口探测报文如果窗口仍为 0重置计时器继续等待如果窗口已恢复继续发送数据探测次数一般为 3 次3.6 拥塞控制慢启动、拥塞避免、快重传与快恢复流量控制关注的是端到端接收方能不能收得过来拥塞控制关注的是网络网络能不能承载这么多数据。TCP 拥塞控制有四个经典算法① 慢启动Slow Start刚建立连接时发送方不知道网络带宽有多少从1 个 MSS开始每收到一个 ACK拥塞窗口cwnd翻倍——指数增长直到达到慢启动阈值ssthresh。② 拥塞避免Congestion Avoidance达到 ssthresh 后从指数增长切换到线性增长每经过一个 RTTcwnd 加 1——慢下来避免一下子撑爆网络。③ 快重传Fast Retransmit收到 3 个重复 ACK 时立即重传前面已讲同时将 ssthresh 减半cwnd 设为 ssthresh 3进入快恢复。④ 快恢复Fast Recovery快重传之后不回到慢启动而是直接进入拥塞避免——因为收到 3 个重复 ACK 说明网络仍然能传输数据只是丢了几个包不需要从头慢启动。现代 Linux 内核支持多种拥塞控制算法默认通常是CUBIC基于丢包的算法Google 的BBR基于带宽和延迟的算法也在广泛使用。四、HTTP 协议演进从文本到二进制从 TCP 到 QUICHTTP 是应用层协议它的每一次演进都对应着底层传输机制的优化需求。4.1 HTTP/1.1文本协议与队头阻塞HTTP/1.1 采用纯文本格式传输报文结构清晰textGET /index.html HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 (空行) (响应体)核心痛点队头阻塞Head-of-Line Blocking虽然引入了 Keep-Alive 长连接但请求必须按顺序响应——前一个请求是大文件后面的小请求就得排队等同一域名仅支持 6-8 个并发 TCP 连接多资源页面需排队加载头部冗余每次请求都携带完整头部重复传输4.2 HTTP/2二进制分帧与多路复用HTTP/2 在应用层做了彻底重构二进制分帧将所有数据拆分为帧Frame替代纯文本多路复用同一 TCP 连接上多个请求/响应通过流 ID标识可并行传输、乱序响应——解决了 HTTP 层的队头阻塞HPACK 头部压缩用静态表 动态表压缩重复头部压缩率可达 50%-70%但 HTTP/2 有个“隐形天花板”它仍然跑在 TCP 上。一旦 TCP 层发生丢包整个连接的所有流都会被阻塞——这是TCP 层的队头阻塞。4.3 HTTP/3基于 UDP 的 QUIC 协议HTTP/3 彻底抛弃了 TCP采用基于UDP的QUIC协议。QUIC 的核心优势优势说明0-RTT 快速建连首次 1-RTT后续 0-RTT复用会话票据无队头阻塞多路复用每个流独立——单流丢包不影响其他流连接迁移基于 Connection ID 而非四元组Wi-Fi 切 4G 不断连内置 TLS 1.3加密是协议的一部分不是附加层HTTP/3 的革新本质上是将可靠传输的控制权从操作系统内核TCP上移到用户态QUIC带来了更大的灵活性和更快的迭代速度。五、横向对比与内核视图5.1 TCP vs UDP 对比维度TCPUDP连接性面向连接三次握手无连接可靠性可靠确认 重传不可靠丢包不重传传输方式面向字节流面向报文首部大小20-60 字节8 字节固定发送缓冲区有无流量控制滑动窗口 零窗口探测无拥塞控制慢启动 拥塞避免等无系统资源高维护连接状态低典型场景Web、文件传输、邮件音视频、DNS、游戏5.2 一次 HTTP 请求在内核中的数据流动以一次 HTTP GET 请求为例数据在 Linux 内核中的完整路径应用层浏览器构造 HTTP 请求报文Socket 层通过 Socket 写入内核的TCP 发送缓冲区传输层TCPTCP 将数据分段MSS 大小加上 TCP 首部网络层IP加上 IP 首部查询路由表决定从哪个网卡发出数据链路层加上 Ethernet 首部封装成帧网卡驱动将帧写入ring buffer通过 DMA 交给网卡硬件发送接收路径相反网卡DMA 将帧写入内存触发硬中断通知 CPU软中断ksoftirqd内核线程处理逐层解封装IP 层重组 IP 分片TCP 层按序重组、ACK 确认、放入接收缓冲区Socket 层应用层通过read()读取数据整个过程中Socket 缓冲区是用户态与内核态的数据交换枢纽sk_buff结构体是内核中描述每个报文的核心数据结构。六、实战调优与总结6.1 TCP 常见优化手段①tcp_tw_reuse复用 TIME_WAIT 端口TIME_WAIT 占着端口不释放高并发下可能导致端口耗尽。net.ipv4.tcp_tw_reuse1允许将 TIME_WAIT 状态的 socket 用于新连接。注意仅在客户端发起连接方有效且需要tcp_timestamps开启。tcp_tw_recycle已不推荐使用在 NAT 环境下会引发问题。②TCP_NODELAY禁用 Nagle 算法Nagle 算法会延迟小包的发送以合并成更大的报文但这对低延迟交互如游戏、实时通信是灾难。设置TCP_NODELAY1禁用 Nagle让数据立即发送。代价是网络中可能出现更多小包。③ 调整缓冲区大小高带宽长距离网络高 BDP中默认的缓冲区可能不够大。调整net.core.rmem_max、net.core.wmem_max和net.ipv4.tcp_rmem、net.ipv4.tcp_wmem可以提升吞吐量。④ 调整监听队列net.core.somaxconn控制 listen 队列最大长度高并发场景下默认 128 可能不够。6.2 HTTP 优化思路启用HTTP/2多路复用减少连接数在弱网环境考虑HTTP/3QUIC降低延迟使用TLS 会话复用减少握手开销合理配置Keep-Alive超时平衡连接复用与资源释放写在最后从 UDP 的 8 字节首部到 TCP 的滑动窗口与拥塞控制再到 HTTP 从 1.1 到 3.0 的演进——每一层协议的设计都是对“如何在不可靠网络上可靠、高效地传输数据”这一问题的回答。理解这些底层机制不是让你去改写内核而是让你在面对超时、丢包、延迟抖动时知道从哪里下手排查——是握手阶段的问题是滑动窗口太小是拥塞控制触发了还是应用层的队头阻塞文章篇幅有限更深入的内核源码剖析和真实案例调优已打包成专栏资料。 福利领取方式如果这篇文章帮你打通了网络协议的“任督二脉”欢迎点赞 在看 转发让更多被网络问题困扰的开发者看到。