Python 网络编程详解:TCP 与 UDP 原理 + 完整实战示例

在 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 是服务端被动监听、客户端主动连接的模式:

  1. 服务端:创建 Socket → 绑定 IP 端口 → 监听连接 → 接收客户端连接 → 收发数据
  2. 客户端:创建 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()

运行效果

  1. 先运行 tcp_server.py,服务端启动并等待连接
  2. 再运行 tcp_client.py,客户端自动连接服务端
  3. 客户端输入任意内容,服务端实时接收并回复,输入 exit 即可退出

三、UDP 协议:高效的无连接通信

3.1 UDP 核心特性

UDP(用户数据报协议,User Datagram Protocol)是无连接、不可靠、快速的传输协议,专注于高速传输,牺牲了部分可靠性。

  • 无连接:无需建立连接,直接发送数据,不需要握手挥手
  • 不可靠:无确认机制,数据可能丢失、乱序、重复
  • 数据报传输:数据有明确边界,一次发送对应一次接收
  • 开销极低:协议头部小,传输速度快、延迟低
  • 支持广播:可一对多群发数据(TCP 不支持)

3.2 UDP 通信流程

UDP 无严格的服务端客户端绑定关系,双方地位平等,任意一方都可以主动收发数据:

  1. 服务端:创建 UDP 套接字 → 绑定端口 → 循环接收数据、回复数据
  2. 客户端:创建 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()

运行效果

  1. 先启动 UDP 服务端,再启动客户端
  2. 客户端随时发送消息,服务端实时响应
  3. 核心区别:即使关闭客户端,服务端不会报错;重启客户端后可直接通信,无需重新连接

四、TCP 与 UDP 核心区别对比

对比维度 TCP UDP
连接特性 面向连接(三次握手 / 四次挥手) 无连接,无需建立通信链路
可靠性 可靠,无丢失、无乱序、无重复 不可靠,可能丢包、乱序
传输方式 字节流传输,无数据边界 数据报传输,有明确数据边界
速度延迟 速度较慢,延迟较高 速度极快,延迟极低
开销大小 协议开销大 协议开销极小
适用场景 文件传输、网页访问、登录通信(需保证数据完整) 直播、游戏、语音视频、物联网上报(容忍少量丢包,追求速度)

五、常见踩坑问题总结

5.1 TCP 粘包问题

TCP 是字节流协议 ,没有数据包边界。操作系统内核有缓冲区:发送方会把多次小数据缓存合并再发送;接收方缓冲区会积攒数据,recv()一次读取缓冲区里所有可用字节。 于是出现两种粘包场景:

  1. 发送粘包:连续多次 send,内核合并成一个数据包发出
  2. 接收粘包:一次 recv 读到多条消息,分不清消息起止

⚠️ 粘包不是 bug,是 TCP 正常特性;UDP 不会粘包(数据报自带边界)。

常见解决方案

  1. 固定长度报文:每条消息固定字节,不足补填充字符(简单,浪费带宽)
  2. 特殊分隔符 :消息末尾加换行 /\r\n,适合文本协议(HTTP 就是这种)
  3. 消息头 + 消息体(推荐):头部存放消息体长度,接收端先读固定长度头部,解析出 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()
运行说明
  1. 先启动 tcp_server_fix.py
  2. 再运行 tcp_client_fix.py
  3. 客户端一次性连续 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 只负责把包发出去;可靠性逻辑,全部要在应用层自己实现。

核心问题拆解

  1. 丢包:数据包在网络路由 / 交换机缓冲区溢出被丢弃,接收方完全收不到。
  2. 乱序:多个数据包走不同路由,后发送的包先到达接收端。
  3. 重复包:网络链路产生复制报文,接收端收到两份一模一样的包。

注意:不要尝试在 UDP 上复刻完整 TCP(会丧失 UDP 低延迟优势),业务按需做最小化可靠机制。

方案 1:序列号 + 接收缓冲区(解决乱序、重复包)

  • 每个 UDP 报文头部增加自增序列号 seq;
  • 接收端维护一个接收窗口(buffer),收到包先放到缓冲区,按 seq 排序;
  • 记录已经连续收到的最大序号,连续区间内的数据向上层交付;乱序包先缓存,等待缺失包补齐;
  • 增加唯一会话 ID,过滤不同连接的数据包;
  • 增加校验和(CRC32/MD5),检测数据包损坏。

去重逻辑:记录最近收到的若干 seq,收到重复 seq 直接丢弃。

方案 2:ACK 确认应答 + 超时重传(解决丢包)

  1. 发送端发送报文,记录发送时间、seq;
  2. 接收端收到合法报文,回复携带seq的 ACK 包;
  3. 发送端等待 ACK,如果超过超时时间没有收到 ACK,重传该报文;
  4. 优化:滑动窗口,不用等一个 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}")
    # 超时自动循环重传

重要避坑总结

  1. 不要盲目把 UDP 做成 TCP 复刻:丢失 UDP 低延迟优势;
  2. 重传必然引入重复包,序列号去重是刚需;
  3. 乱序缓存不能无限大,要设置缓存上限,防止内存溢出;
  4. UDP 有单包大小限制:MTU,大包要做分片,否则底层直接丢包;
  5. 自己实现可靠 UDP 很容易踩坑,生产环境优先 QUIC,不建议手写完整协议。

5.3 端口占用问题

运行代码提示端口占用时,更换端口号,或设置端口复用:

python 复制代码
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

