跳到主要内容

TCP 与 UDP

问题

TCP 和 UDP 有什么区别?TCP 的三次握手和四次挥手是怎样的过程?

答案

TCP(传输控制协议)和 UDP(用户数据报协议)是传输层的两种核心协议。

对比概览

特性TCPUDP
连接面向连接无连接
可靠性可靠传输不可靠传输
顺序保证顺序不保证顺序
流量控制
拥塞控制
头部大小20-60 字节8 字节
传输效率较低较高
适用场景文件传输、网页视频、游戏、DNS

TCP 三次握手

建立 TCP 连接需要三次握手(Three-way Handshake)。

握手过程

详细解释

步骤发送方内容说明
第一次客户端SYN=1, seq=x请求建立连接
第二次服务器SYN=1, ACK=1, seq=y, ack=x+1确认并请求连接
第三次客户端ACK=1, seq=x+1, ack=y+1确认连接

为什么是三次?

// 两次握手的问题:
// 1. 客户端发送 SYN(可能延迟)
// 2. 服务器收到 SYN,发送 SYN+ACK,建立连接
// 3. 客户端收到,建立连接
// 问题:延迟的 SYN 到达服务器,服务器会再次建立连接(资源浪费)

// 三次握手解决:
// 延迟的 SYN 到达 → 服务器 SYN+ACK → 客户端忽略(因为已关闭)
// 服务器收不到第三次 ACK → 不会建立连接

核心目的

  1. 确认双方的发送能力接收能力
  2. 同步初始序列号(ISN)
  3. 防止历史连接的初始化

TCP 四次挥手

关闭 TCP 连接需要四次挥手(Four-way Handshake)。

挥手过程

详细解释

步骤发送方内容说明
第一次客户端FIN=1请求关闭连接
第二次服务器ACK=1确认收到(可能还有数据)
第三次服务器FIN=1数据发完,请求关闭
第四次客户端ACK=1确认关闭

为什么是四次?

// TCP 是全双工通信,双方都可以发送数据
// 关闭连接需要双方分别关闭自己的发送通道

// 第一次:客户端 → 我不发了
// 第二次:服务器 → 知道了(但我可能还要发)
// 第三次:服务器 → 我也不发了
// 第四次:客户端 → 知道了,再见

TIME_WAIT 状态

// 客户端在发送最后一个 ACK 后,进入 TIME_WAIT 状态
// 等待 2MSL(Maximum Segment Lifetime,最大报文生存时间)

// 原因:
// 1. 确保最后一个 ACK 到达服务器
// 如果丢失,服务器会重发 FIN,客户端可以重发 ACK
// 2. 确保本次连接的所有报文都从网络中消失
// 防止延迟报文影响新连接

TCP 可靠传输机制

1. 序列号与确认号

// 每个字节都有序列号
// 接收方通过 ACK 确认收到的数据

// 发送: seq=1000, len=100 (字节 1000-1099)
// 确认: ack=1100 (期望下一个字节)

2. 超时重传

// 发送数据后启动定时器
// 超时未收到 ACK → 重传

// RTT (Round-Trip Time): 往返时间
// RTO (Retransmission Timeout): 重传超时时间
// RTO 通常根据 RTT 动态计算

3. 滑动窗口

// 窗口大小决定一次可以发送多少数据
// 不需要等待每个包的 ACK,提高吞吐量

// 窗口滑动:收到 ACK 后,窗口向右滑动

4. 流量控制

// 接收方告知发送方自己的接收能力
// 通过 TCP 头部的 Window 字段

// 接收方缓冲区满 → Window = 0 → 发送方停止发送
// 接收方处理数据 → Window > 0 → 发送方继续发送

5. 拥塞控制

算法说明
慢启动指数增长拥塞窗口
拥塞避免线性增长拥塞窗口
快重传收到 3 个重复 ACK 立即重传
快恢复拥塞窗口减半后线性增长

UDP 特点

报文结构

 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
| 源端口号 | 目的端口号 |
+--------+--------+--------+--------+
| 长度 | 校验和 |
+--------+--------+--------+--------+
| 数据 |
+--------+--------+--------+--------+

特点

// 1. 无连接
// 发送前不需要建立连接,直接发送

// 2. 不可靠
// 不保证送达,不保证顺序

// 3. 面向报文
// 应用层交下来的报文,UDP 原样发送
// 不拆分也不合并

// 4. 支持多播和广播
// 一对多通信

适用场景

场景原因
DNS 查询请求小,不需要连接开销
视频直播实时性要求高,允许丢帧
在线游戏低延迟,丢包可容忍
VoIP 通话实时性优先
QUIC在 UDP 上实现可靠传输

