ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TCP与UDP核心机制详解:从三次握手到应用选型与排障实战

TCP与UDP核心机制详解:从三次握手到应用选型与排障实战 1. 从一道面试题谈起TCP 和 UDP 到底差了些什么做过网络开发或者准备过大厂面试的朋友基本都躲不开这个问题——TCP 和 UDP 的区别是什么应用场景有哪些网上一搜答案一大把但多数都是教科书式的罗列一个可靠一个不可靠一个面向连接一个无连接背完就忘真到用的时候还是不知道选型。其实这个问题之所以经典恰恰是因为它背后牵出了整个 TCP/IP 协议栈的设计哲学。你只有真正搞懂了 TCP 为了可靠付出了什么代价UDP 为了快放弃了哪些东西才能在实际工程里做对选择。我先说个真实经历。早几年我在做一款硬件设备的数据采集系统设备端通过 4G 网络往服务器回传实时状态。一开始团队拍脑袋全用了 TCP结果遇到弱网环境就出问题TCP 的拥塞控制导致发送窗口不断收缩数据越传越慢最严重的时候延迟飙到十几秒。后来我们做了改造把实时性要求高的状态帧切到 UDP把关键配置下发保留 TCP问题立刻缓解了大半。所以TCP 和 UDP 从来不是谁取代谁的关系而是各管一摊。这篇文章不准备重复太多基础定义我想换个思路从项目实战的视角拆开讲TCP 的连接机制到底在做什么、可靠传输的代价有多大、UDP 为什么在音视频和物联网领域成了主力以及选型时最容易被忽略的几个坑。如果你正准备做网络编程、面试复习或者协议栈排查这篇文章应该能帮你省不少时间。2. 核心机制拆解TCP 的可靠是用什么堆出来的2.1 三次握手建立连接四次挥手断开连接TCP 是面向连接的协议通信双方在收发数据之前必须建立一条逻辑通路。这个过程就是我们常说的三次握手。三次握手的本质是客户端和服务端各自确认你发我收和我发你收两条通道都通畅。具体流程是客户端先发一个 SYN 包带上一个随机初始序列号服务端收到后回 SYNACK确认客户端的序列号同时带上自己的序列号客户端再回一个 ACK 确认服务端的序列号。三次之后双方就都知道对方已经准备好收数据了。为什么要三次而不是两次因为网络存在延迟和乱序两次握手可能会出现已失效的连接请求突然又到达服务端的经典问题。比如客户端发了一个 SYN因为网络拥塞超时了客户端重发 SYN结果第一次的旧 SYN 在网络里绕了一圈之后才到达服务端。如果只握手两次服务端以为要建立新连接白白分配资源。三次握手就能让服务端收到客户端对 SYNACK 的最终确认避免这种历史连接的资源浪费。断开连接则需要四次挥手因为 TCP 是全双工的每个方向的数据通道都要单独关闭。A 对 B 说我发完了FINB 回一个 ACK 确认但此时 B 可能还有数据没发完所以 B 的数据通道还能继续用。等 B 也发完了B 再发 FINA 回 ACK这才算整体关闭。这个机制意味着一件事TCP 的连接状态是有开销的频繁建立/断开连接对性能和资源都不友好。这也是为什么很多应用层会引入长连接和连接池。2.2 可靠性靠的是序列号、确认应答和重传建好连接之后TCP 要保证数据传输不丢、不乱、不重。它靠的是三个基础机制每个字节都有序列号接收方收到数据后回 ACK 确认发送方在超时时间内没收到 ACK 就重传。这里有个容易被人忽略的细节TCP 的确认是累计确认。也就是说接收方回一个 ACKn表示序号小于 n 的所有字节都已经收到。这个设计在正常场景下很高效但如果中间丢了一个包发送方可能连续收到多个重复 ACK触发快速重传。快速重传和超时重传是两套互补的机制快速重传不用等超时收到 3 个重复 ACK 就立刻补发这对局域网这类低丢包环境很管用。但可靠性的代价是等待和重传带来的延迟。如果网络丢包率到了 2% 以上TCP 的吞吐量会断崖式下跌。投一个丢了要重传重传的包又可能把后面的数据全堵住队头阻塞。这个特性注定了 TCP 不适合对实时性极端敏感的业务。2.3 流量控制和拥塞控制为了保护网络和接收方TCP 还内置了两套刹车系统。第一套是流量控制Flow Control保护的是接收方。接收方会在 ACK 里带上自己的接收窗口大小rwnd告诉发送方你最多还能发多少字节别把我缓存撑爆了。这个机制是端到端的。第二套是拥塞控制Congestion Control保护的是整个网络。发送方维护一个拥塞窗口cwnd根据网络状况动态调整。经典算法包括慢启动、拥塞避免、快重传和快恢复。慢启动阶段cwnd 从很小开始指数增长达到阈值后转为线性增长一旦发生丢包就把窗口砍半甚至归零。很多人只记得TCP 是现代互联网的基石却忽略了这套控制机制在弱网环境下的副作用。我实测过在卫星链路上用 TCP 传文件因为往返延迟太高握手就要几百毫秒慢启动阶段吞吐量迟迟上不去。这就是为什么 QUIC基于 UDP 实现能在高延迟网络下把建连和传输性能拉高一个量级。2.4 头部开销不小一个 ACK 可能就是几十字节TCP 的最小头部是 20 字节不含选项字段UDP 的固定头部只有 8 字节。看起来差别不大但如果你的业务是小包高频传输这个差异会被放大。比如每 10 字节的数据就要带 20 字节的头部TCP 还要有握手、确认、状态维护的开销整体流量成本高出不少。另外TCP 还涉及 Nagle 算法和延迟 ACK 的交互问题。Nagle 算法会把多个小数据包合并发送减少网络包数量但和延迟 ACK 一起用可能造成 40ms 级别的人为延迟。很多人在做低延迟应用的时候都踩过这个坑需要在 socket 上显式关闭 Nagle 算法设置 TCP_NODELAY。3. UDP 为什么能轻装上阵3.1 无连接想发就发不需要对暗号UDP 是无连接的没有三次握手也没有四次挥手更没有连接状态表要维护。发送方把数据包封装好直接丢进网络接收方有没有在线、有没有准备好发送方一概不管。这个特性带来一个很大的好处没有建连延迟也没有连接数的限制。TCP 的一个并发连接需要维护发送缓冲、接收缓冲、拥塞窗口、序列号等一大堆状态一个进程能同时维持的连接数是有上限的。而 UDP 不需要维护这些状态理论上你想往多少个对端发包都行这也是 UDP 在广播和组播场景下无法被替代的原因。3.2 基于数据报每个包都是独立的个体TCP 是字节流协议你调用 send 发 100 字节再调用 send 发 200 字节接收方读到的可能是一个拼接好的 300 字节流你根本不知道边界在哪里所以 TCP 应用层一般要自己设计消息边界比如加长度前缀、分隔符。UDP 是数据报协议一次 sendto 发出去的内容就是一个完整的报文一次 recvfrom 读到的就是完整的内容边界天然存在。这个特性让 UDP 在简单请求-响应模型里特别好用比如 DNS 查询、NTP 时间同步、SNMP 管理。写代码的时候不用解析流边界逻辑少了一大截。但要注意** UDP 不保证按序到达也不保证不丢**。如果网络出现乱序接收方收到包的顺序可能和发送顺序不同如果中间链路拥塞某些包可能直接被丢弃。对于需要完整数据的场景比如文件传输UDP 裸用肯定不行必须在应用层自己做排序和重传。QUIC、KCP 这类协议本质上就是在 UDP 之上重新实现了可靠传输用更多的应用层控制换取更低的延迟。3.3 头部只有 8 字节开销极低UDP 头固定 8 字节源端口 2 字节、目的端口 2 字节、长度 2 字节、校验和 2 字节。校验和还是可选的IPv4 下可以不填。这个轻量设计决定了它在高频率小数据包场景下比 TCP 经济得多。我自己做过一个游戏服务器 prototype玩家的位置和状态同步走 UDP每个包大概 50 字节UDP 头只占 16%。如果走 TCP加上头部、ACK、以及 Nagle 算法可能带来的合并等待效果反而更差。所以对很多实时交互应用来说UDP 不是不堪大用而是少即是多。3.4 没有拥塞控制快是快但会把网络打爆UDP 没有拥塞控制发送方可以以任意速率往网络里灌数据。这在局域网和专线场景下是好事因为你不用被 TCP 的窗口收缩拖后腿但在公网上无节制的 UDP 流量会造成网络拥塞挤占其他用户的带宽。所以应用层的 UDP 协议往往需要自己设计码率控制、丢包反馈、抖动平滑等机制。RTP/RTCP 就是典型RTP 负责传音视频数据RTCP 负责周期性反馈接收质量让发送端动态调整发送速率。很多人觉得UDP 代码简单其实要做得稳应用层要补的功课一点都不少。4. 一张大表看懂 TCP 和 UDP 的区别很多人记不住两者的区别是因为所有维度混在一起。我把实际开发和面试中最常涉及的维度整理成一张表方便对照记忆对比维度TCPUDP连接状态面向连接需要建立/断开连接无连接不需要握手可靠性可靠传输不丢不重不乱序尽力而为不保证可靠数据边界字节流无边界应用层自行切分数据报消息边界天然保留传输速度慢握手、确认、重传、拥塞控制快无确认无重传头部开销20 字节起可含更多选项固定 8 字节传输模式单播单播、广播、组播流量控制有基于滑动窗口无拥塞控制有慢启动等算法无应用层自理队头阻塞有序列号保证有序导致无包与包完全独立典型场景网页、文件传输、邮件、数据库音视频、游戏、DNS、物联网上报这张表里最容易被忽略的是队头阻塞这一行。TCP 为了严格按序交付一旦某个包丢了后面已经到达的数据包也只能在缓冲区里排队等重传导致应用层读不到连续数据。在丢包率高的链路上TCP 的有效吞吐会远低于链路容量。UDP 没有这种问题每个数据报独立交付应用层拿到哪些是哪些配合抗丢包算法比如前向纠错 FEC往往能撑住弱网下的音视频通话。另外一个容易搞混的点是TCP 是基于字节流的UDP 是基于数据报的。这两个概念的差异直接影响应用层编码。我见过不少新手用 UDP 连续发多个包接收端一次 recvfrom 居然把几个包读成了一条实际上是理解错了 sendto/recvfrom 的语义把 UDP 当流用了。5. 应用场景选型什么时候上 TCP什么时候用 UDP5.1 必须用 TCP 的场景数据完整性高于一切如果你的业务对数据完整性要求极高丢一个字节都可能导致严重问题那不用多想TCP 是基础选择。典型是文件传输FTP、HTTP、对象存储上传下载、电子邮件SMTP/IMAP、数据库事务日志同步、以及所有基于 HTTP 的 Web 应用。这些场景的共性在于数据是错了没法补救的。一个配置文件传过去少了一位整个程序可能起不来一条订单记录在传输中损坏账目对不上。TCP 的确认和重传机制虽然慢但保证了最终一致性。工业领域里的大多数设备通信也是走 TCP 的比如 Modbus TCP 协议它就是在 TCP 之上封装了 Modbus 报文底层靠 TCP 保证指令不丢失不篡改。PLC 和上位机之间的配置下发、状态采集数据量不大但对准确性极为敏感TCP 完全够用。除此之外长连接业务也推荐 TCP。比如 IM 软件的在线推送、金融行情订阅客户端和服务端建立一条 TCP 长连接服务端可以随时主动推送数据。TCP 的全双工特性让双向通信变得很自然不需要像 UDP 那样自己设计心跳保活和重连逻辑。5.2 优先考虑 UDP 的场景实时性优先丢一点没关系先看音视频通话。视频会议、直播、VoIP 这类业务用户能接受偶尔的画面花屏或声音卡顿但不能接受长时间转圈等待。如果走 TCP一旦出现丢包导致重传整个视频帧会阻塞在那里等重传完成延迟进一步拉大体验直线下降。UDP 配合前向纠错FEC和丢包隐藏PLC算法反而能在弱网下保持基本流畅。再看竞技类游戏。FPS、MOBA 类游戏对操作响应要求极高玩家的位置、操作指令需要毫秒级送达。这类业务宁可传到了但稍微不准也不能等了很久终于准确。所以几乎都选择 UDP 作为主传输通道关键逻辑如服务器权威校验在应用层做兜底。然后是物联网设备上报。大量传感器、智能硬件、嵌入式设备会周期性地向服务器上报状态数据电量、温度、GPS 位置一次上报丢了也无所谓下一帧马上就到。UDP 没有连接状态维护的开销设备重启后无需重新建连直接发包就行服务器的资源占用也低得多。现在很多物联网平台都支持 UDP 接入就是这个原因。还有 DNS 查询。全球每天有海量 DNS 请求每个请求就一个问答用 TCP 的建连成本完全不可接受。DNS 默认走 UDP 53 端口只有响应太大超过 512 字节或者需要区域传输的时候才切换到 TCP。最后别忘了广播和组播场景。UDP 是唯一能实现一对多通信的传输层协议比如局域网内的服务发现SSDP/mDNS、通过网络同步多台设备的时钟PTP、或者一套视频源分发到网络内多个屏幕。这些需求用 TCP 根本做不了因为连接是点对点的。5.3 灰色地带UDP 之上的可靠协议有些场景既想要 TCP 的可靠性又想要 UDP 的低延迟这就催生了各种在 UDP 之上实现可靠传输的协议。最典型的是 Google 推出的 QUIC底层跑在 UDP 上但重新实现了类似 TCP 的连接管理、可靠传输、流控和加密。QUIC 的建连只要 1-RTT甚至可以做到 0-RTT还能减少队头阻塞独立流的包头阻塞不会影响其他流HTTP/3 就是基于 QUIC 的。如果你的服务面向弱网和高延迟场景用 QUIC 替代 TCP 常常能带来立竿见影的效果。国内开发圈还常用 KCP一个基于 UDP 的可靠传输库。它在相同丢包率下比 TCP 传输更快尤其是在高丢包、高延迟的链路上。很多手游尤其是棋牌类游戏都拿 KCP 做底层传输就是看中它比 TCP 更适合复杂网络。但这里要泼一盆冷水在 UDP 之上自己做可靠传输工程量不小。你要处理乱序、重传、去重、流量控制、拥塞控制、连接超时等一堆问题。如果不是有明确的性能瓶颈或者特殊需求建议不要轻易自己造轮子优先用成熟的方案。5.4 混合架构一个系统里 TCP 和 UDP 共存实际工程中很多系统并不是非此即彼而是按数据特性做混合传输。举个例子我之前做的一个直播平台推流客户端信令开播、停播、切换清晰度走 TCP 长连接确保指令不乱不丢媒体流音视频数据走 UDP 的 RTP 协议保证实时性。TCP 负责控制UDP 负责数据各司其职。再做游戏时也一样玩家登录、道具购买等涉及账户安全的请求走 TCP/HTTPS战斗中玩家的移动、技能释放走 UDP降低延迟。系统设计阶段就明确每类数据的传输需求远比后期出了问题再换协议要省事。6. 实战经验网络测试、抓包排查和踩坑记6.1 用 iperf3 做 UDP 打流测试该看哪一端的指标很多人做网络带宽测试或用 iperf3 打流时分不清应该看 sender 还是 receiver 的数据。iperf3 有个特点TCP 测试时带宽、重传等性能指标主要在接收端输出而 UDP 测试时两个方向都会打印各自的发送速率和接收速率同时还会统计丢包。UDP 模式下最常见的误解是看着 sender 端显示的带宽很高就以为链路很好。实际上 sender 端只是表明我发出去多少至于对端真正收到了多少、丢了多少必须看 receiver 端的统计。我自己就碰到过一次在无人机图传调优时iperf3 发送端显示 80Mbps但接收端显示丢包率高达 30%链路实际上根本承载不了这个码率。所以用 iperf3 做 UDP 打流请记住以接收端为准重点关注丢包率lost/Total和抖动jitter两个指标。命令参考# 服务端监听 iperf3 -s -p 5201 # 客户端打流UDP 模式目标带宽 50Mbps iperf3 -c server-ip -u -b 50M -t 30UDP 测试还有一个常见误区-b参数只是限速不是强制带宽。如果链路本身不够iperf3 会尽量发出这个速率但丢包率会飙升如果链路够好实际吞吐就接近目标带宽。做压力测试的时候可以从小带宽开始逐步加大观察接收端的丢包率拐点。6.2 Wireshark 抓包加了 UDP 过滤条件为什么还抓到非 UDP 包这个问题在热词里出现过很多次在 Wireshark 里设置了udp过滤条件但结果里还是混进了 ICMP 数据包。很多人以为自己过滤写错了其实不是。Wireshark 的过滤条件分为两种捕获过滤器Capture Filter和显示过滤器Display Filter。如果用的是捕获过滤器udp写在上方小工具栏里它在抓包时直接丢弃非 UDP 流量但很多人实际是在显示过滤器里写了tcp.port 80这类表达式显示过滤器只是在结果里过滤显示不阻止底层捕获。而 ICMP 包在传输过程中可能与 UDP 协商相关比如端口不可达的 ICMP 回显就是 UDP 把一个包发给一个没人监听的端口路由器或目标主机返回一个 ICMP Port Unreachable。这个 ICMP 包本身不是 UDP 包但如果设置了显示条件udp or icmp自然就会看到混合结果。排查的时候建议用流量图来观察 ICMP 包和 UDP 包的时序关系在 Wireshark 菜单栏选择 统计 - 流量图可以形象地看到 UDP sendto 之后ICMP Port Unreachable 回传的先后顺序。大多数情况下你看到的过滤条件没生效不是 Wireshark 的 bug而是没理解捕获过滤器和显示过滤器的区别。如果只想抓 UDP工具栏里请写成udp or (icmp and icmp.type 3)第二个条件是为了捕获 UDP 发送时可能产生的端口不可达 ICMP 错误方便做链路诊断。6.3 端口绑定冲突only one usage of each socket address热词里出现的这条报错bind: only one usage of each socket address是新手在本地起服务时遇到的高频问题特别是在反复调试、程序崩溃又重启的场景下。意思是你要绑定的 IP:端口已经被另一个 socket 占用了。最常见的诱因有三个同一个进程起了多个实例端口被第一个实例占住。之前运行的程序没退出干净处于 TIME_WAIT 或 ESTABLISHED 状态。有别的程序恰好用了同名端口比如 11434 在很多机器上被本地 AI 服务占用。排查分两步走先查谁占用了端口# 查看指定端口的占用情况 lsof -i :11434 # 或者用 netstat netstat -tunlp | grep 11434 # 在 Windows 上 netstat -ano | findstr 11434如果你确定旧的连接已经没有业务意义可以用SO_REUSEADDR这个 socket 选项解决 TIME_WAIT 状态下的端口复用问题int opt 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));需要说明的是SO_REUSEADDR并不能让你绑定一个正在被LISTEN的端口它解决的是 TIME_WAIT 状态下的复用问题。如果端口被一个活跃的进程占用你要么杀掉那个进程要么换端口。6.4 curl 报 tcp connection reset by peer别急着怀疑功能逻辑这条报错在排查网络问题时也很常见——curl: (35) tcp connection reset by peer。字面意思是连接建立过程中被对方重置了。原因大概率在三个方向一个是服务端进程根本没起来或者起了但崩溃了内核直接回 RST 包第二个是防火墙或者安全组把连接给重置了常见于云服务器安全组配置错误第三个是应用层协议不匹配比如服务端只接受 TLS结果你用 HTTP 明文去连。排查的顺序我建议是先确认端口有没有监听telnet ip port通不通。再用ss -tnp | grep port看连接状态如果是 SYN_SENT说明包发出去了没响应如果是 ESTABLISHED 后立刻断说明可能存在超时或者对端主动关闭。用 Wireshark/tshark 抓包看 TCP 三次握手的包是否有 RST。我自己的习惯是服务端先开一个nc -l port测试一下排除应用本身的问题再往上排查网络设备。6.5 TCP connect 超时很多项目一半的工期都压在弱网适配TCP connect 超时的现象是代码卡在connect()函数很长时间最后报Operation timed out或者Connection timed out。最直接的原因是对端 IP 根本不可达或者有防火墙把 SYN 包静默丢弃DROP 而不是 REJECT。这种情况下客户端会反复重传 SYN直到超过系统超时时间Linux 默认大概 127 秒左右才放弃。如果不想让用户等那么久有几种常见的处理方式在应用层设置 connect 超时。非阻塞模式下用select/poll/epoll控制等待时间超时后手动关闭 socket。把系统参数调小比如net.ipv4.tcp_syn_retries默认为 6可以通过 sysctl 调整减少超时等待。对关键业务做多 IP 冗余一个 IP 超时立刻切换到另一个。很多生产事故就是 connect 超时时间太长导致的假死现象。我见过某个网关程序在对方服务下线后每次连接都要卡 2 分钟才报错业务监控大面积报警。后来在代码里统一加了 5 秒的 connect 超时控制体验立刻正常了。6.6 实测心得UDP 程序比 TCP 程序更容易假成功做 UDP 开发最大的陷阱是发送方sendto()返回成功并不代表数据真的到了对端。因为 UDP 包发出后链路发生了什么发送方根本不知道。如果你不自己实现应用层确认机制出了问题是没法感知的。这也引申出一个排查建议UDP 联调时一定要先抓包确认收发包的实际情况不要只看 sendto 的返回值。组播调试时也要注意网卡的组播地址绑定以及防火墙是否放行了目标端口。我踩过的坑是组播数据在同一个交换机上收不到结果发现是没加 IGMP 成员关系导致的换成设置网卡加入组播组就好了。还有一点如果在局域网内调试 UDP 广播两端要尽量在同一个网段跨网段广播多数路由器默认不转发。如果需要跨网段发广播最好的办法是改成组播或者让服务器端组播代理程序帮你中转。7. 关于选型和排障我最后想说的几句话协议选型这件事说白了是需求驱动。你先问自己三个问题能不能容忍丢包延迟敏感吗数据完整性是不是零容忍这三个问题一答完方向基本就八九不离十了。再往深了问一句对端是不是防火墙后面的公网主机如果是UDP 有时会被运营商 QoS 限速这种场景可能还是 TCP 更靠谱。如果你觉得自己可能会在 UDP 上做可靠传输先去了解一下 KCP 的源码设计里面的序号、ACK、滑动窗口、重传定时器拆得非常漂亮读完会对整个网络协议栈有更深的理解。折腾网络这块纸上谈兵不如撸起袖子抓包实测把模拟器上的时间省下来做几次真实的链路测试比什么教程都管用。
返回列表