在 Python 网络开发、爬虫、接口通信、物联网数据传输等场景中,TCP 和UDP是最核心的两种传输层协议。绝大多数网络通信程序的底层逻辑,都是基于这两个协议实现的。
本文将从零讲解 TCP、UDP 的核心特性、区别,搭配可直接运行的 Python 完整代码示例(服务端 + 客户端),手把手带你掌握两种协议的编程实现、适用场景和避坑要点。
本文所有代码基于 Python3.x,无需额外安装第三方库,使用内置socket标准库即可运行。
一、前置知识:Socket 套接字
Socket(套接字)是网络通信的端点 ,是程序之间通过网络传输数据的接口。简单来说:网络编程就是对 Socket 进行操作。
Python 内置的 socket 模块封装了底层网络协议的复杂逻辑,我们只需简单调用 API,即可快速实现 TCP/UDP 通信。
Socket 通信核心三要素:IP 地址 + 端口号 + 传输协议 (TCP/UDP)
二、TCP 协议:可靠的面向连接通信
2.1 TCP 核心特性
TCP(传输控制协议,Transmission Control Protocol)是面向连接、可靠、有序的传输协议,是互联网最常用的协议(HTTP、HTTPS、FTP 均基于 TCP)。
- 面向连接:通信前必须经过「三次握手」建立专属连接,通信结束后「四次挥手」断开连接
- 可靠性高:自带确认应答、超时重传、拥塞控制机制,数据不丢失、不重复
- 有序传输:严格按照发送顺序接收数据
- 流式传输:数据是无边界的字节流,一次发送可多次接收
- 开销较大:握手、校验机制会消耗带宽和性能
2.2 TCP 通信流程
TCP 是服务端被动监听、客户端主动连接的模式:
- 服务端:创建 Socket → 绑定 IP 端口 → 监听连接 → 接收客户端连接 → 收发数据
- 客户端:创建 Socket → 主动连接服务端 → 收发数据 → 关闭连接
2.3 Python TCP 完整实战示例
我们实现一个简单的TCP 问答通信:客户端发送消息,服务端接收并回复消息。
TCP 服务端代码(tcp_server.py)
python
# 导入内置socket库
import socket
# 1. 创建TCP套接字 (SOCK_STREAM 代表TCP协议)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定IP和端口
# IP为空代表监听本机所有网卡,端口号10086(自定义1024-65535未占用端口)
HOST = ""
PORT = 10086
server_socket.bind((HOST, PORT))
# 3. 开启监听,参数5代表最大挂起连接数
server_socket.listen(5)
print(f"TCP服务端已启动,监听端口:{PORT},等待客户端连接...")
# 4. 等待客户端连接(阻塞方法,无连接时会卡在这里)
client_conn, client_addr = server_socket.accept()
print(f"成功连接客户端:{client_addr}")
# 5. 循环收发数据
while True:
# 接收客户端数据,缓冲区大小1024字节
recv_data = client_conn.recv(1024)
if not recv_data:
print("客户端已断开连接")
break
# 字节数据解码为字符串
recv_msg = recv_data.decode("utf-8")
print(f"收到客户端消息:{recv_msg}")
# 服务端回复消息
send_msg = f"服务端已收到:{recv_msg}"
client_conn.send(send_msg.encode("utf-8"))
# 6. 关闭连接
client_conn.close()
server_socket.close()
TCP 客户端代码(tcp_client.py)
python
import socket
# 1. 创建TCP套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 连接服务端(IP+端口,与服务端保持一致)
HOST = "127.0.0.1"
PORT = 10086
client_socket.connect((HOST, PORT))
# 3. 循环发送接收消息
while True:
# 手动输入消息
msg = input("请输入发送给服务端的消息(exit退出):")
if msg == "exit":
break
# 编码发送数据
client_socket.send(msg.encode("utf-8"))
# 接收服务端回复
recv_data = client_socket.recv(1024)
print(f"收到服务端回复:{recv_data.decode('utf-8')}")
# 4. 关闭套接字
client_socket.close()
运行效果
- 先运行
tcp_server.py,服务端启动并等待连接 - 再运行
tcp_client.py,客户端自动连接服务端 - 客户端输入任意内容,服务端实时接收并回复,输入 exit 即可退出
三、UDP 协议:高效的无连接通信
3.1 UDP 核心特性
UDP(用户数据报协议,User Datagram Protocol)是无连接、不可靠、快速的传输协议,专注于高速传输,牺牲了部分可靠性。
- 无连接:无需建立连接,直接发送数据,不需要握手挥手
- 不可靠:无确认机制,数据可能丢失、乱序、重复
- 数据报传输:数据有明确边界,一次发送对应一次接收
- 开销极低:协议头部小,传输速度快、延迟低
- 支持广播:可一对多群发数据(TCP 不支持)
3.2 UDP 通信流程
UDP 无严格的服务端客户端绑定关系,双方地位平等,任意一方都可以主动收发数据:
- 服务端:创建 UDP 套接字 → 绑定端口 → 循环接收数据、回复数据
- 客户端:创建 UDP 套接字 → 直接发送数据 → 接收回复
3.3 Python UDP 完整实战示例
实现 UDP 双向通信,无需建立连接,直接收发数据。
UDP 服务端代码(udp_server.py)
python
import socket
# 1. 创建UDP套接字 (SOCK_DGRAM 代表UDP协议)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2. 绑定IP和端口
HOST = "127.0.0.1"
PORT = 10087
server_socket.bind((HOST, PORT))
print(f"UDP服务端已启动,监听端口:{PORT}")
# 3. 循环接收客户端数据
while True:
# recvfrom:接收数据,返回(数据, 客户端地址)
recv_data, client_addr = server_socket.recvfrom(1024)
recv_msg = recv_data.decode("utf-8")
print(f"来自{client_addr}的消息:{recv_msg}")
# 针对性回复客户端
send_msg = f"UDP服务端已接收:{recv_msg}"
server_socket.sendto(send_msg.encode("utf-8"), client_addr)
UDP 客户端代码(udp_client.py)
python
import socket
# 1. 创建UDP套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 服务端地址
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 10087
# 2. 循环收发数据
while True:
msg = input("请输入发送给UDP服务端的消息(exit退出):")
if msg == "exit":
break
# 发送数据到服务端
client_socket.sendto(msg.encode("utf-8"), (SERVER_HOST, SERVER_PORT))
# 接收服务端回复
recv_data, _ = client_socket.recvfrom(1024)
print(f"收到UDP服务端回复:{recv_data.decode('utf-8')}")
client_socket.close()
运行效果
- 先启动 UDP 服务端,再启动客户端
- 客户端随时发送消息,服务端实时响应
- 核心区别:即使关闭客户端,服务端不会报错;重启客户端后可直接通信,无需重新连接
四、TCP 与 UDP 核心区别对比
| 对比维度 | TCP | UDP |
|---|---|---|
| 连接特性 | 面向连接(三次握手 / 四次挥手) | 无连接,无需建立通信链路 |
| 可靠性 | 可靠,无丢失、无乱序、无重复 | 不可靠,可能丢包、乱序 |
| 传输方式 | 字节流传输,无数据边界 | 数据报传输,有明确数据边界 |
| 速度延迟 | 速度较慢,延迟较高 | 速度极快,延迟极低 |
| 开销大小 | 协议开销大 | 协议开销极小 |
| 适用场景 | 文件传输、网页访问、登录通信(需保证数据完整) | 直播、游戏、语音视频、物联网上报(容忍少量丢包,追求速度) |
五、常见踩坑问题总结
5.1 TCP 粘包问题
TCP 是字节流协议 ,没有数据包边界。操作系统内核有缓冲区:发送方会把多次小数据缓存合并再发送;接收方缓冲区会积攒数据,recv()一次读取缓冲区里所有可用字节。 于是出现两种粘包场景:
- 发送粘包:连续多次 send,内核合并成一个数据包发出
- 接收粘包:一次 recv 读到多条消息,分不清消息起止
⚠️ 粘包不是 bug,是 TCP 正常特性;UDP 不会粘包(数据报自带边界)。
常见解决方案
- 固定长度报文:每条消息固定字节,不足补填充字符(简单,浪费带宽)
- 特殊分隔符 :消息末尾加换行 /
\r\n,适合文本协议(HTTP 就是这种) - 消息头 + 消息体(推荐):头部存放消息体长度,接收端先读固定长度头部,解析出 body 长度,再读取对应字节。工业 / 后端网络程序最常用。
下面实现**方案 3(消息头 + 消息体)**完整可运行示例。
带消息头方案:TCP 服务端(tcp_server_fix.py)
python
import socket
import struct
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 开启端口复用,避免重启服务端口占用
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
HOST = ""
PORT = 10086
server_socket.bind((HOST, PORT))
server_socket.listen(5)
print(f"TCP服务端(解决粘包)启动,监听 {PORT}")
# 定义:消息头固定4字节,用struct打包uint,代表消息体长度
# !I 代表网络大端序 unsigned int,占4字节
HEADER_SIZE = 4
def recv_all(conn, size):
"""保证读取指定size字节,不足就持续接收"""
buffer = b""
while len(buffer) < size:
data = conn.recv(size - len(buffer))
if not data:
return None # 连接断开
buffer += data
return buffer
while True:
client_conn, client_addr = server_socket.accept()
print(f"新客户端接入:{client_addr}")
with client_conn:
while True:
# 第一步:先读取4字节消息头,拿到body长度
header_data = recv_all(client_conn, HEADER_SIZE)
if not header_data:
print(f"客户端 {client_addr} 断开连接")
break
# 解包得到消息体长度
body_len = struct.unpack("!I", header_data)[0]
# 第二步:按长度读取消息体
body_data = recv_all(client_conn, body_len)
if not body_data:
break
msg = body_data.decode("utf-8")
print(f"收到消息:{msg}")
# 回复客户端:同样封装【长度头 + 消息体】
resp_msg = f"server reply: {msg}"
resp_body = resp_msg.encode("utf-8")
resp_header = struct.pack("!I", len(resp_body))
client_conn.sendall(resp_header + resp_body)
server_socket.close()
带消息头方案:TCP 客户端(tcp_client_fix.py)
python
import socket
import struct
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
HOST = "127.0.0.1"
PORT = 10086
client_socket.connect((HOST, PORT))
HEADER_SIZE = 4
def recv_all(conn, size):
buffer = b""
while len(buffer) < size:
data = conn.recv(size - len(buffer))
if not data:
return None
buffer += data
return buffer
# 连续发送多条消息,用来复现粘包
msgs = ["hello 1", "hello 2", "hello 3"]
for msg in msgs:
body = msg.encode("utf-8")
# 打包:4字节长度头 + 消息体
header = struct.pack("!I", len(body))
client_socket.sendall(header + body)
# 循环接收服务端回复
for _ in msgs:
header = recv_all(client_socket, HEADER_SIZE)
if not header:
break
body_len = struct.unpack("!I", header)[0]
body = recv_all(client_socket, body_len)
print("收到服务端回复:", body.decode("utf-8"))
client_socket.close()
运行说明
- 先启动
tcp_server_fix.py - 再运行
tcp_client_fix.py - 客户端一次性连续 send 3 条消息。原生 TCP 代码会出现粘包,而这套带长度头方案可以正确拆分 3 条独立消息
原理说明:
struct.pack("!I", len(body)):!使用网络字节序(跨平台兼容),I无符号 int,固定 4 字节。recv_all是关键辅助函数,保证读够指定字节才返回,避免半包问题。
另外两种方案极简示例(可选)
方案 2:分隔符方案(换行符 \n)
适合文本协议,缺点:消息内容里不能出现分隔符
python
# 接收端伪代码,按换行分割
data = conn.recv(1024)
buffer += data
while b"\n" in buffer:
line, buffer = buffer.split(b"\n", 1)
print(line.decode())
方案 1:固定长度方案
每条消息固定 20 字节,不足补空格
python
msg = "test".ljust(20).encode("utf-8")
conn.sendall(msg)
# 接收端直接recv(20)读取一条完整消息
粘包小节小结:
- 粘包本质:TCP 是字节流,无边界;不是数据包粘在一起,是缓冲区数据连续
- 工程首选:长度头 + 消息体,通用性最强,二进制协议常用(RPC、游戏、物联网)
- 文本类简单场景:分隔符方案(如 HTTP、Redis 协议)
- 不推荐:单纯靠加大 recv 缓冲区,治标不治本,高负载下依然会出现粘包 / 半包
5.2 UDP 丢包问题
UDP 本身传输层不提供重传、序号、确认应答 ,所以丢包、乱序、重复包都可能发生。 解决思路一句话:UDP 只负责把包发出去;可靠性逻辑,全部要在应用层自己实现。
核心问题拆解
- 丢包:数据包在网络路由 / 交换机缓冲区溢出被丢弃,接收方完全收不到。
- 乱序:多个数据包走不同路由,后发送的包先到达接收端。
- 重复包:网络链路产生复制报文,接收端收到两份一模一样的包。
注意:不要尝试在 UDP 上复刻完整 TCP(会丧失 UDP 低延迟优势),业务按需做最小化可靠机制。
方案 1:序列号 + 接收缓冲区(解决乱序、重复包)
- 每个 UDP 报文头部增加自增序列号 seq;
- 接收端维护一个接收窗口(buffer),收到包先放到缓冲区,按 seq 排序;
- 记录已经连续收到的最大序号,连续区间内的数据向上层交付;乱序包先缓存,等待缺失包补齐;
- 增加唯一会话 ID,过滤不同连接的数据包;
- 增加校验和(CRC32/MD5),检测数据包损坏。
去重逻辑:记录最近收到的若干 seq,收到重复 seq 直接丢弃。
方案 2:ACK 确认应答 + 超时重传(解决丢包)
- 发送端发送报文,记录发送时间、seq;
- 接收端收到合法报文,回复携带
seq的 ACK 包; - 发送端等待 ACK,如果超过超时时间没有收到 ACK,重传该报文;
- 优化:滑动窗口,不用等一个 ACK 再发下一包,支持批量发送。
⚠️ 坑点:
- ACK 本身也会丢,所以重传会带来重复包,必须配合 seq 去重;
- 超时时间不能写死,最好动态 RTT(往返时间)计算,网络抖动时自动调整。
方案 3:选择性重传 SACK(优化重传效率)
基础超时重传:丢一个包,后面所有包都要重传。 SACK(Selective ACK):接收方告诉发送端哪些 seq 收到、哪些缺失 ,发送端只重传丢失的包。 适合大量连续报文传输(文件、大段流数据),游戏、直播常用。
方案 4:FEC 前向纠错(适合实时流,不重传)
不做重传,发送时额外携带冗余校验包。 例如每发 N 个业务包,附带 1 个冗余包;只要丢失数量小于冗余度,接收端可直接还原数据。
适用场景:直播、语音、实时游戏。优点:无等待延迟;缺点:增加带宽开销,丢包过多依然失效。
方案 5:QUIC 协议(成熟工业方案)
QUIC 底层就是 UDP,在 UDP 之上内置:
- 序列号、ACK、SACK、重传
- 连接 ID、加密
- 多路复用,解决 TCP 队头阻塞
如果你不想从零手写一套可靠 UDP,优先直接使用 QUIC,而不是自己造轮子。HTTP/3 底层就是 QUIC。
取舍:什么时候不需要全部实现?
- 物联网心跳上报:允许少量丢包,完全不用重传,丢了下一次上报自动覆盖旧数据。
- 实时语音 / 直播:优先低延迟,丢包用 FEC,不做重传(重传带来延迟比丢包更难受)。
- 文件传输、指令下发:必须可靠,需要 seq + ACK + 超时重传。
极简伪代码示例(最小可靠 UDP 框架)
python
# 简化示意:应用层增加seq、ACK、基础超时重传
import socket
import time
import struct
# 报文头部:seq(4字节) + data_len(4字节)
def make_packet(seq, payload: bytes):
header = struct.pack("!II", seq, len(payload))
return header + payload
def parse_packet(raw: bytes):
seq, length = struct.unpack("!II", raw[:8])
payload = raw[8:8+length]
return seq, payload
# 发送端简易逻辑
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
seq = 1
timeout = 0.3
while True:
pkt = make_packet(seq, b"hello udp")
sock.sendto(pkt, ("127.0.0.1", 10087))
start = time.time()
# 等待ACK
sock.settimeout(timeout)
try:
ack_raw, _ = sock.recvfrom(1024)
ack_seq, _ = parse_packet(ack_raw)
if ack_seq == seq:
print(f"收到ACK seq={seq},发送成功")
seq += 1
continue
except socket.timeout:
print(f"超时,重传 seq={seq}")
# 超时自动循环重传
重要避坑总结
- 不要盲目把 UDP 做成 TCP 复刻:丢失 UDP 低延迟优势;
- 重传必然引入重复包,序列号去重是刚需;
- 乱序缓存不能无限大,要设置缓存上限,防止内存溢出;
- UDP 有单包大小限制:MTU,大包要做分片,否则底层直接丢包;
- 自己实现可靠 UDP 很容易踩坑,生产环境优先 QUIC,不建议手写完整协议。
5.3 端口占用问题
运行代码提示端口占用时,更换端口号,或设置端口复用:
python
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
六、总结
- TCP 优先用在对数据完整性要求高的场景:如接口通信、文件上传下载、系统交互,依靠连接机制保证数据可靠。
- UDP 优先用在对速度要求高、可容忍少量丢包的场景:如实时音视频、游戏对局、设备心跳上报。
- Python 网络编程核心就是
socket模块,TCP 核心为SOCK_STREAM,UDP 核心为SOCK_DGRAM,掌握本文示例即可应对绝大多数基础网络通信需求。
七、拓展互动思考题(附详细解析)
为帮助大家深度理解 TCP/UDP 底层差异,这里补充三个高频面试、实操核心问题,搭配通俗解析和底层原理。
思考题 1:什么是 TCP 半包?为什么必须用 recv_all 函数解决?
半包定义 :发送方已经发送了一条完整的数据包,但接收方一次 recv() 读取的字节数小于数据包总长度,导致只读到了消息的一部分数据,剩余数据留在系统缓冲区中,这就是半包问题。
半包产生原因:
- 系统缓冲区大小限制:
recv(1024)单次最大读取 1024 字节,若数据包大于 1024 字节,必然产生半包; - 网络分片传输:网络拥堵、路由分片会导致一个完整数据包被拆分多次传输;
- 内核调度机制:操作系统不会保证一次 recv 读取完整数据包,只会读取当前缓冲区已就绪的数据。
半包与粘包的区别与关联:
- 粘包:一次读到多条完整数据(数据多读、合并)
- 半包:一次读到半条不完整数据(数据少读、缺失)
二者常常同时出现,是 TCP 字节流特性衍生的两大核心问题。
recv_all 解决原理 :普通 recv() 是 "读多少算多少",而 recv_all 是循环读取缓冲区,直到凑够指定字节数才返回结果,彻底规避半包问题,配合长度头机制即可完美解析完整数据包。
思考题 2:为什么 UDP 完全不会出现粘包、半包问题?
核心原因:TCP 是字节流协议,无数据边界;UDP 是数据报协议,自带完整数据边界。
底层传输机制差异解析:
- 发送机制不同 UDP 每次调用
sendto(),系统都会封装成独立的 UDP 数据报 ,添加报头、独立发包,多条消息之间完全隔离,不会被内核合并;而 TCP 多次send()的数据会被内核缓冲区合并、缓存,无任何分隔。 - 接收机制不同 UDP 的
recvfrom()遵循一报一收原则:一次调用只会接收一个完整的 UDP 数据报,不会拆分、不会合并。哪怕缓冲区设置 1024 字节,发送的单条数据只有 100 字节,也只会收到这 100 字节的完整消息,剩余缓冲区不会读取多余数据;绝不会出现半条消息、多条消息合并的情况。 - 边界保障机制 UDP 协议本身保留了应用层的数据边界,操作系统内核不会对 UDP 数据进行拼接、拆分处理,应用层发多少、收多少,一一对应。
补充注意点 :UDP 虽然没有粘包半包,但存在丢包、乱序问题,这是它为高速传输牺牲可靠性的代价,和 TCP 的优缺点完全互补。
思考题 3:同样是基于 UDP 实现可靠传输,为什么游戏实时通信一般不用完整重传,而是使用 FEC 前向纠错?
答案解析: 游戏实时对战(FPS、MOBA 等)的核心诉求是极低端到端延迟,这是优先级最高的指标。
如果采用 ACK + 超时重传方案:
- 一旦某个数据包丢失,接收端需要等待超时,发送端再重传该包;这个等待 + 重传的时间会带来明显延迟。游戏状态是持续滚动更新的,旧包等重传回来时,画面、角色状态已经过时,就算收到也没有意义;
- 重传会增加网络流量,网络拥堵时会加重丢包,形成恶性循环;
- 游戏的位置、动作状态属于流式实时数据,旧数据可丢弃,只需要最新状态。
而 FEC 前向纠错思路是先发冗余包,不等待重传 :发送端在正常业务数据包之外,附带少量冗余校验包。只要丢失的包数量在冗余能力范围内,接收端可以直接通过剩余数据包还原丢失数据,不需要等待、不需要重传,没有额外等待延迟。
当然 FEC 有代价:冗余包会占用额外带宽;当丢包率超过冗余能力上限,依然无法恢复数据。
一句话总结:游戏实时场景宁可丢少量包,也不能接受重传带来的延迟,FEC 用带宽换低延迟,刚好匹配这类业务。