news 2026/9/30 4:58:28

Python Socket 编程实战:TCP 与 UDP 实现、粘包处理与排错指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python Socket 编程实战:TCP 与 UDP 实现、粘包处理与排错指南

简介:这份资源是面向具备一定Python基础与网络知识的学习者、程序员的教学实验文档,围绕传输层TCP与UDP的Socket编程展开,帮助读者在PyCharm环境中动手实现两种协议的基本通信功能,解决从理论到代码落地的实践需求。资源包共1个PDF文件,大小约735KB,内容以实验步骤与代码示例为主,涵盖UDP套接字发送接收数据、超时设置、丢包模拟与往返时间统计,以及TCP客户端与服务端的创建、连接、收发数据与关闭流程。读者可跟随文档逐步完成Ping客户端与服务器实验,理解UDP无连接、不可靠特性与TCP面向连接、可靠传输的差异,并掌握套接字超时处理、数据编解码等实用技巧。目前已有170人学习,适合教学或自学场景下对照实验代码加深对传输层协议应用的理解。

1. 从一次“连接超时”说起:TCP 与 UDP 的 Socket 编程到底在解决什么

线上服务突然报connection timed out,你登录机器用ss -antp看到一堆SYN-SENT,而隔壁日志里 UDP 采集端却在安静地丢包——同一个网络里,TCP 和 UDP 的表现像两个物种。TCP 与 UDP Socket 编程的 Python 实现,讲的不是“背下三次握手四次挥手”,而是让你在 Python 里亲手建立、维护、关闭这两类连接,理解它们各自在什么场景下会翻车。TCP 面向连接、可靠、有序,适合文件传输、数据库连接、HTTP 这类不能丢数据的场景;UDP 无连接、轻量、不保证到达,适合实时音视频、DNS 查询、游戏状态同步、iperf3打流测试。Python 的socket模块把 BSD Socket 接口封装成几个方法,新手能几行代码跑通,熟手却要面对粘包、半包、端口复用、ConnectionResetError这些血泪经验。这篇文章面向已经会写 Python、但被 Socket 细节卡住的一线开发者,从最小可运行代码讲到参数调优和排错,让你能照着复现,也能判断边界。

2. TCP Socket 的 Python 实现:从三次握手到粘包处理

2.1 TCP 服务端与客户端的最小可运行代码

先跑通一个能收发一行文本的 TCP 回显服务,再谈优化。服务端绑定127.0.0.1:9000,客户端连接后发送一条消息,服务端原样返回。这段代码是后面所有讨论的基线。

# tcp_echo_server.py import socket HOST = "127.0.0.1" PORT = 9000 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv: srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 避免 TIME_WAIT 导致端口占用 srv.bind((HOST, PORT)) srv.listen(5) # 半连接队列长度,不是最大并发数 print(f"listening on {HOST}:{PORT}") conn, addr = srv.accept() # 阻塞直到有客户端完成三次握手 with conn: print(f"connected by {addr}") while True: data = conn.recv(1024) # 一次最多读 1024 字节,不保证读满 if not data: break # 对端关闭连接,recv 返回空 bytes conn.sendall(data) # sendall 内部循环直到发完
# tcp_echo_client.py import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as cli: cli.connect(("127.0.0.1", 9000)) # 触发三次握手 cli.sendall(b"hello tcp\n") resp = cli.recv(1024) print("received:", resp)

逻辑说明:socket.socket(AF_INET, SOCK_STREAM)创建的是 TCP 套接字,SOCK_STREAM对应字节流语义。bind把套接字绑定到本地地址,listen把主动套接字转为被动套接字并设置半连接队列。accept返回一个新的已连接套接字conn,原srv继续监听。客户端connect触发三次握手,内核完成 SYN、SYN-ACK、ACK 后accept才返回。

参数说明:SO_REUSEADDR让服务端在重启时能立刻复用处于TIME_WAIT的端口,生产环境几乎必开。listen(5)的 5 是 backlog,Linux 上实际生效值受somaxconn限制,高并发场景要同时调大。recv(1024)的 1024 是单次读取上限,不是消息边界,这是后面粘包问题的根源。

