tcp-transport-layer-protocol

发布时间:2026/6/28 22:50:53
tcp-transport-layer-protocol Linux 传输层 TCP 协议详解可靠性、连接管理、滑动窗口与粘包问题摘要TCP 是传输层中最重要的协议之一。它提供面向连接、可靠传输、面向字节流的通信能力但这些能力背后并不简单。本文从 TCP 报头格式讲起逐步分析确认应答、超时重传、三次握手、四次挥手、TIME_WAIT、CLOSE_WAIT、滑动窗口、流量控制、拥塞控制、延迟应答、捎带应答以及粘包问题帮助你把 TCP 的核心机制串成一条完整主线。前言UDP 的特点是简单无连接、不保证可靠、面向数据报。TCP 正好走了另一条路它希望在复杂网络环境中尽可能保证数据可靠、有序地到达对端同时还要尽量提高传输效率。这也是 TCP 难学的原因它不是单靠一个机制实现可靠传输而是由一组机制共同配合完成的。可以先记住这条主线可靠性校验和、序列号、确认应答、超时重传、连接管理、流量控制、拥塞控制 性能滑动窗口、快重传、延迟应答、捎带应答 应用视角面向字节流需要应用层处理消息边界下面按这条线展开。一、TCP 报头格式TCP 报文段由 TCP 首部和数据两部分组成。首部里保存了端口、序号、确认号、标志位、窗口大小、校验和等控制信息。0 15 16 31 ---------------------------------- | 16位源端口号 | 16位目的端口号 | ---------------------------------- | 32位序号 | ----------------------------------- | 32位确认序号 | -------------------------------- |首部|保留位| 标志位 |窗口大小| -------------------------------- | 16位校验和 | 16位紧急指针 | ---------------------------------- | 选项 | ----------------------------------- | 数据 | -----------------------------------几个关键字段字段作用源端口号 / 目的端口号标识数据从哪个进程来到哪个进程去32 位序号标识当前发送数据在字节流中的位置32 位确认序号告诉对方下一次应该从哪里开始发送4 位首部长度表示 TCP 首部有多少个 4 字节最大首部长度为15 * 4 60字节16 位窗口大小用于流量控制告诉对端当前还能接收多少数据16 位校验和用于检查 TCP 首部和数据是否出错16 位紧急指针配合URG使用标识紧急数据位置TCP 的 6 个经典标志位也很重要标志位含义URG紧急指针是否有效ACK确认号是否有效PSH提示接收端尽快把数据交给应用层RST复位连接通常表示连接异常需要重新建立SYN请求建立连接FIN通知对方本端准备关闭连接理解这些字段之后后面再看握手、挥手、重传、窗口控制就不会觉得它们是孤立概念。二、确认应答TCP 如何知道数据到了TCP 会把字节流中的每个字节都编号这个编号就是序列号。接收端收到数据后会返回 ACK并在确认号中告诉发送端我已经收到哪些数据了下一次请从哪个位置继续发送。例如发送端发送了字节范围1 ~ 1000接收端正确收到后返回的确认号可能是ACK 1001意思是1001之前的数据我都收到了下次从1001开始发。这个设计有两个好处发送方知道哪些数据已经安全到达接收方可以识别重复数据避免 ACK 丢失导致的重复报文被错误交给应用层。很多初学者会把 ACK 理解成“收到了某个包”。更准确地说ACK 表达的是到某个字节位置为止的数据已经连续收到。三、超时重传ACK 没回来怎么办网络可能丢数据也可能丢 ACK。如果发送端在一段时间内没有收到确认就会重新发送对应数据这就是超时重传。超时时间不能乱设超时时间设置可能问题太长丢包后等待过久降低重传效率太短ACK 还在路上就重发造成重复报文TCP 会根据网络情况动态计算超时时间。在常见系统中超时重传常以一定时间单位递增如果重传一次后仍然没有应答等待时间会继续扩大例如500ms、2 * 500ms、4 * 500ms这种指数退避思路。如果累计重传多次仍然失败TCP 会认为网络或对端主机出现异常最终关闭连接。四、连接管理三次握手与四次挥手TCP 是面向连接的协议。通信前需要建立连接通信结束后需要释放连接。1. 三次握手建立连接三次握手可以用下面的流程表示服务端客户端服务端客户端SYNSYN ACKACK对应状态变化大致是一端状态变化服务端CLOSED - LISTEN - SYN_RCVD - ESTABLISHED客户端CLOSED - SYN_SENT - ESTABLISHED服务端调用listen后进入LISTEN状态等待连接请求。客户端调用connect后发送SYN。服务端收到后回复SYN ACK客户端再回复ACK连接进入可读写状态。2. 四次挥手断开连接断开连接通常需要四次挥手被动关闭方主动关闭方被动关闭方主动关闭方FINACKFINACK为什么关闭连接比建立连接多一次因为 TCP 是全双工的。A 不想发了不代表 B 也立刻没有数据要发。B 可以先确认 A 的关闭请求等自己数据处理完后再发送自己的FIN。主动关闭方常见状态变化ESTABLISHED - FIN_WAIT_1 - FIN_WAIT_2 - TIME_WAIT - CLOSED被动关闭方常见状态变化ESTABLISHED - CLOSE_WAIT - LAST_ACK - CLOSED五、TIME_WAIT为什么端口刚释放还不能立刻复用主动关闭连接的一方会进入TIME_WAIT等待2MSL后才回到CLOSED。MSL是报文最大生存时间。等待2MSL主要有两个意义让旧连接中迟到的报文在网络中自然消失避免影响后续同五元组连接保证最后一个 ACK 有机会重发。如果最后一个 ACK 丢失对端会重新发送 FIN处于TIME_WAIT的一方仍然可以再次回应 ACK。实际开发中经常会遇到服务刚停止又马上启动结果bind失败Address already in use一种常见处理方式是在创建 socket 后设置SO_REUSEADDRintopt1;setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,opt,sizeof(opt));它可以缓解服务重启时端口复用问题。不过这不是让所有连接状态都“消失”也不是解决连接管理问题的万能开关。真正的高并发服务还需要合理设计连接关闭方、连接复用和超时策略。可以通过命令观察连接状态netstat-antp或ss-antp六、CLOSE_WAIT服务器忘记 close 的典型信号CLOSE_WAIT表示对端已经发送FIN本机也已经回复 ACK但本机应用层还没有真正关闭 socket。如果服务器上出现大量CLOSE_WAIT通常说明应用程序没有正确关闭连接。典型场景是读到对端断开后只是跳出循环却忘记调用closebooloknew_sock.Recv(req);if(!ok){// 对端已经关闭不能只 breaknew_sock.Close();break;}这里需要特别注意CLOSE_WAIT往往不是内核“卡住了”而是应用层没有把关闭流程走完。补上对应的close四次挥手才能继续推进。七、滑动窗口可靠传输如何提高效率如果 TCP 每发送一个数据段都等待 ACK再发送下一个可靠是可靠但性能会很差。尤其是在 RTT 较大的网络中大量时间都浪费在等待上。滑动窗口的思想是在没有收到 ACK 之前也允许连续发送一批数据。发送序列 1~1000 1001~2000 2001~3000 3001~4000 4001~5000 ... 窗口覆盖 [1~1000][1001~2000][2001~3000][3001~4000]如果窗口大小是 4000 字节发送端可以一次发出四个段。收到ACK 1001后说明第一个段已确认窗口向后滑动可以继续发送4001~5000。滑动窗口需要发送缓冲区配合。已经发送但尚未确认的数据不能删除因为它们可能需要重传。只有收到确认后内核才能把对应数据从发送缓冲区移除。窗口越大理论吞吐率越高但窗口不能无限大因为接收端处理能力和网络拥塞状态都有限。八、快重传不必总等超时丢包时有两种情况情况处理思路数据到了但 ACK 丢了后续 ACK 可以继续确认不一定需要重传数据段丢了接收端会持续请求缺失位置的数据假设发送端发出1~1000, 1001~2000, 2001~3000, 3001~4000 ...如果1001~2000丢了接收端即使收到了后面的数据也会不断返回ACK 1001发送端连续收到多个相同 ACK 后就能判断某段数据大概率丢失于是立即重发缺失数据而不是傻等超时。这种机制常称为快重传。快重传提升的是丢包恢复速度它和超时重传一起构成 TCP 可靠性的重要补充。九、流量控制别把接收端撑爆接收端处理数据的速度是有限的。如果发送端发得太快接收缓冲区被打满继续发送就可能导致丢包和重传。TCP 通过窗口大小字段实现流量控制接收端在 ACK 中携带自己还能接收多少数据发送端根据这个窗口值调整发送速度如果接收端缓冲区满了窗口可能变成 0发送端暂停发送并定期发送窗口探测报文等待窗口恢复。需要注意TCP 首部里的窗口字段是 16 位看起来最大只能表示65535。实际 TCP 还可以通过选项中的窗口扩大因子扩展窗口大小实际窗口可以理解为实际窗口大小 窗口字段值 窗口扩大因子十、拥塞控制别把网络压垮流量控制关注的是接收端能不能处理拥塞控制关注的是网络路径能不能承受。即使接收端缓冲区很大也不代表网络中间链路一定空闲。如果刚开始就大量发送数据可能让本来就拥塞的网络更加拥堵。TCP 引入拥塞窗口cwnd。实际发送窗口通常取下面两者中的较小值实际发送窗口 min(接收端窗口, 拥塞窗口)慢启动的大致过程连接开始时拥塞窗口较小每收到一个 ACK拥塞窗口增长初期增长很快呈指数趋势超过慢启动阈值后改为线性增长发生超时重传时阈值降低拥塞窗口回到较小值。否是是否连接开始拥塞窗口较小收到 ACK 后增长是否超过慢启动阈值指数增长线性增长是否发生严重丢包阈值减半拥塞窗口回落拥塞控制的目标不是一味保守而是在尽可能提高吞吐量的同时不把网络推向更严重的拥塞。十一、延迟应答与捎带应答1. 延迟应答如果接收端立刻返回 ACK窗口可能比较小。比如接收缓冲区有 1M刚收到 500K 数据时立刻应答返回窗口可能只有 500K。但如果应用层很快消费掉这 500K稍等一会儿再应答窗口可能恢复到 1M。所以 TCP 可以适当延迟 ACK以便返回更大的窗口提高吞吐率。延迟应答通常受两个条件限制限制含义数量限制每隔一定数量的数据段必须应答时间限制超过最大延迟时间必须应答常见实现中数量可能取 2时间可能取 200ms 左右具体取决于系统实现。2. 捎带应答很多应用是“一问一答”的模式。客户端发来数据后服务器不仅要回 ACK还要返回业务响应。此时 ACK 可以和业务响应一起发回去这就是捎带应答。它减少了单独 ACK 报文数量也让通信更高效。十二、面向字节流与粘包问题创建一个 TCP socket 时内核会维护发送缓冲区和接收缓冲区。调用write时数据先进入发送缓冲区数据太长可能拆成多个 TCP 段发送数据太短可能先留在缓冲区等到合适时机再发接收端从接收缓冲区调用read取数据一次write和一次read没有严格对应关系。这就是 TCP 面向字节流的含义。例如发送端写 100 字节write 100 字节接收端可以一次读 100 字节也可以分多次读完。反过来发送端多次小write接收端也可能一次read读到合并后的数据。所谓粘包问题里面的“包”指的是应用层消息。站在 TCP 传输层角度数据按序进入缓冲区站在应用层角度看到的是连续字节流不知道哪一段是一条完整业务消息。解决办法只有一个核心明确应用层消息边界。常见做法做法适用场景固定长度每条消息大小固定长度字段 正文常见通用方案分隔符文本协议中常见但要避免正文和分隔符冲突UDP 通常没有 TCP 意义上的粘包问题因为 UDP 面向数据报接收端要么收到一个完整数据报要么收不到不会把多个应用数据报合成一条字节流交给应用层。十三、TCP 异常情况实际运行中连接不一定总是正常关闭。异常表现进程终止文件描述符释放仍然可以触发正常关闭流程机器重启类似进程终止连接会被释放掉电或网线断开对端可能暂时认为连接还在后续写入可能触发RSTTCP 内部有保活定时器可以定期探测对端是否还存在。很多应用层协议也会实现自己的心跳机制用来更快发现断线。十四、TCP 和 UDP 怎么选不能简单说 TCP 一定优于 UDP。它们是不同取舍下的工具。对比项TCPUDP连接面向连接无连接可靠性提供可靠传输机制协议层不保证可靠数据模型字节流数据报消息边界应用层自己处理保留数据报边界典型场景文件传输、重要状态更新、HTTP、SSHDNS、实时音视频、广播、低延迟场景如果要基于 UDP 实现可靠传输就要在应用层参考 TCP 的思路补机制引入序列号保证顺序引入确认应答确认对端收到引入超时重传丢了就补发根据需要设计窗口、流控和拥塞策略。这也是很多面试题喜欢问“如何用 UDP 实现可靠传输”的原因本质上是在考你是否理解 TCP 的可靠性组件。十五、常见问题与易错点1. 认为 TCP 没有丢包TCP 不是让网络不丢包而是在丢包后通过确认、重传、序列号等机制恢复。2. 认为一次 write 对应一次 readTCP 是字节流不保留应用层消息边界。程序必须自己定义协议格式不能依赖读写次数天然匹配。3. 看到 TIME_WAIT 就觉得是 bugTIME_WAIT是 TCP 正常连接管理的一部分。真正需要关注的是数量是否异常、是否影响端口复用和连接建立。4. 大量 CLOSE_WAIT 不处理大量CLOSE_WAIT往往说明应用程序没有正确关闭 socket这是实实在在的代码问题。5. 把流量控制和拥塞控制混为一谈流量控制看接收端拥塞控制看网络路径。一个保护对端缓冲区一个保护网络整体。总结TCP 的复杂性来自它的目标既要可靠又要尽量高效。为了可靠它引入校验和、序列号、ACK、超时重传、连接管理、流量控制和拥塞控制为了性能它又引入滑动窗口、快重传、延迟应答和捎带应答。写 TCP 程序时最关键的应用层认知是TCP 提供的是连续字节流不提供业务消息边界。只要把可靠性机制、连接状态和字节流模型理顺再去排查TIME_WAIT、CLOSE_WAIT、粘包、吞吐量低等问题就会清楚很多。