Python Socket 编程实战:TCP 与 UDP 实现、粘包处理与排错指南
发布时间:2026/9/30 4:58:30来源:尧图网络
简介这份资源是面向具备一定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(flistening on {HOST}:{PORT}) conn, addr srv.accept() # 阻塞直到有客户端完成三次握手 with conn: print(fconnected 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(bhello 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 是 backlogLinux 上实际生效值受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交给sendallsendall保证全部写入内核发送缓冲区。recv_exact循环读取直到凑够n字节这是处理半包的标准写法。recv_msg先读 4 字节头解析出长度再读消息体。参数说明struct.pack(!I, ...)的!是网络字节序大端跨平台通信必须统一否则 x86 小端机器和网络设备之间会解析出错误长度。长度字段用 4 字节可支持单条消息最大 4GB实际业务里通常限制在几 MB 以内超过要分片。recv的缓冲区大小建议设为 4096 或 8192太小系统调用频繁太大浪费内存。2.3 连接生命周期四次挥手、TIME_WAIT 与优雅关闭TCP 断开需要四次挥手主动关闭方会进入TIME_WAIT状态并持续 2MSLLinux 默认 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(ffrom {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(btime?, (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 字节超过 MTUIP 层会分成两个分片接收端要等两个分片都到齐才能重组。任何一个分片在途中丢失整个数据报被丢弃且接收端不会通知发送端。热词里“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(bx * 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(fsize{size}: {ok if ok else lost})逻辑说明向 UDP 服务端发送不同大小的包服务端回显则说明该大小能通过。1472 是常见安全上限1473 开始可能分片。本地回环 MTU 通常 65536测不出分片要在真实网络里测。参数说明size是 UDP 载荷字节数不含 IP 和 UDP 头。生产环境建议应用层把消息控制在 1200 字节以内给隧道封装留余量。如果必须发大包要么在应用层分片并自己实现确认重传要么改用 TCP。3.3 UDP 的适用边界什么时候不该用 UDPUDP 适合能容忍丢包、对延迟敏感、或者自己实现可靠性的场景。DNS 查询用 UDP 因为请求响应小且快实时音视频用 UDP 因为重传旧帧没意义iperf3用 UDP 打流测丢包率。但文件传输、数据库连接、HTTP API 这些不能丢数据的场景用 UDP 就是自找麻烦。常见误区是“UDP 快所以都用 UDP”实际上 UDP 省的是握手和重传开销如果应用层自己实现确认重传复杂度比 TCP 高得多性能未必更好。# UDP 打流测试统计发送与接收数量计算丢包率 import socket, time def udp_flood(host, port, count1000, size1024): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 20) # 发送缓冲区 1MB payload bx * size for i in range(count): s.sendto(payload, (host, port)) print(fsent {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)或closeTCP 收到 FINrecv返回空表示对端不再发送数据。解决应用层要区分“对端关闭写方向”和“连接出错”。收到空bytes后如果还要回传数据可以继续sendall如果对端已closesendall会触发BrokenPipeError。常见错误是收到空bytes就break退出循环导致没处理对端可能还在等回复的情况。4.3 现象UDP 服务端recvfrom收到ConnectionResetError原因UDP 是无连接协议但如果客户端sendto后关闭了套接字服务端回包时内核会返回 ICMP 端口不可达下一次recvfrom或sendto可能抛出ConnectionResetErrorWindows 上常见[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(back, 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(timeout1.0) for key, mask in events: callback key.data callback(key.fileobj)逻辑说明sel.register把套接字和回调函数绑定sel.select阻塞直到有事件就绪返回就绪的套接字列表。TCP 监听套接字就绪时调用accept_tcp已连接套接字就绪时调用handle_tcpUDP 套接字就绪时调用handle_udp。所有套接字设为非阻塞避免单个recv卡住整个事件循环。参数说明sel.select(timeout1.0)的 1 秒超时让循环有机会处理其他逻辑比如定时任务。DefaultSelector在 Linux 上自动选epoll在 macOS 上选kqueueWindows 上选select。注意select有 1024 文件描述符限制高并发要用epoll或kqueueDefaultSelector已经帮你选好了。验证习惯我每次写完 Socket 代码先用127.0.0.1跑通再用nc或telnet手动发数据确认边界最后用tcpdump抓包看实际字节流。TCP 重点看recv返回的字节数是否符合预期UDP 重点看sendto的包大小是否超过 1472。这个习惯帮我省下了大量“明明代码没错但就是不通”的排查时间。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网