Python TCP与UDP Socket编程实战:从选型到避坑指南 简介面向已具备一定Python编程基础与基本网络知识的学习者和程序员这份PDF实验文档围绕Pycharm环境下的传输层协议实验展开完整讲解TCP与UDP的Socket编程实现。内容从Pycharm安装与项目创建入手逐步演示UDP套接字的数据收发、超时设置、Ping程序模拟丢包及RTT统计以及TCP客户端与服务端的套接字创建、连接、数据编解码与关闭流程并通过对比帮助读者理解TCP与UDP的可靠性与适用场景。文档包含实验目的、环境准备、步骤说明与代码示例并给出了Ping服务器完整代码及客户端编写要求适合课堂实验、自学实践或考前复习使用。全部资源仅含1个PDF文件压缩包大小为735KB无需额外配套代码即可随文档逐步操作练习。目前已有170人学习/浏览对入门网络Socket编程具有较高的参考价值。1. TCP与UDP的Socket编程Python为什么是上手最快的落地方式计算机网络中TCP与UDP Socket编程的Python实现核心要解决的是进程之间跨机器通信的问题你既要写服务端监听端口又要写客户端发起连接还要在“可靠有序”和“低延迟低开销”之间做选择。TCP三次握手的可靠性和UDP协议栈的轻量特性让很多新人在选型时直接卡住。我见过不少项目内部数据采集用TCP结果被粘包折腾一周帧同步用UDP上线后发现丢包率远超预期。Python的socket模块把底层API包得足够薄既能看清协议细节又能快速落到生产脚本是同时验证这两种通信思路最顺手的工具。这篇笔记适合刚进网络编程、以及准备把通信模块写进自动化脚本的从业者。2. 先选型再动手TCP与UDP的差异、socket API与Python骨架2.1 三次握手与四次挥手TCP的可靠性到底来自哪里TCP是面向连接的协议通信前先通过三次握手确认双方收发能力客户端发SYN服务端回SYNACK客户端再回ACK此后进入数据收发阶段。通信结束时走四次挥手确保双方都没有数据要发了才真正断开。这个机制带来的直接结果是数据有序、不丢失、不重复代价是每次建连和断连都有额外开销而且一旦网络抖动重传机制会让延迟明显放大。UDP则完全不同它不握手、不保序、不重传应用层把数据交给sendto就直接丢进网络。首部只有8字节TCP首部是20字节起数据量小、频率高、拓扑是广播或组播的场景UDP优势非常明显但“发出去不保证到达”这一条必须写进你的架构决策里。选型时我一般这样判断接收方必须拿到完整有序的数据比如文件传输、命令下发、数据库同步用TCP数据本身有时效性丢一拍可以接受比如实时位置上报、游戏状态同步、音视频流用UDP。这个选择没有绝对的对错但一定要想清楚“如果丢了一个包后果是什么”再决定。TCP/IP协议族里的这个分工延续了几十年你没有必要在应用层逆着它的设计去硬写。2.2 Python socket API服务端与客户端的最小骨架Python的socket模块底层调用的就是操作系统提供的socket接口所以你在Linux写的代码拿回Windows改改地址类型就能跑。创建socket时要指定地址族和套接字类型AF_INET代表IPv4SOCK_STREAM代表TCP的流式套接字SOCK_DGRAM代表UDP的数据报套接字。代码里最常见的错误是把类型和协议搞混TCP必须配SOCK_STREAMUDP必须配SOCK_DGRAM反过来你会在bind或connect时收到意想不到的异常。API作用关键参数socket.socket(family, type)创建套接字AF_INET, SOCK_STREAM / SOCK_DGRAMbind(address)绑定地址和端口(ip, port) 元组listen(backlog)进入监听模式仅TCP等待队列长度accept()接受一个客户端连接仅TCP返回(conn, addr)connect(address)主动连接服务端目标(ip, port)send(data) / recv(bufsize)TCP收发字节串bufsize是本次最多读多少字节sendto(data, addr) / recvfrom(bufsize)UDP收发数据报addr是目标地址元组这里要特别强调send和recv操作的是字节串不是字符串。Python 3里直接send(hello)会报TypeError必须先编码成utf-8或gbk再发。另外recv(1024)里的1024是“一次最多读多少字节”不是“必须读满1024才返回”这两个概念的差别是后面理解粘包问题的基础。2.3 用一段TCP回显程序跑通端口绑定、监听与accept循环直接写一个最小可跑的TCP回显服务端和客户端这是所有TCP业务的地基。服务端监听本机9000端口把客户端发来的内容原样回回去。import socket server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 方便重启避免端口被TIME_WAIT占住 server.bind((0.0.0.0, 9000)) # 0.0.0.0 表示监听所有网卡 server.listen(5) # backlog5最多排队5个连接 print(server listening on :9000) while True: conn, addr server.accept() # 阻塞等待新客户端 print(connected from, addr) while True: data conn.recv(1024) # 一次最多收1024字节 if not data: break # 收到空串说明客户端关闭 conn.sendall(data) # sendall比send更稳妥能保证尽量发完 conn.close()import socket client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((127.0.0.1, 9000)) # 连本地回环地址 client.sendall(bhello tcp) # b开头的是字节串 resp client.recv(1024) print(received:, resp.decode(utf-8)) client.close()服务端里我做了三件事先bind到9000端口bind的ip用0.0.0.0而不是127.0.0.1因为前者允许局域网其他机器连进来后者只允许本地访问listen的backlog参数控制的是内核维护的等待队列长度并发量不大时5够用内层while循环持续读取直到收到空数据recv返回b表示对端调用了close这时候必须退出循环否则会死循环。客户端的sendall和send的区别在于send可能只发出部分字节并返回本次发出去的长度sendall则负责把数据全部写入应用层写代码优先用sendall。每次接受一个连接就串行处理这个服务端只适合验证流程真正多客户端并发在第5章展开。3. UDP的Socket编程从最小可跑代码到可靠性补偿3.1 UDP服务端与客户端没有listen也没有acceptUDP服务端的写法比TCP还简单因为无连接的特性不需要listen和accept只要bind住端口就可以用recvfrom接收任意来源的数据报。对应地客户端也不需要connect直接用sendto把数据报丢出去。这里没有“连接建立失败”的概念你往一个没人监听的端口发数据UDP协议栈本身不会告诉你。import socket udp_server socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_server.bind((0.0.0.0, 9001)) # 绑定UDP端口9001 print(udp server listening on :9001) while True: data, addr udp_server.recvfrom(2048) # 返回数据和来源地址 print(fpacket from {addr}: {data.decode(utf-8)}) udp_server.sendto(data, addr) # 原样回给同一个地址import socket udp_client socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_client.sendto(bhello udp, (127.0.0.1, 9001)) resp, server_addr udp_client.recvfrom(2048) print(received:, resp.decode(utf-8))这段代码里有三个容易被忽略的点。第一recvfrom的返回值是两个值数据本身和来源地址元组你回包时必须要用这个addr否则发错地方对方收不到。第二UDP的bind端口和TCP端口是独立的9000跑TCP、9001跑UDP完全没有冲突反过来同一个端口两种协议同时存在也合法。第三UDP客户端不connect也能收发但如果你调用connect之后再send内核会把对端地址固定下来后续只能用send发送系统也会在你connect时做一次路由检查提前发现“目标不可达”这类错误这种用法在UDP端口测试时经常遇到能帮你快速筛掉一部分网络配置问题。3.2 为什么UDP会丢包缓冲区、MTU与端口探测UDP丢包不是玄学原因通常能在三层里找到。第一层是内核缓冲区满收包速度超过应用层recvfrom消费速度内核接收队列溢出新到的包直接丢弃这种情况在服务器上最常见UDP洪水一到应用层CPU没来得及处理丢包率就会飙升。第二层是链路MTU以太网默认MTU是1500字节IP首部20字节加UDP首部8字节后UDP payload超过1472字节就可能触发分片分片包在传输中丢失任何一个整个数据报都废了。第三层是目标端口没人监听包到了但没人收这不算网络层的丢包但应用层表现同样是数据消失了。想做UDP端口测试Linux下最简单的方式是nc -uWindows可以用Python写一个三行的探测脚本创建UDP socket、settimeout(2秒)、sendto一个探测包然后尝试recvfrom收到说明服务在线超时说明端口无响应或防火墙拦截。实践中我建议把每个UDP包控制在1400字节以内既规避了MTU分片也降低了单包被路由器丢弃的概率。要真正确认丢包率拿数据说话后面5.3节会给iperf3打流的具体做法。3.3 给UDP补可靠性的常用做法序列号与超时重传很多自研协议基于UDP实现因为它不想背TCP的队头阻塞和建连开销但可靠性还得自己补。最朴素的方案是给每个数据报加一个递增的序列号接收方检查序列号是否连续发现断了就发一个NACK通知重传发送方给每个包启动一个超时定时器超过阈值没收到确认就重发。这套机制做出来就是精简版的TCP浅尝辄止可以想做到生产级需要处理乱序、重复包、拥塞控制工程量并不小。import socket, time udp_client socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_client.settimeout(0.5) # 500ms未收到ACK就重传 seq 0 while seq 3: msg fseq{seq}, payloadhello.encode(utf-8) for _ in range(3): # 最多重传3次 udp_client.sendto(msg, (127.0.0.1, 9002)) try: ack, _ udp_client.recvfrom(256) print(ack:, ack.decode(utf-8)) break except socket.timeout: print(timeout, retransmit seq, seq) continue seq 1这段代码演示的是发送端视角的“超时重传”同一个序列号最多发3次收到ACK才跳下一个。真实项目里接收端只认新序列号重复收到的包直接丢弃ACK里要带上收到的序列号让发送端精确定位。你的业务如果只是局域网内偶尔丢一两个包这种补偿足够了但如果要跨公网、跨运营商先评估一下你的团队有没有能力处理乱序和拥塞如果没有直接用TCP是更务实的选择。别为“快”去造协议协议栈不是你的核心竞争力的地方就交给TCP。4. 避坑TCP与UDP Socket编程里最常见的4个翻车现场4.1 Windows下报“每个套接字地址只允许使用一次”的10048错误现象服务端程序重启后偶尔能启动但频繁重启没多久就抛OSErrorWindows系统下常见的是错误码10048。原因主动关闭方会进入TIME_WAIT状态默认等待约240秒才释放端口你立刻重启服务端去bind同一个端口内核就把你拒了Linux下表现则是不设置SO_REUSEADDR时bind失败。解决创建socket后立即调用setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)这行代码的含义是允许端口在TIME_WAIT阶段被重新绑定调试期加这一行能少踩不少坑。生产环境里如果你用systemd托管服务还可以通过systemd的ReusePortTrue配合SO_REUSEPORT做多进程监听但那是另一个话题了。4.2 TCP recv永远收不全一条消息粘包与半包现象客户端一次send发送了一整条业务数据服务端recv回来发现数据和其他包粘在一起或者一条消息被截成两半按JSON解析怎么都报错。原因TCP是字节流协议不保留应用层消息边界发送方多次写、接收方多次读数据在缓冲区里重新组合后你根本分不清“一条”的界限在哪里。解决自定消息格式最通用的是“长度前缀内容”的做法。前面4字节用struct模块打包成无符号整数表示正文长度接收方先读满4字节拿到长度再按这个长度循环读取正文直到收齐为止。这不是抢占式处理也不是什么黑匣子它就是TCP应用层必须自己维护的协议边界。import struct def recv_exact(sock, n): chunks [] remains n while remains 0: chunk sock.recv(remains) if not chunk: raise ConnectionError(connection closed) chunks.append(chunk) remains - len(chunk) return b.join(chunks) def recv_msg(sock): header recv_exact(sock, 4) length struct.unpack(!I, header)[0] return recv_exact(sock, length)这段代码把recv封装成recv_exact核心思想是“要多少读多少”一次recv没读够就继续读直到凑满所需的字节数。recv_exact里每次recv(remains)的下限是剩余字节数是为了防止一次性多读后面消息的数据。至于粘包分界符的做法比如用\n或空行切分只适合业务字段里不可能出现该字符的场景通用性远不如长度前缀。4.3 UDP客户端在没人回复时卡住不动现象用UDP发了一个请求服务端没起来或防火墙把端口拦了客户端recvfrom一直阻塞程序像死掉了一样。原因UDP socket默认是阻塞模式没有任何机制能通知你“对端不存在”recvfrom会一直挂在那里等。解决给socket加超时udp_client.settimeout(3)之后recvfrom在3秒内收不到数据会抛socket.timeout你在异常分支里做重发或降级处理。最稳妥的做法是直接在代码里创建socket后先调settimeout再进收发循环不要指望操作系统的UDP协议栈给你反馈它在这件事上就是个哑巴。4.4 负载一大UDP丢包率飙升先怀疑单包太大现象局域网里传小文件没问题一传大的结构化数据UDP丢包率直接到30%以上调试时包越小越稳。原因你传的业务数据超过MTU后会被IP层分片任何一个分片在路由器、交换机或接收端被丢弃整个数据报都算丢高负载下路由器还可能主动丢弃尾部超长包俗称尾丢弃策略。解决控制单包大小UDP payload保持1400字节以下对应MTU 1500减去IP和UDP首部的28字节大业务数据自己拆成多个小包接收端按序列号重组。配合4.3的超时重传这两个技巧能解决局域网UDP链路80%的通信问题。4.5 accept循环里做耗时处理第二个客户端永远连不上现象第一个客户端连上服务端两边数据交互正常第二个客户端connect却一直pending直到第一个断开才被accept。原因单线程的accept循环里每个连接的数据通信都是串行处理的第一个连接占住了CPU后续连接只能在内核的backlog队列里排队。解决把每个连接的处理丢给线程accept循环只负责接收新连接。这个做法在第5章会展开但这里先记住结论生产级TCP服务端必须并发处理连接单线程死循环只会让你在第一个压力测试时就翻车。5. 把Socket放到真实场景多连接、非阻塞、粘包与性能验证5.1 多客户端并发用threading撑起一个能同时服务的TCP服务端第2章的串行服务端只能做教学验证真实项目里一台采集设备要同时上报几百个点位必须并发处理。最直接的做法是每来一个客户端就创建一个线程专门处理它。import socket, threading def handle_conn(conn, addr): with conn: while True: data conn.recv(1024) if not data: break conn.sendall(data) print(connection closed, addr) server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 9003)) server.listen(50) # 并发上来后backlog要调大 while True: conn, addr server.accept() threading.Thread(targethandle_conn, args(conn, addr), daemonTrue).start()这里把每个客户端连接的生命周期和业务处理都收进handle_conn函数线程设为daemon是为了主进程退出时不会因为某个长连接线程阻塞在recv而挂住。listen的backlog调到50因为线程调度需要时间如果队列太短很多连接会被内核直接RST拒绝。这个方案能应对数百个连接但是每个线程的内核栈空间占用不小高并发上到几千就需要换selector或asyncioPython的GIL会让纯线程方案在多核机器上有些吃亏。5.2 非阻塞与select不想开线程时的另一种调度方式线程不是唯一出路你还可以把socket设为非阻塞配合select做事件驱动。服务端把监听socket和所有客户端socket放进一个read列表select一旦发现某个socket可读再决定去accept还是去recv。这种方式的优势是单线程里能管理大量连接没有线程切换开销代码却复杂不少。import socket, select server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setblocking(False) # 非阻塞模式是select的前提 server.bind((0.0.0.0, 9004)) server.listen(10) clients [] while True: readable, _, _ select.select([server] clients, [], [], 1.0) for sock in readable: if sock is server: conn, addr server.accept() conn.setblocking(False) clients.append(conn) else: data sock.recv(1024) if not data: clients.remove(sock) sock.close() else: sock.sendall(data)select的核心参数是第一个列表里面放所有你想读的socket返回的readable只是“可能有数据”的子集你还是要逐个recv。之所以要setblocking(False)是因为在高并发下某个socket可能刚被select标记为可读下一个瞬间数据就被另一个线程抢走非阻塞recv在没数据时直接抛BlockingIOError而不是挂住。这段代码适合连接数多但每个连接流量不大的场景比如长时间维持的监控通道如果你是命令交互需要同时读写还得分出write和except两个集合复杂度继续上升。5.3 iperf3 UDP打流用数据而不是感觉判断链路质量写好了通信程序不能只看“偶尔能通”就上线。验证网络性能和丢包率我用iperf3打流这是网络从业人员都认的工具。先在一台机器上启动服务端另一台机器上用UDP模式向它打流量。# 服务端监听默认端口5201 iperf3 -s # 客户端UDP模式打流100Mbps持续10秒每包1400字节 iperf3 -u -c 192.168.1.100 -b 100M -t 10 -l 1400参数含义-u启用UDP模式-c指定服务端地址-b设置目标带宽-t指定打流时长-l设置负载包大小。跑完看服务端输出里的Lost/Total Datagrams和jitter两列前者是丢包率后者是延时抖动。如果你看到丢包率超过1%先检查你的包大小是不是超过MTU再检查链路是千兆还是百兆最后看看服务端UDP接收缓冲区是否有溢出。这套打流结果可以直接作为你UDP业务“能否上线”的判定数据比你自己写脚本ping几百次靠谱得多。TCP同理可以用iperf3不加-u来测最大吞吐主要用于确认协议栈和链路瓶颈。6. 缓冲区与TCP_NODELAY调好这两个参数延迟和吞吐立刻不一样到了这个阶段连接能建、数据能跑但你还会遇到两个看着很玄学的现象小数据包的延迟突然飙升或者UDP在大流量下还是偶尔丢几包。前者十有八九是Nagle算法在捣乱后者多半是接收缓冲区太小这两个调参就是最后的临门一脚。Nagle算法会把多个小包攒成一个发送减少网络上的小报文数量这在传输大量数据时是好事但你的业务是高频小报文交互比如每几秒发一次坐标或状态Nagle就会把数据扣在本地等ACK延迟能叠加到几十毫秒。交互式场景下的标准解法是关闭这个算法代码里一行搞定tcp_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)。这里踩过的坑是只对服务端设置不管客户端客户端发送小报文一样会触发Nagle两边都要设。UDP接收缓冲区则是另一个容易被忽视的点。Linux默认的net.core.rmem_max可能只有两百多KB业务突发流量稍微大一点就会触发内核丢包缓解办法是把接收缓冲调大udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 * 1024 * 1024)再配合sysctl里net.core.rmem_max和net.core.wmem_max的系统级上限一起调。之前做一个监控项目延迟问题排查了半天最后发现就是服务端没关Nagle教训就是性能问题不要只盯着网络拓扑协议栈参数也是个黑匣子。这个方向你值得花半小时在实验环境里把参数穷举一遍记录不同配置下的延迟和吞吐后续遇到相似业务能直接套用模板。希望帮到你。本文还有配套的精品资源点击获取