【筑基·055】TCP vs UDP:可靠和快速的取舍
码农修仙传 · 筑基期 · 第55篇
我是玄芯散人,带你从炼气修到大乘。
境界标识
╔══════════════════════════════════╗
║ 筑基期 · 第55篇 ║
║ TCP vs UDP ║
║ 预计阅读:16分钟 ║
╚══════════════════════════════════╝
修仙引入
上一篇讲了HTTP和DNS。你知道了浏览器拿到IP之后要跟服务器建立TCP连接,然后才能收发HTTP报文。但有个问题一直绕过去了:TCP凭什么说自己是"可靠的"?可靠是怎么做到的?
更奇怪的是,UDP明显比TCP简单,不握手不确认不重传,但视频直播和在线游戏和DNS查询全在用它。不可靠的协议凭什么扛起了互联网一半的流量?
这篇就把TCP和UDP放在一起对比,讲清楚TCP的可靠性机制是怎么一层层叠上去的,以及UDP在什么情况下反而比TCP更合适。
硬核主体
TCP三次握手:为什么是三次
TCP(Transmission Control Protocol)在发送数据之前,双方要先建立连接。建立连接的过程叫三次握手(Three-Way Handshake)。
text
客户端 服务器
| |
| --- SYN, seq=x ------------> | 第1次:客户端说"我想连"
| |
| <--- SYN+ACK, seq=y, ack=x+1 | 第2次:服务器说"我收到了,也想连"
| |
| --- ACK, ack=y+1 -----------> | 第3次:客户端说"我确认你的确认"
| |
| 连接建立,可以发数据了 |
三次握手的过程大家背得滚瓜烂熟。但为什么是三次,不是两次或者四次?
两次不够。假设只有两次:客户端发SYN,服务器回SYN+ACK,然后直接开始传数据。问题在于客户端发的第一个SYN可能在网络里堵了很久才到服务器。客户端以为超时了已经放弃,服务器却收到这个"幽灵SYN",回了SYN+ACK,然后傻等数据。服务器资源白白浪费在一个压根不会来的客户端身上。
第三次握手解决了这个问题。客户端收到服务器的SYN+ACK后,再回一个ACK。如果客户端根本不想连了(那个SYN是旧的),它不会回ACK,服务器等不到就释放连接。三次握手说白了就是双方都确认了对方的接收能力。
四次没必要。TCP是全双工的,双方都能发数据。但你仔细看,第二次握手服务器把SYN和ACK合并成一条发了,所以只需要三次,不需要四次。
四次挥手:为什么断开比连接多一次
TCP断开连接叫四次挥手(Four-Way Handshake),比建立连接多一次。
text
客户端 服务器
| |
| --- FIN, seq=u -------------> | 第1次:客户端说"我发完了"
| |
| <--- ACK, ack=u+1 ----------- | 第2次:服务器说"收到了,但我还有数据要发"
| |
| <--- FIN, seq=v ------------- | 第3次:服务器说"我也发完了"
| |
| --- ACK, ack=v+1 -----------> | 第4次:客户端说"收到,再见"
| |
| 等待 2MSL 后关闭 |
为什么断开要四次?因为TCP是全双工的。建立连接时,双方同时准备好,所以SYN和ACK可以合并。断开时,客户端说"我不发了",但服务器可能还有数据要发,所以先回ACK,等自己的数据发完了再发FIN。FIN和ACK不能合并,因为中间隔了"服务器还在发数据"这段时间。
挥手之后客户端要等2MSL(Maximum Segment Lifetime,报文最大生存时间)才真正关闭。这是因为最后一个ACK可能丢,如果丢了服务器会重发FIN,客户端得留时间来响应。不等就关闭,服务器就永远等不到回应了。
TCP的可靠性:四个机制叠在一起
TCP说自己是可靠传输,不是空口白话。可靠性是四个机制叠出来的:
第一,序列号和确认号。TCP发的每个字节都有编号。接收方收到数据后回一个ACK,告诉发送方"我收到了到第N个字节,下一个该发第N+1个了"。发送方就知道哪些数据到了哪些没到。
第二,重传机制。发送方发出数据后启动一个定时器(RTO,Retransmission Timeout)。定时器到了还没收到ACK,就重发。这就是TCP处理丢包的方式。
第三,校验和。每个TCP报文头里有个校验和字段,接收方算一遍如果不匹配就丢弃,当没收到。数据在传输中被篡改了也能发现。
第四,按序到达。TCP给每个字节编了号,接收方根据序列号把乱序到达的报文重新排序再交给上层。UDP不管这个,先到的先交,后到的后交,上层自己处理顺序。
这四个机制加在一起,TCP就能保证数据不丢(靠重传),不错(靠校验和),不乱(靠序列号排序),不重(靠去重)。
滑动窗口:流量控制怎么做
TCP保证可靠了,但还有个问题:发送方发得快,接收方处理得慢,接收方的缓冲区会溢出。
TCP用滑动窗口(Sliding Window)做流量控制。接收方在ACK报文里带一个窗口大小(Window Size),告诉发送方"我还能接收多少字节"。发送方根据这个值控制发送速度。
text
发送窗口(Window Size = 4000)
┌──────────────────────────────────────┐
│ 已确认 │ 已发未确认 │ 可以发 │ 不能发 │
│ 1000字节 │ 2000字节 │ 2000字节 │ ... │
└──────────────────────────────────────┘
↑
窗口右边界
窗口随ACK左移,窗口大小随接收方反馈调整
窗口会滑动。发送方收到ACK后,已确认的部分移出窗口,窗口右移,新的数据可以发了。接收方处理完数据腾出缓冲区后,会把窗口调大告诉发送方。
如果接收方缓冲区满了,会在ACK里把窗口设为0。发送方收到零窗口通知就停止发送,定期发窗口探测报文,等接收方腾出空间。
拥塞控制:不只照顾接收方
流量控制照顾的是接收方。但网络中间的路由器也可能堵。如果所有发送方都不管网络状态猛发,路由器队列溢出,丢包率飙升,所有人的速度都往下掉。
TCP的拥塞控制有四个阶段:
#mermaid-svg-AiOy1qZFdPvUfkWV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AiOy1qZFdPvUfkWV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AiOy1qZFdPvUfkWV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AiOy1qZFdPvUfkWV .error-icon{fill:#552222;}#mermaid-svg-AiOy1qZFdPvUfkWV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AiOy1qZFdPvUfkWV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AiOy1qZFdPvUfkWV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AiOy1qZFdPvUfkWV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AiOy1qZFdPvUfkWV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AiOy1qZFdPvUfkWV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AiOy1qZFdPvUfkWV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AiOy1qZFdPvUfkWV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AiOy1qZFdPvUfkWV .marker.cross{stroke:#333333;}#mermaid-svg-AiOy1qZFdPvUfkWV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AiOy1qZFdPvUfkWV p{margin:0;}#mermaid-svg-AiOy1qZFdPvUfkWV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AiOy1qZFdPvUfkWV .cluster-label text{fill:#333;}#mermaid-svg-AiOy1qZFdPvUfkWV .cluster-label span{color:#333;}#mermaid-svg-AiOy1qZFdPvUfkWV .cluster-label span p{background-color:transparent;}#mermaid-svg-AiOy1qZFdPvUfkWV .label text,#mermaid-svg-AiOy1qZFdPvUfkWV span{fill:#333;color:#333;}#mermaid-svg-AiOy1qZFdPvUfkWV .node rect,#mermaid-svg-AiOy1qZFdPvUfkWV .node circle,#mermaid-svg-AiOy1qZFdPvUfkWV .node ellipse,#mermaid-svg-AiOy1qZFdPvUfkWV .node polygon,#mermaid-svg-AiOy1qZFdPvUfkWV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AiOy1qZFdPvUfkWV .rough-node .label text,#mermaid-svg-AiOy1qZFdPvUfkWV .node .label text,#mermaid-svg-AiOy1qZFdPvUfkWV .image-shape .label,#mermaid-svg-AiOy1qZFdPvUfkWV .icon-shape .label{text-anchor:middle;}#mermaid-svg-AiOy1qZFdPvUfkWV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AiOy1qZFdPvUfkWV .rough-node .label,#mermaid-svg-AiOy1qZFdPvUfkWV .node .label,#mermaid-svg-AiOy1qZFdPvUfkWV .image-shape .label,#mermaid-svg-AiOy1qZFdPvUfkWV .icon-shape .label{text-align:center;}#mermaid-svg-AiOy1qZFdPvUfkWV .node.clickable{cursor:pointer;}#mermaid-svg-AiOy1qZFdPvUfkWV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AiOy1qZFdPvUfkWV .arrowheadPath{fill:#333333;}#mermaid-svg-AiOy1qZFdPvUfkWV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AiOy1qZFdPvUfkWV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AiOy1qZFdPvUfkWV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AiOy1qZFdPvUfkWV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AiOy1qZFdPvUfkWV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AiOy1qZFdPvUfkWV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AiOy1qZFdPvUfkWV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AiOy1qZFdPvUfkWV .cluster text{fill:#333;}#mermaid-svg-AiOy1qZFdPvUfkWV .cluster span{color:#333;}#mermaid-svg-AiOy1qZFdPvUfkWV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AiOy1qZFdPvUfkWV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AiOy1qZFdPvUfkWV rect.text{fill:none;stroke-width:0;}#mermaid-svg-AiOy1qZFdPvUfkWV .icon-shape,#mermaid-svg-AiOy1qZFdPvUfkWV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AiOy1qZFdPvUfkWV .icon-shape p,#mermaid-svg-AiOy1qZFdPvUfkWV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AiOy1qZFdPvUfkWV .icon-shape .label rect,#mermaid-svg-AiOy1qZFdPvUfkWV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AiOy1qZFdPvUfkWV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AiOy1qZFdPvUfkWV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AiOy1qZFdPvUfkWV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 快恢复
快重传
拥塞避免
慢启动
是
3个重复ACK
cwnd=1 MSS
每收到一个ACK
cwnd翻倍
cwnd达到ssthresh
进入拥塞避免
cwnd每RTT加1 MSS
检测到丢包?
ssthresh = cwnd/2
cwnd = 1 MSS
回到慢启动
收到3个重复ACK
立即重传丢失报文
不等RTO超时
ssthresh = cwnd/2
cwnd = ssthresh
继续拥塞避免
不回到1
慢启动(Slow Start):连接刚建立时,TCP不知道网络能承受多快的速度,所以从1个MSS(Maximum Segment Size,最大报文段长度)开始。每收到一个ACK,拥塞窗口(cwnd)翻倍。1变2,2变4,4变8。增长很快但不是无限增长,到慢启动阈值(ssthresh)后进入拥塞避免阶段。
拥塞避免(Congestion Avoidance):cwnd从指数增长改为线性增长,每个RTT(Round-Trip Time,往返时间)加1个MSS。增长慢了,但还在探测网络的极限。
快重传(Fast Retransmit):等RTO超时再重传太慢。如果发送方连续收到3个重复ACK,说明那个报文大概率丢了,不等超时直接重传。
快恢复(Fast Recovery):快重传后不回到慢启动的cwnd=1,而是把cwnd设为当前值的一半,ssthresh也设为一半,然后继续拥塞避免。这是AIMD(Additive Increase, Multiplicative Decrease)策略:加性增长,乘性减少。增长慢慢加,一旦发现拥塞立刻砍一半。
UDP:什么都不要,只要快
UDP(User Datagram Protocol)的设计哲学完全相反。TCP花了一堆机制保证可靠,UDP把这些全扔了。
UDP报文头只有8个字节。TCP头部至少20字节。UDP不建连接,不握手,不确认,不重传,不排序,不流控。发就完了,到不到不管。
text
UDP头部(8字节)
┌──────────┬──────────┐
│ 源端口 │ 目的端口 │
├──────────┼──────────┤
│ 长度 │ 校验和 │
└──────────┴──────────┘
TCP头部(20字节起)
┌──────────┬──────────┐
│ 源端口 │ 目的端口 │
├──────────┼──────────┤
│ 序列号 │
├──────────────────────┤
│ 确认号 │
├──────────┬──────────┤
│数据偏移| │ 标志位 │
├──────────┼──────────┤
│ 窗口大小 │ 校验和 │
├──────────┼──────────┤
│ 紧急指针 │
└──────────────────────┘
UDP这么"简陋",为什么还有一堆场景用它?
DNS查询用UDP。一个域名解析请求就一个报文,一来一回。用TCP得三次握手再发数据再四次挥手,为了一个512字节(UDP标准限制)的查询建个连接,太浪费了。
视频直播用UDP。直播画面丢几帧没人注意,但延迟300毫秒观众要骂人。TCP丢了重传,重传到了已经过时了,不如丢了就丢了。UDP发出去不管,接收方收到什么播什么,延迟最低。
在线游戏用UDP。你打游戏时角色位置每秒更新几十次,丢一两个位置包不影响游戏体验,但延迟200毫秒你可能就被人打了。TCP的重传机制在这里帮倒忙。
DHCP用UDP。设备刚开机没有IP地址,需要通过DHCP获取。这时候连IP都没有,TCP的三次握手无从谈起,只能用不需要建立连接的UDP。
TCP vs UDP:不是谁替代谁
很多人觉得TCP比UDP"高级",这是个误区。它们解决不同的问题:
text
TCP UDP
连接 面向连接 无连接
可靠性 保证到达 不保证
顺序 按序到达 不保证顺序
速度 慢(有握手和确认) 快(直接发)
头部 20字节起 8字节
流量控制 有(滑动窗口) 无
拥塞控制 有(AIMD) 无
适用 网页/邮件/文件传输 视频/游戏/DNS/IoT
TCP适合需要可靠传输的场合。你下载一个文件,少一个字节都打不开,必须用TCP。你发一封邮件,内容不能丢不能错,必须用TCP。你打开一个网页,HTML少一行标签整个页面布局就乱了,也得用TCP。
UDP适合实时性要求高且容忍丢包的场合。视频通话偶尔花屏可以接受,延迟太大没法交流。游戏位置同步丢一两帧无所谓,卡顿半秒操作全废。DNS查一个域名,TCP握手的时间够UDP查三遍了。
QUIC:UDP的逆袭
上一篇讲HTTP/3时提过一句:HTTP/3底层换了QUIC协议。这里展开说为什么。
HTTP/3底层用的不是TCP,是QUIC(Quick UDP Internet Connections)协议。QUIC跑在UDP上面。
这个选择看起来很反直觉。HTTP/1.1和HTTP/2都跑在TCP上,为什么HTTP/3要换成UDP?
TCP有个固有的问题:队头阻塞(Head-of-Line Blocking)。TCP保证按序到达,如果一个报文丢了,后面到达的报文只能等在缓冲区里,不能交给应用。HTTP/2在一条TCP连接上多路复用多个流,一个流的数据丢了,所有流都被阻塞。
QUIC在UDP上自己实现了可靠传输和拥塞控制,但每个流独立。一个流丢了包,其他流不受牵连继续跑。
另一个好处是连接迁移。TCP用四元组标识连接,包括源IP地址和源端口和目的IP地址和目的端口。你手机WiFi换成4G,IP变了,TCP连接就断了,要重新握手。QUIC用连接ID标识连接,IP变了连接不断,无缝切换。
QUIC还把三次握手和TLS握手合并了,建连速度比TCP+TLS快不少。Google在Chrome里用了多年QUIC,目前互联网上超过四分之一的流量跑在QUIC上。
修仙术语对照表
| 修仙术语 | 技术现实 | 本篇位置 |
|---|---|---|
| 传音符建链 | 三次握手 | TCP三次握手 |
| 幽灵传信 | 旧SYN延迟到达 | 为什么三次不是两次 |
| 断链四步 | 四次挥手 | 四次挥手 |
| 等待散灵 | 2MSL等待 | TIME_WAIT |
| 传信编号 | 序列号 | 可靠性机制 |
| 回执 | 确认号ACK | 可靠性机制 |
| 丢失重发 | 重传机制 | 可靠性机制 |
| 灵力缓冲窗 | 滑动窗口 | 流量控制 |
| 零窗静默 | 零窗口通知 | 流量控制 |
| 探路慢行 | 慢启动 | 拥塞控制 |
| 稳步推进 | 拥塞避免 | 拥塞控制 |
| 拥堵即半 | AIMD乘性减少 | 拥塞控制 |
| 无链直传 | UDP无连接 | UDP设计 |
| 多流不堵 | QUIC独立流 | QUIC协议 |
| 链不随形迁 | 连接迁移 | QUIC协议 |
突破条件
- 能画出三次握手的时序图,解释为什么两次不够
- 能画出四次挥手的时序图,解释为什么比握手多一次
- 说出TCP可靠性的四个机制(序列号和重传和校验和和按序到达)
- 用自己的话解释滑动窗口怎么防止接收方被压垮
- 说出慢启动和拥塞避免的区别(指数增长vs线性增长)
- 能列出至少三个UDP的典型用途,每个解释为什么不用TCP
- 说清楚QUIC为什么选择UDP而不是TCP
筑基网络五篇到这里讲完了。你掌握了TCP/IP四层体系和DNS查询流程和HTTP报文格式和TCP可靠性机制和UDP的适用情况。下一篇进入数据库的世界。
下期预告 + 互动
下一篇:数据库是藏经阁:SQL增删改查到B+树索引。
TCP保证你的数据可靠到达,但数据到了之后怎么存?怎么查?怎么保证一万个人同时写不冲突?数据库的索引为什么能让查询快几百倍?B+树到底长什么样?下一篇讲数据库的基本概念。
留两个问题:
- 你看直播时,画面偶尔卡顿但声音正常,可能是TCP重传还是UDP丢包造成的?
- DNS查询用UDP,但DNS响应超过512字节时会自动切换到TCP,你知道为什么吗?
我是玄芯散人,带你从炼气修到大乘。
本文是「码农修仙传」系列第055篇。系列导航见 xren.ren