六、总结

  1. TCP 优先用在对数据完整性要求高的场景:如接口通信、文件上传下载、系统交互,依靠连接机制保证数据可靠。
  2. UDP 优先用在对速度要求高、可容忍少量丢包的场景:如实时音视频、游戏对局、设备心跳上报。
  3. Python 网络编程核心就是 socket 模块,TCP 核心为 SOCK_STREAM,UDP 核心为 SOCK_DGRAM,掌握本文示例即可应对绝大多数基础网络通信需求。

七、拓展互动思考题(附详细解析)

为帮助大家深度理解 TCP/UDP 底层差异,这里补充三个高频面试、实操核心问题,搭配通俗解析和底层原理。

思考题 1:什么是 TCP 半包?为什么必须用 recv_all 函数解决?

半包定义 :发送方已经发送了一条完整的数据包,但接收方一次 recv() 读取的字节数小于数据包总长度,导致只读到了消息的一部分数据,剩余数据留在系统缓冲区中,这就是半包问题。

半包产生原因:

  1. 系统缓冲区大小限制:recv(1024) 单次最大读取 1024 字节,若数据包大于 1024 字节,必然产生半包;
  2. 网络分片传输:网络拥堵、路由分片会导致一个完整数据包被拆分多次传输;
  3. 内核调度机制:操作系统不会保证一次 recv 读取完整数据包,只会读取当前缓冲区已就绪的数据。

半包与粘包的区别与关联:

  • 粘包:一次读到多条完整数据(数据多读、合并)
  • 半包:一次读到半条不完整数据(数据少读、缺失)

二者常常同时出现,是 TCP 字节流特性衍生的两大核心问题。

recv_all 解决原理 :普通 recv() 是 "读多少算多少",而 recv_all 是循环读取缓冲区,直到凑够指定字节数才返回结果,彻底规避半包问题,配合长度头机制即可完美解析完整数据包。

思考题 2:为什么 UDP 完全不会出现粘包、半包问题?

核心原因:TCP 是字节流协议,无数据边界;UDP 是数据报协议,自带完整数据边界。

底层传输机制差异解析:

  1. 发送机制不同 UDP 每次调用 sendto(),系统都会封装成独立的 UDP 数据报 ,添加报头、独立发包,多条消息之间完全隔离,不会被内核合并;而 TCP 多次 send() 的数据会被内核缓冲区合并、缓存,无任何分隔。
  2. 接收机制不同 UDP 的 recvfrom() 遵循一报一收原则:一次调用只会接收一个完整的 UDP 数据报,不会拆分、不会合并。哪怕缓冲区设置 1024 字节,发送的单条数据只有 100 字节,也只会收到这 100 字节的完整消息,剩余缓冲区不会读取多余数据;绝不会出现半条消息、多条消息合并的情况。
  3. 边界保障机制 UDP 协议本身保留了应用层的数据边界,操作系统内核不会对 UDP 数据进行拼接、拆分处理,应用层发多少、收多少,一一对应。

补充注意点 :UDP 虽然没有粘包半包,但存在丢包、乱序问题,这是它为高速传输牺牲可靠性的代价,和 TCP 的优缺点完全互补。

思考题 3:同样是基于 UDP 实现可靠传输,为什么游戏实时通信一般不用完整重传,而是使用 FEC 前向纠错?

答案解析: 游戏实时对战(FPS、MOBA 等)的核心诉求是极低端到端延迟,这是优先级最高的指标。

如果采用 ACK + 超时重传方案:

  1. 一旦某个数据包丢失,接收端需要等待超时,发送端再重传该包;这个等待 + 重传的时间会带来明显延迟。游戏状态是持续滚动更新的,旧包等重传回来时,画面、角色状态已经过时,就算收到也没有意义;
  2. 重传会增加网络流量,网络拥堵时会加重丢包,形成恶性循环;
  3. 游戏的位置、动作状态属于流式实时数据,旧数据可丢弃,只需要最新状态。

而 FEC 前向纠错思路是先发冗余包,不等待重传 :发送端在正常业务数据包之外,附带少量冗余校验包。只要丢失的包数量在冗余能力范围内,接收端可以直接通过剩余数据包还原丢失数据,不需要等待、不需要重传,没有额外等待延迟。

当然 FEC 有代价:冗余包会占用额外带宽;当丢包率超过冗余能力上限,依然无法恢复数据。

一句话总结:游戏实时场景宁可丢少量包,也不能接受重传带来的延迟,FEC 用带宽换低延迟,刚好匹配这类业务。

相关推荐
一帅1 小时前
VirtualField:给别人的类"缝口袋"的全过程
后端
后端LV1 小时前
把限流从「注解」做活:RateLimitKeyResolver 四种真实业务的键玩法
java·后端
一帅1 小时前
InDy Advice:用一行 invokedynamic,把 Agent 藏进"平行宇宙"
后端
inhere1 小时前
miglite v0.8.0:迁移文件可以嵌进二进制了
后端
geovindu1 小时前
rust: tree
开发语言·后端·rust
imDwAaY1 小时前
Bean的生命周期
java·笔记·后端·学习·spring·dubbo
上下求索,莫负韶华1 小时前
Spring全家桶
java·后端·spring
行百里er1 小时前
Redis 性能优化——内存、慢查询、Big Key 与 Hot Key
redis·后端
打工仔折腾 AI2 小时前
Prometheus接入Pushgateway实战:二进制与Docker部署、指标推送与远程写入
后端·python·docker·容器·性能优化·prometheus·ai agent 实战