常见面试问题

Q1: TCP 和 UDP 的区别?

答案

维度TCPUDP
连接面向连接无连接
可靠性可靠(重传、确认)不可靠
顺序保证有序不保证
速度较慢较快
头部20-60 字节8 字节
流控/拥塞
传输方式字节流数据报

Q2: 为什么 TCP 握手是三次,挥手是四次?

答案

三次握手

  • 目的是确认双方的发送和接收能力
  • 两次不够:无法防止历史连接
  • 四次浪费:三次已经够用

四次挥手

  • TCP 是全双工,需要双方分别关闭
  • 服务器收到 FIN 后可能还有数据要发送
  • 所以 ACK 和 FIN 分开发送
// 握手时:SYN + ACK 可以合并
// 挥手时:ACK 和 FIN 不能合并(中间可能还有数据)

Q3: TIME_WAIT 状态的作用?

答案

TIME_WAIT 持续 2MSL(约 4 分钟)的原因:

  1. 确保最后的 ACK 到达

    • 如果丢失,服务器会重发 FIN
    • 客户端可以重发 ACK
  2. 确保旧连接的报文消失

    • 防止延迟报文被新连接接收
    • 新连接可能使用相同的端口号

Q4: TCP 如何保证可靠传输?

答案

  1. 序列号 + 确认号:追踪每个字节
  2. 超时重传:丢包后重新发送
  3. 滑动窗口:流水线传输,提高效率
  4. 流量控制:防止接收方被淹没
  5. 拥塞控制:防止网络过载

Q5: HTTP/3 为什么使用 UDP?

答案

HTTP/3 基于 QUIC 协议,QUIC 使用 UDP 的原因:

  1. 避免队头阻塞:TCP 一个包丢失会阻塞所有流
  2. 更快的握手:0-RTT 建立连接
  3. 连接迁移:IP 变化不影响连接
// QUIC 在 UDP 基础上实现了:
// - 类似 TCP 的可靠传输
// - 类似 TLS 的加密
// - 多路复用
// - 快速握手

Q6: TCP 的拥塞控制算法(慢启动、拥塞避免、快重传、快恢复)

答案

TCP 的拥塞控制机制用于防止发送方向网络注入过多数据导致网络拥塞。核心思想是维护一个拥塞窗口(cwnd),动态调整发送速率。四个经典算法协同工作:

四个算法的关系

1. 慢启动(Slow Start)

连接刚建立时,不知道网络容量,从小的 cwnd 开始指数增长

// 慢启动过程(简化描述)
let cwnd = 1; // 初始拥塞窗口:1 MSS(最大报文段大小)
let ssthresh = 64; // 慢启动阈值

// 每收到一个 ACK,cwnd 增加 1 MSS
// 实际效果:每经过一个 RTT,cwnd 翻倍
// RTT 1: cwnd = 1 → 发送 1 个包
// RTT 2: cwnd = 2 → 发送 2 个包
// RTT 3: cwnd = 4 → 发送 4 个包
// RTT 4: cwnd = 8 → 发送 8 个包
// ...指数增长,直到 cwnd >= ssthresh

2. 拥塞避免(Congestion Avoidance)

当 cwnd 达到慢启动阈值后,切换为线性增长,谨慎探测网络容量:

// 拥塞避免过程
// 当 cwnd >= ssthresh 时进入拥塞避免
// 每经过一个 RTT,cwnd 增加 1 MSS(而不是翻倍)

// RTT 1: cwnd = 64 → 发送 64 个包
// RTT 2: cwnd = 65 → 发送 65 个包
// RTT 3: cwnd = 66 → 发送 66 个包
// ...线性增长,缓慢探测网络容量上限

3. 快重传(Fast Retransmit)

不等超时,收到 3 个重复 ACK 就立即重传丢失的报文段:

// 快重传场景
// 发送: pkt1, pkt2, pkt3, pkt4, pkt5
// pkt2 丢失
// 接收方收到 pkt3 → 发送 ACK2(重复)
// 接收方收到 pkt4 → 发送 ACK2(重复)
// 接收方收到 pkt5 → 发送 ACK2(重复)
// 发送方收到 3 个重复 ACK2 → 立即重传 pkt2(不等超时)
为什么是 3 个重复 ACK?

1-2 个重复 ACK 可能只是网络乱序(包到达顺序不对),不一定是丢包。3 个重复 ACK 大概率说明包确实丢了,所以立即重传。