2.2 粘包与半包:为什么 recv 一次读不完整

TCP 是字节流,没有消息边界。客户端连续sendall两次,服务端可能一次recv全读到,也可能分两次读到半个。热词里“为什么 socket 接收到奇数字节后面会补一个随机数”这类困惑,本质是把 TCP 当成了消息队列。解决方式只有三种:固定长度、分隔符、长度前缀。长度前缀最通用,下面用struct打包 4 字节大端长度。

# 发送端:长度前缀 + 消息体 import socket, struct def send_msg(sock, payload: bytes): header = struct.pack("!I", len(payload)) # ! 表示网络字节序,I 是 4 字节无符号整数 sock.sendall(header + payload) # 接收端:先读满 4 字节头,再按长度读满消息体 def recv_exact(sock, n: int) -> bytes: buf = b"" while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise ConnectionError("peer closed") buf += chunk return buf def recv_msg(sock) -> bytes: header = recv_exact(sock, 4) (length,) = struct.unpack("!I", header) return recv_exact(sock, length)

逻辑说明:send_msg把长度和内容拼成一个bytes交给sendall,sendall保证全部写入内核发送缓冲区。recv_exact循环读取直到凑够n字节,这是处理半包的标准写法。recv_msg先读 4 字节头解析出长度,再读消息体。

参数说明:struct.pack("!I", ...)的!是网络字节序(大端),跨平台通信必须统一,否则 x86 小端机器和网络设备之间会解析出错误长度。长度字段用 4 字节可支持单条消息最大 4GB,实际业务里通常限制在几 MB 以内,超过要分片。recv的缓冲区大小建议设为 4096 或 8192,太小系统调用频繁,太大浪费内存。

2.3 连接生命周期:四次挥手、TIME_WAIT 与优雅关闭

TCP 断开需要四次挥手,主动关闭方会进入TIME_WAIT状态并持续 2MSL(Linux 默认 60 秒)。高并发短连接服务如果由服务端主动关闭,会积累大量TIME_WAIT,耗尽本地端口。常见做法是让客户端主动关闭,服务端设置SO_REUSEADDR,并在应用层用shutdown半关闭再close。

# 优雅关闭:先关写方向,等对端读完再关读方向 conn.shutdown(socket.SHUT_WR) # 发送 FIN,不再发送数据 while True: data = conn.recv(1024) if not data: break conn.close()

逻辑说明:shutdown(SHUT_WR)只关闭写方向,对端recv会收到空bytes知道数据结束,但仍可回传剩余数据。直接close会同时关闭读写,可能导致对端收到RST而丢失未读数据。

参数说明:SHUT_WR、SHUT_RD、SHUT_RDWR分别关闭写、读、读写方向。TIME_WAIT无法消除,只能通过net.ipv4.tcp_tw_reuse允许复用(仅对客户端主动连接生效),或改用长连接减少关闭次数。注意不要在生产环境随意开tcp_tw_recycle,它在 NAT 环境下会导致连接异常。

3. UDP Socket 的 Python 实现:无连接、丢包与数据报边界

3.1 UDP 服务端与客户端的最小可运行代码

UDP 没有握手,sendto直接发出,recvfrom一次收一个完整数据报。下面实现一个 UDP 时间服务,客户端发任意内容,服务端返回当前时间。

# udp_time_server.py import socket, time with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as srv: srv.bind(("0.0.0.0", 9001)) print("udp server on 9001") while True: data, addr = srv.recvfrom(2048) # 一次收一个数据报,最多 2048 字节 print(f"from {addr}: {data!r}") srv.sendto(str(time.time()).encode(), addr)
# udp_time_client.py import socket with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as cli: cli.settimeout(2.0) # UDP 不保证到达,必须自己设超时 cli.sendto(b"time?", ("127.0.0.1", 9001)) try: data, addr = cli.recvfrom(2048) print("server time:", data.decode()) except socket.timeout: print("no response, packet likely lost")

