TCP和UDP的区别
2025/12/21大约 12 分钟
TCP和UDP的区别
基本定义
TCP (Transmission Control Protocol) - 传输控制协议
- 面向连接的、可靠的、基于字节流的传输层协议
- 提供全双工通信
- 保证数据顺序和完整性
- 有流量控制和拥塞控制机制
- 适用于对数据可靠性要求高的场景
UDP (User Datagram Protocol) - 用户数据报协议
- 无连接的、不可靠的、基于数据报的传输层协议
- 简单快速,开销小
- 不保证数据送达、顺序或不重复
- 无流量控制和拥塞控制
- 适用于对实时性要求高、可容忍少量丢包的场景
详细对比表
| 对比项 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接(三次握手建立) | 无连接(直接发送) |
| 可靠性 | 可靠传输(ACK确认) | 不可靠传输(尽力而为) |
| 传输方式 | 字节流 | 数据报(有边界) |
| 速度 | 慢(需要建立连接、确认) | 快(无需连接和确认) |
| 开销 | 大(20字节头部+确认机制) | 小(8字节头部) |
| 顺序保证 | 保证顺序 | 不保证顺序 |
| 重复保护 | 去重 | 可能重复 |
| 流量控制 | 有(滑动窗口) | 无 |
| 拥塞控制 | 有(慢启动、拥塞避免等) | 无 |
| 一对一/多 | 仅支持一对一 | 支持一对一、一对多、多对多 |
| 头部大小 | 20-60字节 | 8字节 |
| 应用场景 | HTTP、HTTPS、FTP、SMTP | DNS、DHCP、视频直播、游戏 |
TCP头部结构
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口 (16位) | 目标端口 (16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 序列号 (32位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 确认号 (32位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 头长 |保留|U|A|P|R|S|F| 窗口大小 (16位) |
| (4位)|(6位)|R|C|S|S|Y|I| |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 校验和 (16位) | 紧急指针 (16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 选项 (可变长度) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
TCP头部大小:20-60字节(基本20字节 + 最多40字节选项)UDP头部结构
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口 (16位) | 目标端口 (16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 长度 (16位) | 校验和 (16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
UDP头部大小:固定8字节TCP的可靠性保证机制
1. 序列号和确认号
- 每个字节都有序列号
- 接收方发送ACK确认收到的数据
- 发送方根据ACK判断是否需要重传
2. 超时重传
- 发送数据后启动定时器
- 超时未收到ACK则重传
- 动态调整超时时间(RTO)
3. 校验和
- 检测数据在传输过程中是否损坏
- 损坏的数据包会被丢弃
4. 流量控制(滑动窗口)
- 接收方通告窗口大小
- 防止发送方发送过快导致接收方缓冲区溢出
5. 拥塞控制
- 慢启动(Slow Start)
- 拥塞避免(Congestion Avoidance)
- 快速重传(Fast Retransmit)
- 快速恢复(Fast Recovery)
6. 顺序保证
- 使用序列号对数据排序
- 乱序到达的数据会被重新排序
Golang代码示例
package main
import (
"fmt"
"net"
"time"
)
// ============ TCP 示例 ============
// TCP服务器
func tcpServer() {
// 监听TCP端口
listener, err := net.Listen("tcp", "localhost:8080")
if err != nil {
fmt.Println("TCP监听失败:", err)
return
}
defer listener.Close()
fmt.Println("TCP服务器启动,监听 localhost:8080")
for {
// 接受连接
conn, err := listener.Accept()
if err != nil {
fmt.Println("接受连接失败:", err)
continue
}
// 处理连接
go handleTCPConnection(conn)
}
}
func handleTCPConnection(conn net.Conn) {
defer conn.Close()
fmt.Printf("新的TCP连接: %s\n", conn.RemoteAddr())
// 读取数据
buffer := make([]byte, 1024)
for {
n, err := conn.Read(buffer)
if err != nil {
fmt.Println("读取数据失败:", err)
return
}
data := buffer[:n]
fmt.Printf("TCP收到: %s\n", string(data))
// 回复数据
response := fmt.Sprintf("TCP回复: %s", string(data))
conn.Write([]byte(response))
}
}
// TCP客户端
func tcpClient() {
// 连接TCP服务器(三次握手)
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
fmt.Println("TCP连接失败:", err)
return
}
defer conn.Close()
fmt.Println("TCP连接成功")
// 发送数据
messages := []string{"Hello", "World", "TCP"}
for _, msg := range messages {
_, err := conn.Write([]byte(msg))
if err != nil {
fmt.Println("发送失败:", err)
return
}
fmt.Printf("TCP发送: %s\n", msg)
// 接收响应
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
fmt.Println("接收失败:", err)
return
}
fmt.Printf("TCP收到响应: %s\n", string(buffer[:n]))
time.Sleep(1 * time.Second)
}
}
// ============ UDP 示例 ============
// UDP服务器
func udpServer() {
// 监听UDP端口
addr, err := net.ResolveUDPAddr("udp", "localhost:9090")
if err != nil {
fmt.Println("解析地址失败:", err)
return
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println("UDP监听失败:", err)
return
}
defer conn.Close()
fmt.Println("UDP服务器启动,监听 localhost:9090")
buffer := make([]byte, 1024)
for {
// 接收数据(无需建立连接)
n, clientAddr, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Println("读取UDP数据失败:", err)
continue
}
data := buffer[:n]
fmt.Printf("UDP收到来自 %s: %s\n", clientAddr, string(data))
// 发送响应
response := fmt.Sprintf("UDP回复: %s", string(data))
conn.WriteToUDP([]byte(response), clientAddr)
}
}
// UDP客户端
func udpClient() {
// 解析服务器地址
serverAddr, err := net.ResolveUDPAddr("udp", "localhost:9090")
if err != nil {
fmt.Println("解析地址失败:", err)
return
}
// 创建UDP连接(实际上是伪连接,只是绑定了地址)
conn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil {
fmt.Println("创建UDP连接失败:", err)
return
}
defer conn.Close()
fmt.Println("UDP客户端就绪")
// 发送数据(无需三次握手,直接发送)
messages := []string{"Hello", "World", "UDP"}
for _, msg := range messages {
_, err := conn.Write([]byte(msg))
if err != nil {
fmt.Println("UDP发送失败:", err)
continue
}
fmt.Printf("UDP发送: %s\n", msg)
// 接收响应(可能丢失,不保证)
buffer := make([]byte, 1024)
conn.SetReadDeadline(time.Now().Add(2 * time.Second)) // 设置超时
n, err := conn.Read(buffer)
if err != nil {
fmt.Println("UDP接收超时或失败:", err)
continue
}
fmt.Printf("UDP收到响应: %s\n", string(buffer[:n]))
time.Sleep(1 * time.Second)
}
}
// ============ TCP vs UDP 性能测试 ============
func benchmarkTCP(dataSize int, count int) time.Duration {
// 启动TCP服务器
listener, _ := net.Listen("tcp", "localhost:0")
defer listener.Close()
go func() {
conn, _ := listener.Accept()
buffer := make([]byte, dataSize)
for i := 0; i < count; i++ {
conn.Read(buffer)
conn.Write([]byte("OK"))
}
conn.Close()
}()
// TCP客户端
conn, _ := net.Dial("tcp", listener.Addr().String())
defer conn.Close()
data := make([]byte, dataSize)
buffer := make([]byte, 2)
start := time.Now()
for i := 0; i < count; i++ {
conn.Write(data)
conn.Read(buffer)
}
return time.Since(start)
}
func benchmarkUDP(dataSize int, count int) time.Duration {
// 启动UDP服务器
serverAddr, _ := net.ResolveUDPAddr("udp", "localhost:0")
serverConn, _ := net.ListenUDP("udp", serverAddr)
defer serverConn.Close()
go func() {
buffer := make([]byte, dataSize)
for i := 0; i < count; i++ {
n, addr, _ := serverConn.ReadFromUDP(buffer)
if n > 0 {
serverConn.WriteToUDP([]byte("OK"), addr)
}
}
}()
// UDP客户端
clientConn, _ := net.DialUDP("udp", nil, serverConn.LocalAddr().(*net.UDPAddr))
defer clientConn.Close()
data := make([]byte, dataSize)
buffer := make([]byte, 2)
start := time.Now()
for i := 0; i < count; i++ {
clientConn.Write(data)
clientConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
clientConn.Read(buffer)
}
return time.Since(start)
}
// ============ 广播和组播示例(UDP特性) ============
// UDP广播
func udpBroadcast() {
conn, err := net.Dial("udp", "255.255.255.255:9999")
if err != nil {
fmt.Println("创建广播连接失败:", err)
return
}
defer conn.Close()
message := "Broadcast Message"
conn.Write([]byte(message))
fmt.Println("发送UDP广播:", message)
}
// UDP组播
func udpMulticast() {
// 组播地址范围:224.0.0.0 ~ 239.255.255.255
groupAddr, _ := net.ResolveUDPAddr("udp", "224.0.0.1:9999")
conn, err := net.DialUDP("udp", nil, groupAddr)
if err != nil {
fmt.Println("创建组播连接失败:", err)
return
}
defer conn.Close()
message := "Multicast Message"
conn.Write([]byte(message))
fmt.Println("发送UDP组播:", message)
}
// ============ 可靠UDP实现示例(应用层实现) ============
type ReliableUDP struct {
conn *net.UDPConn
seqNum uint32
ackChan map[uint32]chan bool
}
func NewReliableUDP(addr string) (*ReliableUDP, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
return nil, err
}
return &ReliableUDP{
conn: conn,
seqNum: 0,
ackChan: make(map[uint32]chan bool),
}, nil
}
func (r *ReliableUDP) SendReliable(data []byte) error {
r.seqNum++
seqNum := r.seqNum
// 构造带序列号的数据包
packet := append([]byte{byte(seqNum >> 24), byte(seqNum >> 16),
byte(seqNum >> 8), byte(seqNum)}, data...)
// 创建ACK通道
r.ackChan[seqNum] = make(chan bool, 1)
// 发送数据,带重传机制
for retry := 0; retry < 3; retry++ {
r.conn.Write(packet)
// 等待ACK
select {
case <-r.ackChan[seqNum]:
delete(r.ackChan, seqNum)
return nil
case <-time.After(1 * time.Second):
fmt.Printf("序列号 %d 超时,重传 (%d/3)\n", seqNum, retry+1)
}
}
delete(r.ackChan, seqNum)
return fmt.Errorf("发送失败,已重传3次")
}
func main() {
// 启动TCP服务器
go tcpServer()
time.Sleep(1 * time.Second)
// 启动UDP服务器
go udpServer()
time.Sleep(1 * time.Second)
fmt.Println("=== TCP客户端示例 ===")
go tcpClient()
time.Sleep(5 * time.Second)
fmt.Println("\n=== UDP客户端示例 ===")
go udpClient()
time.Sleep(5 * time.Second)
fmt.Println("\n=== 性能对比 ===")
dataSize := 1024
count := 1000
tcpTime := benchmarkTCP(dataSize, count)
fmt.Printf("TCP: %d次传输,每次%d字节,耗时: %v\n", count, dataSize, tcpTime)
udpTime := benchmarkUDP(dataSize, count)
fmt.Printf("UDP: %d次传输,每次%d字节,耗时: %v\n", count, dataSize, udpTime)
fmt.Printf("UDP比TCP快: %.2f%%\n", float64(tcpTime-udpTime)/float64(tcpTime)*100)
}使用场景对比
TCP适用场景:
- 文件传输:FTP、HTTP下载,需要完整数据
- Web浏览:HTTP/HTTPS,需要可靠传输
- 邮件:SMTP、POP3、IMAP
- 远程登录:SSH、Telnet
- 数据库连接:MySQL、PostgreSQL
- 即时通讯:微信聊天消息
- 金融交易:支付、转账等关键业务
UDP适用场景:
- 视频直播:允许少量丢帧,实时性重要
- 在线游戏:位置同步,实时性>可靠性
- 语音通话:VoIP,少量丢包可接受
- DNS查询:快速查询,失败可重试
- DHCP:获取IP地址
- 流媒体:视频点播(可容忍丢包)
- IoT设备:传感器数据上报
- 广播/组播:一对多通信
性能对比
吞吐量:
- UDP > TCP(无确认和重传开销)
- TCP有滑动窗口限制
延迟:
- UDP延迟更低(无连接建立和等待ACK)
- TCP需要等待ACK确认
CPU占用:
- UDP占用更少(协议简单)
- TCP需要维护连接状态、重传队列等
带宽利用率:
- UDP头部8字节,开销小
- TCP头部20-60字节,开销大
相关面试题
Q1: 如何在UDP上实现可靠传输?
答案:
在应用层实现类似TCP的机制:
- 序列号:为每个数据包编号
- 确认机制:接收方发送ACK确认
- 超时重传:未收到ACK则重传
- 去重:根据序列号去除重复包
- 顺序保证:使用序列号排序
- 流量控制:接收方通告窗口大小
实际应用:
- QUIC协议:基于UDP实现可靠传输(HTTP/3使用)
- KCP协议:游戏中常用的可靠UDP
- UDT协议:高速数据传输
Q2: TCP如何保证顺序?
答案:
- 序列号(Sequence Number):
- 每个字节都有唯一的序列号
- 发送方按序列号发送数据
- 接收方排序:
- 接收到乱序数据包时,根据序列号缓存
- 等待缺失的数据包到达
- 按序交付给应用层
- 确认号(ACK):
- 确认号表示期望接收的下一个字节序列号
- 乱序到达不影响ACK发送
Q3: 为什么UDP更快?
答案:
- 无连接开销:不需要三次握手和四次挥手
- 无确认等待:发送后立即返回,不等待ACK
- 头部更小:8字节 vs 20-60字节
- 无重传机制:不维护重传队列和定时器
- 无流量控制:不需要维护滑动窗口
- 无拥塞控制:不需要慢启动等算法
- 协议简单:内核处理更快
代价:不可靠,可能丢包、乱序、重复
Q4: TCP的粘包和拆包问题是什么?如何解决?
答案:
粘包:多个小数据包被合并成一个大包发送
拆包:一个大数据包被拆分成多个小包发送
原因:
- TCP是字节流协议,没有消息边界
- Nagle算法会合并小包
- MSS(最大报文段)限制会拆分大包
解决方案:
- 固定长度:每个消息固定大小
- 分隔符:用特殊字符分隔消息(如\n)
- 长度前缀:消息头包含长度信息
- 自定义协议:定义消息格式
// 长度前缀方案
func sendMessage(conn net.Conn, data []byte) {
length := uint32(len(data))
// 先发送4字节长度
binary.Write(conn, binary.BigEndian, length)
// 再发送数据
conn.Write(data)
}
func receiveMessage(conn net.Conn) ([]byte, error) {
// 先读取4字节长度
var length uint32
binary.Read(conn, binary.BigEndian, &length)
// 再读取指定长度的数据
data := make([]byte, length)
io.ReadFull(conn, data)
return data, nil
}Q5: UDP有粘包问题吗?
答案:
没有。UDP是数据报协议,有明确的消息边界:
- 每次发送的数据都是一个独立的数据报
- 接收方一次
recvfrom对应一次sendto - 要么完整接收,要么丢失,不会粘连
但是:
- UDP可能丢包、乱序
- 应用层需要处理这些问题
Q6: TCP的TIME_WAIT状态是什么?为什么需要?
答案:
定义:TCP连接主动关闭方在发送最后一个ACK后进入的状态,持续2MSL(最大报文段生存时间,通常1-4分钟)
作用:
- 确保被动关闭方收到最后的ACK:
- 如果最后的ACK丢失,被动方会重传FIN
- TIME_WAIT状态可以响应重传的FIN
- 防止旧连接的数据包干扰新连接:
- 等待网络中残留的数据包消失
- 保证新连接不会收到旧连接的数据
问题:
- 大量TIME_WAIT占用端口资源
- 高并发服务器可能端口耗尽
解决方案:
- 启用
SO_REUSEADDR - 调整
tcp_tw_reuse参数 - 使用连接池
- 让客户端主动关闭
Q7: 如何选择TCP还是UDP?
答案:
选择TCP的条件:
- ✅ 需要可靠传输(不能丢数据)
- ✅ 需要保证顺序
- ✅ 数据完整性要求高
- ✅ 已有成熟的协议(HTTP、FTP)
- ❌ 可以接受较高延迟
选择UDP的条件:
- ✅ 实时性要求高
- ✅ 可以容忍丢包
- ✅ 需要广播/组播
- ✅ 数据量小(DNS查询)
- ❌ 可以接受不可靠
决策流程:
是否需要可靠传输?
└── 是 → 选TCP
└── 否 → 是否需要实时性?
└── 是 → 选UDP
└── 否 → 看具体需求Q8: QUIC协议是什么?
答案:
定义:Quick UDP Internet Connections,基于UDP的传输层协议,结合了TCP的可靠性和UDP的快速
特点:
- 基于UDP:避免TCP的队头阻塞
- 0-RTT连接:快速建立连接
- 多路复用:一个连接多个流
- 连接迁移:IP变化不断连接(移动网络)
- 改进的拥塞控制
- 内置TLS 1.3:安全且快速
优势:
- 比TCP+TLS快(减少握手次数)
- 无队头阻塞(不同流独立)
- 更好的移动网络支持
应用:
- HTTP/3基于QUIC
- Google服务广泛使用
- 逐渐成为主流
关键点总结
TCP vs UDP核心区别:
- 连接性:TCP面向连接,UDP无连接
- 可靠性:TCP可靠,UDP不可靠
- 速度:UDP更快,TCP较慢
- 开销:UDP小,TCP大
- 场景:TCP适合文件传输,UDP适合实时通信
选择原则:
- 数据重要性 > 实时性 → TCP
- 实时性 > 数据重要性 → UDP
- 需要广播/组播 → UDP
- 已有协议标准 → 遵循标准
优化建议:
- TCP:调整窗口大小、启用TCP Fast Open
- UDP:应用层实现可靠性(如QUIC)
- 根据业务场景选择合适的协议