4. 快恢复(Fast Recovery)

配合快重传使用,避免像超时那样将 cwnd 直接降到 1:

// 快恢复过程
let cwnd = 32;
let ssthresh: number;

// 检测到丢包(3 个重复 ACK)时:
ssthresh = Math.floor(cwnd / 2); // ssthresh = 16
cwnd = ssthresh + 3; // cwnd = 19(加 3 是因为已收到 3 个重复 ACK)

// 之后进入拥塞避免阶段,cwnd 线性增长

// 对比:如果是超时丢包
// ssthresh = Math.floor(cwnd / 2); // ssthresh = 16
// cwnd = 1; // cwnd 重置为 1,重新慢启动

完整流程图

事件ssthresh 变化cwnd 变化进入阶段
连接建立初始值(较大)1 MSS慢启动
cwnd >= ssthresh不变继续增长拥塞避免
3 个重复 ACKcwnd / 2ssthresh + 3快恢复 → 拥塞避免
超时cwnd / 21 MSS慢启动

Q7: TCP 粘包和拆包问题怎么解决?

答案

什么是粘包和拆包?

TCP 是面向字节流的协议,没有消息边界的概念。发送方写入的多个数据包可能被合并成一个 TCP 段发送(粘包),或一个大数据包被拆分成多个 TCP 段发送(拆包)。

注意

严格来说,"粘包"不是 TCP 的 bug,而是 TCP 面向字节流的设计特性。应用层需要自己定义消息边界。

产生原因

// 粘包的原因:
// 1. 发送方:Nagle 算法将小包合并发送,减少网络开销
// 2. 接收方:TCP 接收缓冲区中多个包堆积,应用层一次读取了多个包

// 拆包的原因:
// 1. 发送的数据超过 MSS(最大报文段大小),TCP 会拆分
// 2. 发送的数据超过发送缓冲区剩余空间

解决方案

有三种常见的应用层解决方案:

方案 1:固定长度协议

// 每个消息固定长度,不足的部分用特定字符填充
const FIXED_LENGTH = 128;

// 发送方:填充到固定长度
const encodeFixedLength = (message: string): Buffer => {
const buf = Buffer.alloc(FIXED_LENGTH, 0); // 用 0 填充
buf.write(message, 'utf-8');
return buf;
};

// 接收方:按固定长度切割
const decodeFixedLength = (data: Buffer): string[] => {
const messages: string[] = [];
for (let i = 0; i < data.length; i += FIXED_LENGTH) {
const msg = data.subarray(i, i + FIXED_LENGTH)
.toString('utf-8')
.replace(/\0+$/, ''); // 去除填充字符
if (msg) messages.push(msg);
}
return messages;
};

方案 2:分隔符协议

// 用特殊分隔符标记消息边界(如 \n、\r\n、自定义分隔符)

class DelimiterDecoder {
private buffer = '';
private delimiter: string;

constructor(delimiter = '\n') {
this.delimiter = delimiter;
}

// 接收数据,返回完整的消息数组
decode(chunk: string): string[] {
this.buffer += chunk;
const messages: string[] = [];
let index: number;

// 按分隔符切割
while ((index = this.buffer.indexOf(this.delimiter)) !== -1) {
messages.push(this.buffer.substring(0, index));
this.buffer = this.buffer.substring(index + this.delimiter.length);
}

return messages;
}
}

// 使用示例
const decoder = new DelimiterDecoder('\n');
// 假设收到粘包数据:"hello\nworld\nfoo"
const msgs = decoder.decode('hello\nworld\nfoo');
// msgs = ['hello', 'world'],'foo' 留在 buffer 中等待下次拼接

方案 3:长度前缀协议(最常用)

// 消息格式:[4字节消息长度][消息内容]
// 这是最常用的方案,HTTP 的 Content-Length 就是这个思路

class LengthPrefixCodec {
private buffer: Buffer = Buffer.alloc(0);
private readonly HEADER_SIZE = 4; // 4 字节存储消息长度

// 编码:在消息前加上长度头
encode(message: string): Buffer {
const body = Buffer.from(message, 'utf-8');
const header = Buffer.alloc(this.HEADER_SIZE);
header.writeUInt32BE(body.length, 0); // 大端序写入长度
return Buffer.concat([header, body]);
}

// 解码:从缓冲区中提取完整消息
decode(chunk: Buffer): string[] {
this.buffer = Buffer.concat([this.buffer, chunk]);
const messages: string[] = [];

while (this.buffer.length >= this.HEADER_SIZE) {
const bodyLength = this.buffer.readUInt32BE(0);

// 数据不够一个完整消息,等待更多数据
if (this.buffer.length < this.HEADER_SIZE + bodyLength) {
break;
}

// 提取完整消息
const body = this.buffer.subarray(
this.HEADER_SIZE,
this.HEADER_SIZE + bodyLength
);
messages.push(body.toString('utf-8'));

// 移除已处理的数据
this.buffer = this.buffer.subarray(this.HEADER_SIZE + bodyLength);
}

return messages;
}
}