逻辑说明:SOCK_DGRAM创建 UDP 套接字,bind后即可recvfrom。recvfrom返回数据和发送方地址,一次调用对应一个数据报,不会出现 TCP 那种粘包。客户端sendto后必须自己设超时,因为 UDP 没有重传机制,丢包时recvfrom会一直阻塞。

参数说明:recvfrom(2048)的 2048 是单次接收缓冲区上限,超过部分在 Linux 上会被截断并丢弃。UDP 数据报理论最大 65507 字节(65535 减去 IP 头 20 和 UDP 头 8),但实际受 MTU 限制,超过 1472 字节(以太网 MTU 1500 减去 IP 20 和 UDP 8)会触发 IP 分片,任一分片丢失整个数据报就废了。settimeout(2.0)是应用层超时,不是协议层。

3.2 UDP 数据报边界与 IP 分片:为什么大包容易丢

UDP 保留消息边界,sendto一次发 3000 字节,recvfrom要么收到完整 3000 字节,要么什么都收不到,不会收到 1500 字节。但 3000 字节超过 MTU,IP 层会分成两个分片,接收端要等两个分片都到齐才能重组。任何一个分片在途中丢失,整个数据报被丢弃,且接收端不会通知发送端。热词里“udp 划分 ip 数据报片”说的就是这个过程。

# 探测路径 MTU:发送不同大小的 UDP 包,观察是否收到回复 import socket def probe_mtu(host, port, size): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.settimeout(1.0) s.sendto(b"x" * size, (host, port)) try: s.recvfrom(2048) return True except socket.timeout: return False for size in (500, 1000, 1400, 1472, 1473, 2000): ok = probe_mtu("127.0.0.1", 9001, size) print(f"size={size}: {'ok' if ok else 'lost'}")

逻辑说明:向 UDP 服务端发送不同大小的包,服务端回显则说明该大小能通过。1472 是常见安全上限,1473 开始可能分片。本地回环 MTU 通常 65536,测不出分片,要在真实网络里测。

参数说明:size是 UDP 载荷字节数,不含 IP 和 UDP 头。生产环境建议应用层把消息控制在 1200 字节以内,给隧道封装留余量。如果必须发大包,要么在应用层分片并自己实现确认重传,要么改用 TCP。

3.3 UDP 的适用边界:什么时候不该用 UDP

UDP 适合能容忍丢包、对延迟敏感、或者自己实现可靠性的场景。DNS 查询用 UDP 因为请求响应小且快;实时音视频用 UDP 因为重传旧帧没意义;iperf3用 UDP 打流测丢包率。但文件传输、数据库连接、HTTP API 这些不能丢数据的场景,用 UDP 就是自找麻烦。常见误区是“UDP 快所以都用 UDP”,实际上 UDP 省的是握手和重传开销,如果应用层自己实现确认重传,复杂度比 TCP 高得多,性能未必更好。

# UDP 打流测试:统计发送与接收数量,计算丢包率 import socket, time def udp_flood(host, port, count=1000, size=1024): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 << 20) # 发送缓冲区 1MB payload = b"x" * size for i in range(count): s.sendto(payload, (host, port)) print(f"sent {count} packets of {size} bytes")

逻辑说明:连续发送 1000 个 1024 字节的 UDP 包,接收端统计实际收到多少。SO_SNDBUF调大发送缓冲区,减少sendto阻塞。注意 UDP 发送缓冲区满时sendto会阻塞或报错,取决于套接字是否非阻塞。

参数说明:SO_SNDBUF和SO_RCVBUF是内核缓冲区大小,Linux 上实际值会是设置值的两倍,且有上限net.core.wmem_max。打流测试时接收端要尽快recvfrom,否则缓冲区满后新包被丢弃。丢包率 = (发送数 - 接收数) / 发送数,但要注意发送端统计的是“交给内核”的数量,不是“发出网卡”的数量。

