深入浅出传输层:UDP 与 TCP 协议全景指南
文章目录第一讲传输层基石与 UDP 协议剖析端口概念、UDP 特性、报文机制1. 核心前提数据如何找到对应的程序2. UDP 协议寄明信片式的通信3. UDP 的缓冲区与全双工4. 必要的系统调用演示本讲高频面试题解析第二讲TCP 初探与连接管理报文结构、三次握手、四次挥手、状态流转深度解析1. 认识 TCP 的“控制开关”2. 建立连接三次握手 (Three-way Handshake)3. 断开连接四次挥手 (Four-way Wave)4. 关键状态深度剖析5. 必要的系统调用演示本讲高频面试题解析第三讲TCP 可靠性基石确认应答、超时重传、序列号机制1. 序列号 (Sequence Number)给每一个字节贴上编号2. 确认应答 (ACK) 机制明确无误的回执3. 超时重传机制耐心等待与重复丢弃本讲高频面试题解析第四讲TCP 性能起飞滑动窗口、快重传、流量控制与拥塞控制1. 滑动窗口 (Sliding Window)批量发送的艺术2. 快重传 (Fast Retransmit)高速异常处理3. 流量控制 (Flow Control)照顾读者的接收极限4. 拥塞控制 (Congestion Control)照顾整个社会的快递网络本讲高频面试题解析第五讲TCP 特性进阶与终极对比延迟应答、捎带应答、粘包问题及场景剖析1. 延迟应答 (Delayed ACK)让子弹飞一会儿2. 捎带应答 (Piggybacking)顺风车模式3. 面向字节流与“粘包问题” (核心避坑指南)4. TCP 异常情况处理5. TCP 与 UDP 终极对比本讲高频面试题解析第一讲传输层基石与 UDP 协议剖析端口概念、UDP 特性、报文机制1. 核心前提数据如何找到对应的程序在讲解具体协议前我们需要先明确一个概念网络通信的本质是两台机器上的进程在交换数据。你可以把互联网想象成一个庞大的城市群IP 地址就像是小区的详细地址比如XX路XX号。它负责把数据包送达到特定的那台电脑主机上。端口号 (Port)就像是小区里面具体的门牌号。一台电脑上同时运行着浏览器、QQ、微信等多个程序数据包到了电脑后操作系统就是通过端口号来决定把数据交给哪个具体的应用程序。为了在茫茫网海中唯一确定一次通信网络底层采用了一个五元组来进行标识源 IP、源端口号、目的 IP、目的端口号、协议号。------------------- ------------------- | 主机 A | | 服务器 | | | | | | ------------- | 网络传输轨迹 | ------------- | | | 浏览器(2001) | | ------------------------- | | HTTP服务(80)| | | ------------- | 源IP: 172.20.100.34 | ------------- | | | 目的IP: 172.20.100.32 | | ------------------- -------------------注0到1023是系统保留的知名端口比如 HTTP 默认占用 80 端口SSH 占用 22 端口。我们自己写 C 服务端程序时一定要避开这些范围通常选择 1024 到 65535 之间的动态端口。2. UDP 协议寄明信片式的通信UDP用户数据报协议的核心设计理念就是简单、直接。它的传输过程非常像是在邮局寄明信片。无连接你寄明信片不需要先给对方打个电话确认只要知道对方的地址IP 和端口填好扔进邮筒就可以直接发送。不可靠明信片在半路上如果丢了、被大雨淋湿了邮递员不会通知你也不会帮你重新寄一张。UDP 也是如此没有确认应答机制也没有重传机制数据发出去就不管了。面向数据报你在这张明信片上写了多少字对方收到就是多少字。UDP 绝对不会把你的数据拆成两半发送也不会把两次发的数据合并到一起。应用层交给 UDP 多长的报文它就原样发送。比如你用 UDP 发送了 100 个字节发送端调用一次发送函数接收端必须对应调用一次接收函数完整收取这 100 个字节绝不能分 10 次每次收 10 个字节。3. UDP 的缓冲区与全双工在操作系统底层UDP 的收发机制非常干净利落发送端UDP 没有真正意义上的发送缓冲区。你的代码一旦调用发送函数数据就立刻交给了系统内核内核直接把它丢给网络层发走。接收端UDP 具有接收缓冲区。网络上源源不断来的数据包会先暂存在这个缓冲区里等你的程序来读。但注意如果你的程序读得太慢导致缓冲区满了后续再到达的 UDP 数据包就会被内核毫不留情地直接丢弃。同时UDP 的套接字Socket是全双工的意味着同一个通道你可以一边源源不断地发数据一边同时收数据互不干扰。4. 必要的系统调用演示在 Linux 环境下使用 C 编写 UDP 通信有两个最基础的系统调用函数你必须掌握函数1socket原型int socket(int domain, int type, int protocol);大白话说明这就好比去电信营业厅申请一部座机电话调用成功后系统会交给你一个电话把手文件描述符你后续所有的网络收发操作都要对着这个把手进行。函数2bind原型int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);大白话说明光有电话把手还不行这个函数的作用是给你的座机插上电话线并绑定一个专属的电话号码IP 和端口号这样别人的数据包才能准确找到你的程序。简单的 C 服务端绑定演示无需完整执行仅作逻辑参考int sock socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in local; local.sin_family AF_INET; local.sin_port htons(8080); local.sin_addr.s_addr INADDR_ANY; bind(sock, (struct sockaddr*)local, sizeof(local));本讲高频面试题解析1. 一台主机上的一个进程是否可以 bind 多个端口号可以。一个进程可以创建多个 Socket文件描述符每一个 Socket 都可以独立 bind 一个不同的端口号。就像一个大公司进程可以申请多部不同的座机电话端口号来接听不同业务的来电。2. 一个端口号是否可以被多个进程 bind通常情况下不可以。端口号是用来唯一标识接收端进程的如果多个进程绑定同一个端口操作系统收到数据包后就不知道该把数据交给谁了。这就好比一个电话号码不能同时分配给两户独立的人家。注在 Linux 中通过特殊选项如 SO_REUSEPORT 可以实现多进程监听同端口用于负载均衡但在没有特殊配置的常规语境下答案是否定的。3. UDP 协议能够传输的最大数据是多少如果超过了会怎样UDP 协议首部自带一个 16 位的长度字段这意味着一个 UDP 数据报包含头部和数据的最大总长度是 65535 字节约 64K。在当今互联网环境下64K 是一个非常小的限制。如果需要传输大于 64K 的数据开发者必须在应用层用 C 代码手动对数据进行分包发送并在接收端手动将这些小包拼装成完整数据。第二讲TCP 初探与连接管理报文结构、三次握手、四次挥手、状态流转深度解析如果说上一讲的 UDP 是随手丢进邮筒、不管死活的明信片那么 TCP传输控制协议就是需要双方先打电话沟通、过程严密监控、并且每次交接都要求签字回执的“机密级保价快递”。TCP 的设计初衷是为了在不可靠的网络环境中硬生生砸出一条可靠的传输通道。要做到这一点第一步就是建立和断开连接。1. 认识 TCP 的“控制开关”在 TCP 发送的每一个数据包报文头部除了和 UDP 一样有源端口和目的端口外还有 6 个非常关键的“标志位”由 0 和 1 组成的开关。我们今天主要认识其中三个负责连接管理的开关SYN (Synchronize)同步开关。当这个开关置为 1 时表示“我想和你建立连接”。我们称之为同步报文段。FIN (Finish)结束开关。当置为 1 时表示“我的数据发完了我们断开连接吧”。我们称之为结束报文段。ACK (Acknowledge)确认开关。当置为 1 时表示“你刚才说的话我收到了”。几乎除了最开始的请求外后续所有包的 ACK 都会是 1。2. 建立连接三次握手 (Three-way Handshake)TCP 通信的第一步就像两个人打电话。必须确认双方的麦克风发送能力和听筒接收能力都正常才能开始聊正事。客户端状态 (Client) 服务端状态 (Server) CLOSED LISTEN (等待呼叫) | | | ------------- (1) SYN (我想连你) ------------- | SYN_SENT SYN_RCVD (收到请求) | | | ------- (2) SYN ACK (好我也连你收到) ------- | | | ESTABLISHED ------- (3) ACK (收到你的同意) --------- | (可以发数据了) ESTABLISHED (可以发数据了)一环扣一环的逻辑第一次握手客户端发送 SYN。服务端收到后服务端可以得出结论客户端的发送正常我的接收正常。第二次握手服务端回复 SYN我也要连你 ACK你的请求我收到了。客户端收到后客户端得出结论我的收发正常服务端的收发也正常。此时客户端单方面认为连接已经建立进入ESTABLISHED状态。第三次握手但这还不够服务端此时还不知道自己的发送能力好不好。所以客户端必须再回一个 ACK。服务端收到后彻底放心我的发送和接收也都正常。双方正式进入ESTABLISHED状态。3. 断开连接四次挥手 (Four-way Wave)数据传完了要拆除通道。由于 TCP 是全双工的双向通道独立存在所以断开连接就像男女朋友和平分手双方都要明确表态“我没有话要说了”。主动断开方 (常为Client) 被动断开方 (常为Server) ESTABLISHED ESTABLISHED | | | ------------- (1) FIN (我发完了) ------------- | FIN_WAIT_1 CLOSE_WAIT (准备关闭) | | | ------------ (2) ACK (知道了) ---------------- | FIN_WAIT_2 | | (服务端可能还有剩余数据要发此时继续发) | | | ------------ (3) FIN (我也发完了) ------------ | TIME_WAIT LAST_ACK (等最后确认) | | | ------------- (4) ACK (彻底拜拜) ------------- | | CLOSED (等待 2MSL 时间) | CLOSED逻辑推演客户端发 FIN表示自己不再发送数据了但还能接收。服务端回 ACK表示收到了。此时进入半关闭状态。服务端把手头还没处理完的数据继续处理发走。服务端确实没东西发了也发送 FIN。客户端回 ACK。彻底结束。4. 关键状态深度剖析初学者最容易在面试和实战中栽在以下两个状态上TIME_WAIT (客户端的倔强等待)主动断开连接的一方在发送完最后一次 ACK 后不能立刻销毁自己必须在原地进入TIME_WAIT状态死等2MSL最大报文生存时间的 2 倍Linux 通常默认约 60 秒。为什么因为如果最后这个 ACK 迷路丢了服务端收不到确认就会以为自己的 FIN 没发出去从而重发一遍 FIN。如果客户端不等待直接退出了收到重发的 FIN 就会引发混乱。等 2MSL 就是为了确保最后一个 ACK 绝对到达同时让本次连接在网络中迷路的残余数据包彻底自然消亡干干净净。CLOSE_WAIT (服务端的致命 BUG)被动方收到 FIN 并回复 ACK 后就会进入CLOSE_WAIT状态。此时被动方需要在代码逻辑里把剩下的事情做完然后主动调用close()发送 FIN 才能往下走。如果你在 Linux 上用netstat命令发现服务器上有大量的CLOSE_WAIT别怀疑一定是你的 C 业务代码里有 BUG在连接断开时忘了调用 close() 关闭文件描述符Socket。5. 必要的系统调用演示在实现 TCP 服务端时相比 UDP我们需要新的系统调用来支撑“连接”的概念。函数1listen原型int listen(int sockfd, int backlog);大白话说明这就好比营业厅把你的座机设置成了“客服热线”模式让这个 Socket 从主动变被动开始在后台静静地排队监听外面打进来的电话backlog参数就是排队等候的最大长度。函数2accept原型int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);大白话说明这就好比客服专员按下接听键从外面排队打进来的电话中接起一个。它会返回一个全新的电话把手新 Socket专门用来和这个特定的客户一对一单线联系而原来的客服热线监听 Socket还在继续接别人的电话。函数3connect原型int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);大白话说明这是客户端专用的函数好比拿着电话把手主动拨打服务端的电话号码调用这个函数的过程底层就在自动帮你完成那著名的“三次握手”。C TCP 服务端连接搭建 Demo示意伪代码逻辑// 1. 创建套接字intlisten_socksocket(AF_INET,SOCK_STREAM,0);// SOCK_STREAM 代表面向字节流的TCP// 2. 绑定IP和端口 (和UDP一样)structsockaddr_inlocal;// ... 填充 local 结构体信息 (IP:INADDR_ANY, Port:8080)bind(listen_sock,(structsockaddr*)local,sizeof(local));// 3. 启动监听转为被动接收状态listen(listen_sock,5);// 4. 死循环处理外部连接while(true){structsockaddr_inclient_addr;socklen_t lensizeof(client_addr);// 阻塞等待直到有客户端完成三次握手进来intclient_sockaccept(listen_sock,(structsockaddr*)client_addr,len);if(client_sock0){// 连接建立成功开始使用 client_sock 进行 read/write 收发数据// ... (处理业务)// 业务处理完毕必须调用 close否则会导致 CLOSE_WAIT 泄漏close(client_sock);}}本讲高频面试题解析1. 为什么建立连接是三次握手而不是两次或者四次解析与答案网络是不可靠的建立可靠连接的底线是双方都要确认自己的发送和接收能力正常。如果是两次握手服务端收到 SYN 只能确认客户端能发自己回复了 SYNACK 后服务端不知道客户端有没有收到即服务端无法确认自己的发送能力是否正常。这就容易导致一种极端情况服务端回复完就以为连接建立了分配了内存资源但网络拥堵导致客户端没收到客户端继续发请求服务器就会产生大量死连接SYN 洪泛攻击的原理。如果是四次握手服务端把 SYN 和 ACK 分两次发完全可以但没必要一起发可以提高效率所以合并成了三次。2. 为什么断开连接需要四次挥手而不是三次解析与答案因为 TCP 是全双工的。当客户端发送 FIN 时只是意味着“客户端到服务端”的单向通道关闭了客户端不发了。但是服务端收到 FIN 时手头可能还有没发完的数据不能立刻也跟着发 FIN。所以服务端只能先回一个 ACK 安抚客户端导致两次挥手。等服务端磨磨唧唧把所有剩余业务数据发完才会发送自己的 FIN第三次挥手客户端再确认第四次挥手。所以中间的 ACK 和 FIN 中间夹杂着业务数据的发送无法像握手那样合并。3. 解决服务器 TIME_WAIT 状态引起的 bind 失败的方法是什么解析与答案如果服务器主动断开连接然后挂掉重启时会发现端口被占用Address already in use这就是因为底层的连接还卡在 2MSL 的TIME_WAIT状态。在 C 服务器开发中我们可以通过系统调用setsockopt设置SO_REUSEADDR选项。它相当于告诉系统允许创建端口号相同但 IP 地址不同的多个 Socket 描述符直接绕过这个等待限制进行端口复用。第三讲TCP 可靠性基石确认应答、超时重传、序列号机制在上一讲中我们通过三次握手建立了一条专线。但真实的网络环境非常恶劣数据包在路由器之间传递时随时可能因为网络拥堵、设备断电而“半路失踪”或者因为不同路径导致“后发先至”。为了把复杂的机制讲透我们来模拟一个“跨国对讲机汇报机密文件”的场景你要向远在海外的老板汇报一份 10000 字的商业计划书但对讲机信号极差随时有杂音甚至会断断续续。TCP 是怎么保证老板能一个字不差地听完的1. 序列号 (Sequence Number)给每一个字节贴上编号如果对讲机信号不好你肯定不会一口气把 10000 字全念完那样一旦中间断了老板根本不知道从哪里接上。TCP 的做法极其严谨它把要发送的数据全部拆散并且给每一个字节注意是每一个字节不是每一个包都编上号。这就好比你把 10000 字的计划书里的每一个字都标上序号第1字、第2字…第10000字。第1字节 第2字节 第10000字节 ------------------- ...... ----------- | 字 | 符 | | 据 | ------------------- ...... ----------- ^ ^ ^ 序号1 序号2 序号10000发送时TCP 会把一小批连着的字比如第 1 到 1000 个字节打包成一个“报文段”发出去并且在包裹外面写上“本包裹从序号 1 开始”。有了这个序号接收方就算先收到了后发的包裹也能根据序号在本地缓冲区里把它们重新拼成正确的顺序解决乱序问题。2. 确认应答 (ACK) 机制明确无误的回执老板在对讲机那头听到了你念的第 1 到 1000 个字。为了让你放心他必须给你一个反馈。TCP 的确认应答不是简单地说一句“我收到了”。TCP 的 ACK 会带有一个“确认序列号”。它的核心含义是“前面的我都收到了下一次请从第 N 个字节开始发给我。”发送方 (你) 接收方 (老板) | | | ------------- 数据 (序号 1 ~ 1000) -------- | | | | --------- 确认应答 (下一个请发 1001) ------ | | | | ----------- 数据 (序号 1001 ~ 2000) ------- | | | | --------- 确认应答 (下一个请发 2001) ------ |这种设计非常巧妙。假设你一口气发了两个包1~ 10001001~2000老板回了一个“下一个请发 2001”。就算第一个确认包下一个发1001半路丢了你只要收到了“2001”的确认你就能百分之百断定2001 之前的所有数据老板已经全部安全收到了。3. 超时重传机制耐心等待与重复丢弃在极差的对讲机环境中总会有意外发生。TCP 的超时重传机制完美处理了两种最常见的丢包场景。场景一你发的数据在路上丢了你把第 1001~2000 个字念出去了等了半天对讲机没动静。TCP 内部有一个定时器只要在一个“特定的时间间隔”内没有收到老板的回执TCP 就会认为包丢了自动把 1001~2000 的数据重新发一遍。场景二老板的回执在路上丢了导致重复发送老板其实收到了 1001~2000并且回复了“下一个发 2001”但这句话在对讲机里被杂音吞了。你等了一会没听到反馈以为刚才发的老板没听到于是又念了一遍 1001~2000。此时老板收到了两份一模一样的内容。老板会懵吗不会因为每个数据包都有序列号。老板一看这批字的序号我刚才已经收过了于是直接把重复的包丢弃去重并再次回复“我已经收到了下一个请发 2001”发送方 (你) 接收方 (老板) | | | ----------- 数据 (1001 ~ 2000) ------------ | 等待ACK | | (老板收到并回复 超时触发 | ----------- X 确认 (下发2001) 丢失 X ------- | 但回执丢了) | | | (重传) 数据 (1001 ~ 2000) | (老板发现序号重复 | | 直接丢弃并重发回执) | --------- 确认应答 (下一个请发 2001) ------ |超时的时间到底定多久如果定得太长重传效率极低定得太短网络稍微卡一下就疯狂重发。TCP 采用了动态调整的策略在 Linux 中超时时间通常以 500ms 为单位。第一次没回音等 500ms 重传。如果重发一次还不行等 2 * 500ms 重传。还不行等 4 * 500ms以此类推呈指数形式递增。当累计重传次数达到一定阈值TCP 就会绝望认为网络已经彻底瘫痪强制关闭连接。本讲高频面试题解析1. TCP 为什么能保证数据按顺序到达且不重复解析与答案核心在于序列号Sequence Number机制。TCP 传输时将每个字节的数据都进行了编号。接收端可以通过序列号将网络中乱序到达的数据包重新排序拼接同时如果因为超时重传导致接收端收到了相同序列号的数据包接收端操作系统内核可以利用序列号精准识别出重复数据并将其直接丢弃从而实现去重。2. 简述 TCP 的超时重传机制以及超时时间的确定策略。解析与答案当发送端发出数据后如果在一个特定的时间间隔内未收到接收端的确认应答ACK就会重新发送该数据段无论是因为数据丢了还是 ACK 丢了。超时时间是动态计算的以 500ms 为单位采用指数避退的原则500ms, 1000ms, 2000ms…。连续多次重发无果后TCP 会认为对端主机异常或网络故障强制断开连接。3. 经典开放题如何用 UDP 实现可靠传输解析与答案这是一道非常考验底层理解的综合题。UDP 本身不可靠要实现可靠传输必须在应用层比如你的 C 代码中去“抄袭” TCP 的作业引入序列号在应用层协议头部加上 ID 编号保证接收端能按序组装数据并丢弃重复包。引入确认应答接收端收到数据后必须向发送端回传一个带有对应 ID 的确认包。引入超时重传发送端在内存中维护一个定时器和未确认数据的缓存如果超过设定时间没收到对应 ID 的应答就从缓存中调出数据重新调用sendto发送。第四讲TCP 性能起飞滑动窗口、快重传、流量控制与拥塞控制在上一讲中我们弄懂了 TCP 是如何通过“发一个包等一个确认再发下一个包”来保证绝对可靠的。但是这种“一发一收”的串行模式太慢了。如果两台机器相隔万里数据往返一次需要很久那传输效率简直不堪入目。为了让性能起飞TCP 引入了一套非常优雅的组合拳。我们把场景升级一下你现在是一个图书出版商要把一套 10000 页的百科全书邮寄给远方的读者。1. 滑动窗口 (Sliding Window)批量发送的艺术既然一页一页寄太慢那我们就一次性寄出一大批。TCP 允许发送方在没有收到确认应答ACK的情况下连续发送多个数据段。这个无需等待确认就可以发送的最大数据量就叫窗口大小。场景模拟假设窗口大小是 4000 字节相当于一次可以连发 4 个 1000 字节的包裹。你一口气把第 1 到 4000 字节包裹 A、B、C、D全寄出去了。当你收到包裹 A 的确认回执后说明包裹 A 已经安全抵达。此时你的“窗口”就可以向前滑动一下把你配额里的空位腾出来继续寄出包裹 E4001-5000 字节。 批量发送与窗口滑动演示 [发送方视角] 数据 1---1000---2000---3000---4000---5000---6000 窗口 [---------------------------] (一口气发4个包裹无需等待) 收到 1000 的确认后窗口向右滑动 数据 1---1000---2000---3000---4000---5000---6000 窗口 [---------------------------] (腾出额度立刻发送 4001-5000)在操作系统的内核中这个机制是通过发送缓冲区来实现的。只有被确认应答过的数据才能从缓冲区里真正删掉没被确认的数据必须留在窗口里以备不测。2. 快重传 (Fast Retransmit)高速异常处理既然是批量发送万一中间某个包裹丢了怎么办滑动窗口机制下有两种丢包情况处理方式极其聪明。情况一回执ACK丢了包裹 A、B、C 都到了但读者回复的“A收到下发B”和“B收到下发C”的回执在半路丢了。结果完全不要紧因为只要你收到了“C收到下一个请发D”的回执你就百分百确定 A 和 B 绝对已经到了这叫累积确认。情况二真正的包裹丢了触发快重传如果你一口气发了 1、2、3、4、5 号包裹。结果 2 号包裹掉河里了。读者收到了 1回复“下一个请发 2”。接着读者收到了 3发现 2 还没来读者不管继续回复“下一个请发 2”。收到 4回复“下一个请发 2”。收到 5回复“下一个请发 2”。你作为发送方连续收到了三个完全一样的确认应答“下一个请发 2”。这时候你立刻心领神会2 号包裹绝对是丢了此时你根本不需要等待超时重传的定时器到期而是立刻把 2 号包裹重新补发过去。读者收到补发的 2 号后因为之前 3、4、5 都已经存在接收缓冲区里了所以会直接回复“下一个请发 6”。3. 流量控制 (Flow Control)照顾读者的接收极限读者的书桌接收缓冲区大小是有限的。如果你发的太猛把读者的书桌堆满了再寄过去的书就会掉在地上丢包引发一连串的重传灾难。机制TCP 的接收端会在每次回复 ACK 时在 TCP 报文头部的16位窗口大小字段里填上自己当前“还剩多少空桌子”缓冲区剩余大小。如果你看到读者回复的窗口变小了你就自觉减慢发送速度。如果读者回复窗口大小是0你就彻底停止发送。但你会定期发送一个窗口探测包去问问读者“老铁桌子清理出空位了吗”细节补充16 位最大只能表示 65535 字节。现代网络下这个窗口太小了所以在 TCP 头部的 40 字节选项区里有一个窗口扩大因子 M实际窗口大小等于窗口字段的值左移 M 位。4. 拥塞控制 (Congestion Control)照顾整个社会的快递网络流量控制是照顾对方拥塞控制是照顾整个网络环境。如果你们俩的网卡都很强书桌也很大你一上来就发几万个包裹但此时正值“双十一”中间的路由器早就堵死了。这时候再强行发只会导致大面积丢包。TCP 为此引入了慢启动机制并维护了一个拥塞窗口。实际发送时发送方会取拥塞窗口和对方反馈的接收窗口中的较小值。整个过程就如同热恋的感觉循序渐进又大起大落慢启动试探一开始先试探拥塞窗口设为 1。收到一个 ACK窗口加 1。这导致发送量呈指数级增长1, 2, 4, 8…虽然叫慢启动但前期增长极快。拥塞避免求稳当窗口达到一个设定的慢启动阈值 (ssthresh)时停止指数膨胀改为每次加 1 的线性增长。网络拥塞遇挫如果发生了超时重传说明网络出现严重拥堵掉包了TCP 会立刻进入“贤者模式”。直接把慢启动阈值砍掉一半把拥塞窗口瞬间重置为 1重新开始慢启动。通过不断地探测、增长、遇挫、折半TCP 在尽可能快地发送数据和避免压垮网络之间找到了一条完美的折中曲线。本讲高频面试题解析1. 在滑动窗口机制下如果某个 ACK 报文丢失了发送端是否一定会重传数据解析与答案不一定。TCP 的确认应答具有“累积确认”的特性。如果前面部分的 ACK 丢失但后续数据的 ACK 成功到达发送端发送端通过后续的 ACK 就能确认前面的数据已经被成功接收。只有当真正的数据包丢失或者最后一个包的 ACK 丢失导致超时才会引发重传。2. 简述 TCP 的快重传快速重发控制机制。解析与答案当发送端连续发送多个数据段时如果其中某个数据段丢失接收端在收到后续乱序到达的数据时会持续发送针对丢失数据段的确认应答。如果发送端连续收到3 次相同的确认应答就会判定该序列号对应的数据包已丢失并立即进行重发而无需等待该数据包的超时定时器触发。这种机制大大提高了异常情况下的重传效率。3. 流量控制和拥塞控制有什么区别解析与答案流量控制的作用对象是接收端。它是为了防止发送方发送速度过快导致接收方的缓冲区溢出桌子堆满而引发丢包。通过 TCP 报文头部的窗口大小字段进行端到端的动态反馈。拥塞控制的作用对象是整个网络链路。它是为了防止在网络本身拥堵时通信双方还大量注入数据导致网络瘫痪。发送方通过维护“拥塞窗口”并执行慢启动、拥塞避免等算法主动探测和适应网络的承载能力。第五讲TCP 特性进阶与终极对比延迟应答、捎带应答、粘包问题及场景剖析经过前四讲我们已经掌握了 TCP 如何通过连接管理、序列号、超时重传保证“稳”又如何通过滑动窗口、快重传、流量控制保证“快”。今天我们要看看 TCP 在应用层交互时还有哪些极其聪明的“小动作”以及我们在实际 C 开发中最容易踩坑的“粘包问题”。1. 延迟应答 (Delayed ACK)让子弹飞一会儿上一讲我们提到接收端在回复 ACK 的时候会顺便把自己的“窗口大小剩余缓冲区空间”告诉发送端。窗口越大发送端下次就能发越多的数据。场景模拟假设你的接收缓冲区总共 1M刚收到了 500K 的数据。如果操作系统立刻回复 ACK那它只能告诉对方“我还有 500K 空间”。发送端一看窗口只有 500K那就慢点发吧。但实际上你的 C 服务端程序处理数据极快只要 10 毫秒就能把这 500K 数据从缓冲区里读走消费掉。这时候 TCP 耍了个聪明我不立刻回复我等一等比如等 200 毫秒。在这 200 毫秒内应用程序早就把数据抽干了。此时 TCP 再回复 ACK就可以理直气壮地告诉对方“我还有 1M 的空间”通过“延迟应答”TCP 巧妙地放大了返回的窗口大小从而在保证网络不拥塞的前提下最大限度地提升了传输效率。注当然不能无限等。通常是每隔 N 个包比如 2 个就必须应答一次或者超过最大延迟时间比如 200ms也必须应答。2. 捎带应答 (Piggybacking)顺风车模式在很多 C 网络应用比如我们写的 HTTP Web 服务器或 RPC 调用中客户端和服务端的交互通常是“一发一收”的。客户端对服务端说“How are you?”服务端不仅需要在底层回复一个 ACK确认收到还需要在应用层回复一句“Fine, thank you”。既然服务端反正都要发一个数据包回去TCP 就会把那个底层的 ACK 确认信号直接搭在业务数据的“顺风车”上一起发过去。这就是捎带应答极大减少了网络中纯 ACK 包的数量节省了带宽。3. 面向字节流与“粘包问题” (核心避坑指南)这是 C 后端开发面试的绝对高频考点也是新手极易写出 Bug 的地方。TCP 的传输机制叫面向字节流。回忆一下第一讲UDP 叫“面向数据报”像寄明信片一张是一张。而 TCP 就像自来水管里的水流。必要的系统调用演示函数read / write(或 recv / send)原型ssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count);详细与大白话在 Linux 系统中网络套接字Socket也是一种文件描述符。这两个系统调用就是最基础的文件读写函数向指定的fd写入字节或从fd读取字节。大白话说明write就是往水管子里倒水发数据你可以一次倒 100 毫升也可以分 100 次每次倒 1 毫升read就是拿着桶在管子另一头接水收数据只要管子里有水你想接多少就接多少你根本不知道发送方当时是分几次倒进来的。八戒吃馒头粘包问题应用层发数据就像是在传送带上放馒头。UDP 放馒头每个馒头都装在独立的盒子里有报文边界八戒接收端拿一个盒子就是一个完整的馒头。TCP 放馒头是把面团揉在一起变成一条长长的面筋放在传送带上。TCP 接收缓冲区视角纯字节流 ---------------------------------------- | { | m | s | g | 1 | } | { | m | s | g | ... ---------------------------------------- 毫无边界的一串字符应用程序怎么知道哪里是一个完整的业务请求如果你直接调用read可能一次性读到了“一个半”请求这就叫粘包问题这里的“包”指的是应用层的业务数据包。解决粘包的根本思路明确数据包之间的边界。在 C 开发中我们通常有三种做法定长包约定每个请求固定大小如sizeof(Request)。缓冲区每次严格按这个长度截取。特殊分隔符在包与包之间加上明确的字符。比如 HTTP 协议常用的\r\n。读取时扫描到分隔符就算一个完整包。包头带长度字段最常用定义一个结构体头部指明正文有多长。C 解决粘包的包头约定 Demo// 约定通信协议的数据包结构structNetPackage{uint32_tlength;// 包体的长度// char data[0]; // 柔性数组后面紧跟实际数据};// 读取侧的逻辑思路// 1. 先严格 read 4 个字节解析出 length 的值。// 2. 根据 length 的值再去 read 对应长度的字节这就完美取出了一个完整的应用层数据包。4. TCP 异常情况处理如果连接正在进行中发生了意外怎么办进程终止 / 机器重启你在 Linux 上用 CtrlC 杀掉 C 进程或者直接重启机器。操作系统内核非常负责任它会在进程销毁时自动关闭对应的文件描述符Socket底层依然会向对方发送 FIN 报文经历正常的断开流程。机器掉电 / 网线物理断开断电是一瞬间的事操作系统根本来不及发 FIN。此时接收端还傻傻地以为连接活着。但没关系一旦接收端尝试写入数据就会发现连不通从而引发 Reset复位报文即使不写入数据TCP 内部也内置了保活定时器Keep-Alive会定期探活发现对方失联后会自动释放连接。5. TCP 与 UDP 终极对比TCP 这么好是不是一定要用 TCP绝对不是。TCP 和 UDP 没有绝对的优劣只有适不适合。TCP 的代名词是“可靠”。它拥有复杂的校验和、序列号、确认应答、超时重传、滑动窗口、流量与拥塞控制。适用于文件传输、重要状态更新、Web 网站HTTP/HTTPS、远程登录SSH等绝对不允许丢哪怕一个字节的场景。UDP 的代名词是“快速与实时”。它轻量、无连接、不重传。适用于视频直播流、语音通话、早期的 QQ 聊天以及局域网广播。在这些场景里丢一两帧画面只会闪烁一下但如果为了重传这一帧画面导致整个直播卡顿 2 秒反而是不可接受的。本讲高频面试题解析1. 什么是 TCP 的粘包问题为什么 UDP 没有粘包问题如何解决 TCP 粘包解析与答案粘包问题是指应用层无法从接收缓冲区中剥离出完整的业务数据包。TCP 存在粘包是因为 TCP 是面向字节流的协议头中没有如同 UDP 一样的“报文长度”字段数据在传输和缓冲时失去了应用层的边界。UDP 没有粘包是因为 UDP 是面向数据报的报文头部有 16 位 UDP 长度字段要么收到完整的报文交付给上层要么不收绝不会出现“半个包”的情况。解决方案是在应用层约定协议明确边界常用方法有1. 发送固定长度的数据包2. 在数据包末尾添加特殊分隔符3. 在数据包头部添加长度字段Length-Value 格式。2. 简述 TCP 的延迟应答机制它有什么好处解析与答案延迟应答是指接收端在收到数据后不立刻返回 ACK 确认而是稍微等待一段时间或者等收到一定数量的包后再发送 ACK。好处在于这段延迟时间可以让接收端的应用层有时间处理掉接收缓冲区里的数据从而在回复 ACK 时能在 TCP 头部填入一个更大的“窗口大小”。窗口变大网络的吞吐率和整体传输效率就会随之提升。3. 发现服务器上出现了大量的 CLOSE_WAIT 状态可能是什么原因解析与答案根据四次挥手的状态流转图当服务端被动关闭方收到客户端的 FIN 并回复了 ACK 之后服务端就会进入CLOSE_WAIT状态。此时服务端需要在处理完剩余业务逻辑后主动调用close(fd)发送自己的 FIN 才能推进状态。如果服务器上出现大量CLOSE_WAIT毫无疑问是代码层面出现了 BUG通常是因为业务逻辑漏洞、异常分支没有处理干净导致忘了调用close函数关闭 Socket 文件描述符。