三种方案对比

方案优点缺点适用场景
固定长度实现简单浪费空间,不灵活消息长度固定的场景
分隔符灵活,无浪费消息内容不能包含分隔符文本协议(如 Redis RESP)
长度前缀最灵活,支持二进制实现稍复杂最通用(HTTP、Protobuf)
面试加分点

实际项目中,WebSocket 自带消息边界(基于帧的协议),不存在粘包问题。HTTP 通过 Content-Length 头或 Transfer-Encoding: chunked 来解决边界问题。粘包问题主要出现在自定义 TCP 协议的场景(如游戏服务器、IoT 设备通信)。

Q8: WebRTC 为什么用 UDP 而不是 TCP?

答案

WebRTC(Web Real-Time Communication)是用于浏览器之间实时音视频通信的技术。它选择 UDP 作为底层传输协议,主要有以下几个原因:

核心原因:实时性优先

原因 1:TCP 的重传机制导致延迟不可控

// TCP 丢包时的行为:
// 1. 检测到丢包(超时或 3 个重复 ACK)
// 2. 重传丢失的包
// 3. 后续的包必须等待丢失的包到达后才能交付给应用层(队头阻塞)

// 对于视频通话:
// - 一帧视频的延迟 = 33ms (30fps)
// - TCP 重传可能需要 100-300ms+
// - 等重传完成时,这帧视频已经过期了,用户看到的是卡顿

// UDP 的行为:
// - 丢了就丢了,跳过这帧
// - 后续帧正常播放
// - 用户感知:偶尔画面略有抖动,但不会卡顿

原因 2:TCP 的拥塞控制过于保守

// TCP 检测到丢包时:
// - 慢启动:cwnd 降到 1,从头开始
// - 快恢复:cwnd 降到一半
// → 带宽急剧下降,视频画质突然变差

// WebRTC 使用自己的拥塞控制算法(GCC/BBR):
// - 更适合实时媒体的特点
// - 可以根据延迟变化(而非丢包)来调整码率
// - 平滑降级,而不是断崖式降级

原因 3:TCP 的连接建立耗时长

// TCP + TLS 连接建立:
// TCP 三次握手: 1 RTT
// TLS 握手: 1-2 RTT
// 总计: 2-3 RTT(可能 200-600ms)

// WebRTC 使用 ICE + DTLS + SRTP:
// ICE 连接检查(并行探测多条路径)
// DTLS 握手(基于 UDP 的 TLS,通常 1 RTT)
// 并且支持 ICE restart,网络切换更快

WebRTC 的协议栈

WebRTC 并不是裸用 UDP,而是在 UDP 之上构建了一套完整的协议栈:

协议作用基于
SRTP加密传输音视频数据,支持丢包容忍UDP
SCTP可靠/不可靠传输数据通道消息DTLS over UDP
DTLS基于 UDP 的 TLS,提供加密UDP
ICE/STUN/TURNNAT 穿越,建立 P2P 连接UDP
SCTP 的灵活性

WebRTC 的 DataChannel 使用 SCTP 协议,它支持可靠传输不可靠传输两种模式:

  • 可靠模式:类似 TCP,保证送达和顺序(用于聊天消息、文件传输)
  • 不可靠模式:类似 UDP,允许丢包(用于游戏状态同步)

总结对比

维度TCPUDP (WebRTC)
延迟重传导致不可控延迟丢包跳过,延迟稳定
队头阻塞有(一个包丢失阻塞所有后续包)
拥塞控制保守(丢包 → 大幅降速)灵活(GCC 算法,按延迟调整)
连接建立2-3 RTTICE + DTLS 并行探测
可靠性强制可靠按需选择(SRTP 不可靠/SCTP 可选)
NAT 穿越困难ICE/STUN/TURN 专门解决
面试加分点

可以补充提到 HTTP/3 的 QUIC 协议也是基于 UDP 的,原因类似——避免 TCP 的队头阻塞和连接建立延迟。但 QUIC 和 WebRTC 的设计目标不同:QUIC 追求可靠的网页加载,WebRTC 追求低延迟的实时通信

相关链接