4. 避坑与排查:TCP/UDP Socket 编程的 5 个常见翻车现场

4.1 现象:服务端重启报Address already in use

原因:上一次进程关闭后,监听端口处于TIME_WAIT状态,内核默认不允许新套接字绑定。解决:服务端bind前设置SO_REUSEADDR,代码见 2.1。如果还不行,检查是否有其他进程占用端口,用ss -antp | grep 9000确认。注意SO_REUSEPORT是另一个选项,允许多个进程绑定同一端口做负载均衡,但行为与SO_REUSEADDR不同,不要混用。

4.2 现象:客户端recv返回空bytes,但连接没断

原因:对端调用了shutdown(SHUT_WR)或close,TCP 收到 FIN,recv返回空表示对端不再发送数据。解决:应用层要区分“对端关闭写方向”和“连接出错”。收到空bytes后,如果还要回传数据,可以继续sendall;如果对端已close,sendall会触发BrokenPipeError。常见错误是收到空bytes就break退出循环,导致没处理对端可能还在等回复的情况。

4.3 现象:UDP 服务端recvfrom收到ConnectionResetError

原因:UDP 是无连接协议,但如果客户端sendto后关闭了套接字,服务端回包时内核会返回 ICMP 端口不可达,下一次recvfrom或sendto可能抛出ConnectionResetError(Windows 上常见[WinError 10054])。解决:UDP 服务端要捕获这个异常并继续循环,不要让它终止服务。更稳妥的做法是忽略 ICMP 错误,或者用setsockopt关闭SIO_UDP_CONNRESET(仅 Windows)。

# UDP 服务端容错:捕获 ConnectionResetError 继续服务 import socket with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as srv: srv.bind(("0.0.0.0", 9001)) while True: try: data, addr = srv.recvfrom(2048) srv.sendto(b"ack", addr) except ConnectionResetError: continue # 客户端已关闭,忽略 ICMP 错误

4.4 现象:TCP 客户端connect超时,但服务端ss看不到连接

原因:客户端发出的 SYN 没到达服务端,可能是防火墙丢包、路由不通、服务端 backlog 满。解决:先用ping和traceroute确认网络可达,再用telnet host port或nc -vz host port测试端口。如果服务端ss -lnt显示Recv-Q接近Send-Q,说明 accept 队列满,要调大listen的 backlog 并加快accept速度。注意connect超时时间由内核net.ipv4.tcp_syn_retries控制,默认 6 次约 127 秒,应用层可以设settimeout提前放弃。

4.5 现象:UDP 发送大包时sendto返回Message too long

原因:UDP 载荷超过内核允许的最大值,或超过路径 MTU 且设置了IP_MTU_DISCOVER为IP_PMTUDISC_DO(禁止分片)。解决:把消息控制在 1472 字节以内,或在应用层分片。Linux 上可以用setsockopt(IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_DONT)允许分片,但分片会降低可靠性。更推荐的做法是应用层自己分片并加序号,接收端重组。

5. 进阶技巧:用selectors同时管 TCP 和 UDP,以及一个验证习惯

单线程同时处理多个 TCP 连接和 UDP 数据报,最稳的写法是selectors模块,它封装了select、poll、epoll,跨平台。下面这个例子同时监听一个 TCP 端口和一个 UDP 端口,TCP 回显,UDP 返回时间。

import selectors, socket, time sel = selectors.DefaultSelector() def accept_tcp(sock): conn, addr = sock.accept() conn.setblocking(False) sel.register(conn, selectors.EVENT_READ, handle_tcp) def handle_tcp(conn): data = conn.recv(1024) if data: conn.sendall(data) else: sel.unregister(conn) conn.close() def handle_udp(sock): data, addr = sock.recvfrom(2048) sock.sendto(str(time.time()).encode(), addr) tcp_srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_srv.bind(("0.0.0.0", 9000)) tcp_srv.listen(5) tcp_srv.setblocking(False) sel.register(tcp_srv, selectors.EVENT_READ, accept_tcp) udp_srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_srv.bind(("0.0.0.0", 9001)) udp_srv.setblocking(False) sel.register(udp_srv, selectors.EVENT_READ, handle_udp) while True: events = sel.select(timeout=1.0) for key, mask in events: callback = key.data callback(key.fileobj)

逻辑说明:sel.register把套接字和回调函数绑定,sel.select阻塞直到有事件就绪,返回就绪的套接字列表。TCP 监听套接字就绪时调用accept_tcp,已连接套接字就绪时调用handle_tcp,UDP 套接字就绪时调用handle_udp。所有套接字设为非阻塞,避免单个recv卡住整个事件循环。

参数说明:sel.select(timeout=1.0)的 1 秒超时让循环有机会处理其他逻辑,比如定时任务。DefaultSelector在 Linux 上自动选epoll,在 macOS 上选kqueue,Windows 上选select。注意select有 1024 文件描述符限制,高并发要用epoll或kqueue,DefaultSelector已经帮你选好了。

验证习惯:我每次写完 Socket 代码,先用127.0.0.1跑通,再用nc或telnet手动发数据确认边界,最后用tcpdump抓包看实际字节流。TCP 重点看recv返回的字节数是否符合预期,UDP 重点看sendto的包大小是否超过 1472。这个习惯帮我省下了大量“明明代码没错但就是不通”的排查时间。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 4:58:21

STAR-RIS辅助NOMA联合优化:PSO算法实战与避坑指南

简介&#xff1a;该资源面向通信工程领域的研究人员、高校教师与研究生&#xff0c;聚焦基于粒子群优化&#xff08;PSO&#xff09;的STAR-RIS辅助NOMA无线通信系统优化问题。STAR-RIS可同时反射与传输信号&#xff0c;结合NOMA能提升覆盖范围、服务用户数与频谱效率&#xff…

作者头像 李华
网站建设 2026/9/30 4:58:18

海空小目标识别:从实验室到实战的军事应用与验证

1. 项目概述&#xff1a;小目标识别不是“看得见”&#xff0c;而是“看得懂、判得准、跟得稳”“海空小目标识别”这六个字&#xff0c;表面看是雷达或光电系统里一个技术模块的名称&#xff0c;实则是一条横跨传感器物理极限、信号处理算法边界、战场决策逻辑链条的生死线。我…

作者头像 李华
网站建设 2026/9/30 4:58:15

数字孪生与三维可视化有何区别?落地避坑指南

你接过一个“数字孪生可视化”项目&#xff0c;甲方说得很简单&#xff1a;把我们的制冷站、车间、园区做成一屏看全的三维大屏&#xff0c;最好能打开机房门、看到管道里的水流&#xff0c;温度一变颜色立刻红起来。听起来很“数字孪生”&#xff0c;对不对&#xff1f;但如果…

作者头像 李华
网站建设 2026/9/30 4:58:02

数字垃圾清理指南:缓存、临时文件与重复文件的判断标准

1. 从“清理垃圾”这个动作说起&#xff1a;它到底在解决什么问题第一次看到“当我们需要不停清理垃圾防止世界被污染”这个说法&#xff0c;我脑子里冒出来的不是环保工人&#xff0c;而是我电脑右下角那个永远在转的清理软件图标。后来仔细一想&#xff0c;这两件事本质上是一…

作者头像 李华
网站建设 2026/9/30 4:57:44

基于CYGNSS星载GNSS-R的湖泊水域面积监测方法

简介&#xff1a;一份面向遥感、水资源与环境监测研究者的GNSS-R湖泊监测Python实践资料&#xff0c;针对高时空分辨率湖泊水域面积动态监测需求&#xff0c;提供基于星载GNSS-R技术的完整实现方案。资源包共1个docx文件&#xff0c;大小59KB&#xff0c;内容以函数模块组织&am…

作者头像 李华
网站建设 2026/9/30 4:57:20

STM32嵌入式C++实战:从零编写GPIO类并完成编译仿真闭环

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华