
简介一份面向具备Python基础与基本网络知识的学习者和程序员的Socket编程实验资料基于PyCharm环境聚焦TCP与UDP两种传输层协议的具体实现。内容涵盖UDP套接字的数据发送、接收与超时设置通过Ping服务器模拟随机丢包并统计往返时间RTT同时完整演示TCP客户端和服务端的创建套接字、绑定地址、监听、连接、发送/接收数据及关闭连接等步骤有助于直观对比TCP的可靠连接与UDP的无连接特性理解两者在不同网络场景下的适用性。压缩包共1个文件为735KB的PDF文档适合教学或自学时对照操作。目前已有170人学习下载。读者可从中获取实验目的与要求、PyCharm环境配置说明、完整代码示例及逐步实现思路尤其适合初次尝试Python网络编程的入门者。1. 从「请求超时」到「跑通文件传输」这份TCP/UDP Socket实验在教什么第一次在PyCharm里运行UDP Ping客户端看到满屏“请求超时”再对比TCP客户端和服务端能正常收发是很多人理解传输层协议的第一个坎。这份实验资源把计算机网络里TCP与UDP的Socket编程拆成了能照做的步骤先搭PyCharm环境再用Python实现UDP数据报的发送、接收、超时与丢包统计最后用TCP完成连接、交互和文件传输。它适合正在上计算机网络课、或者自学Socket编程的Python开发者不需要路由器交换机一台联网PC就能把两种协议跑起来并看到真实现象。核心价值在于把教材里“不可靠传输”和“可靠连接”的抽象概念变成可运行、可打断、可验证的代码。2. PyCharm环境与Socket选型AF_INET、SOCK_DGRAM与SOCK_STREAM怎么选先花十分钟把环境立起来。这个实验全程在PyCharm里做IDE不熟的话后面的调试会反复卡在“代码没错但不知道去哪跑”这种基础问题上。2.1 PyCharm安装和项目创建路径、解释器与第一次Run安装步骤本身不难双击下载好的.exe一路 Next到 Installation Options 页面把四个勾选全部勾上再点 Install。有两个地方容易埋坑。第一个坑是安装路径。实验文档专门强调“尽量不要选择带中文和空格的目录”这不是洁癖是后续 PyCharm 的终端和虚拟环境工具在某些中文路径下会出现编码识别问题尤其是 Windows 上路径带“网络编程”这类目录名pip 和解释器经常翻车。我一般直接放到D:\PyCharm这种纯英文路径。装完以后新建项目时真正影响后续运行的是解释器选择。PyCharm 默认会创建虚拟环境 venv但如果你机器上已经装了 Python 3.8也可以直接选 Existing interpreter指向系统 Python省掉每次项目都重新装包的麻烦。注意看 Location 字段“自己起个名 my_pythonProject”时同样别带中文和空格。创建 Python 文件的操作是选中项目右键 New → Python File输入test回车。文件里写一行print(hello socket)右键选 Run test控制台输出就说明环境通了。这里有一个新手常犯的问题在 PyCharm 底部 Termial 里执行python test.py能跑但右键 Run 却报错原因是解释器选错了项目。右键工具栏的当前解释器路径要和实际 Python 安装路径一致。2.2 socket模块三个核心参数地址族、套接字类型与协议进入代码部分前先把socket()构造函数的三个参数讲透后面所有代码都建立在这上面。第一个参数是地址族实验用的是socket.AF_INET代表 IPv4如果要处理 IPv6 就用AF_INET6。第二个参数是套接字类型UDP 用SOCK_DGRAMTCP 用SOCK_STREAM这是本实验选型的分水岭。参数值语义对应协议地址族AF_INETIPv4 网络地址TCP/UDP地址族AF_INET6IPv6 网络地址TCP/UDP套接字类型SOCK_DGRAM数据报面向消息有边界UDP套接字类型SOCK_STREAM字节流面向连接无边界TCP第三个参数是协议号通常省略让系统根据前两个参数自动推断。socket.socket(socket.AF_INET, socket.SOCK_DGRAM)创建的 UDP 套接字socket.socket(socket.AF_INET, socket.SOCK_STREAM)创建的是 TCP 套接字。我见过有人在这两个类型之间混用比如 TCP 套接字去调sendto()直接抛TypeError原因就是把数据报和字节流模式搞混了。2.3 为什么用UDP和TCP做对照三次握手代价与实时性取舍实验要求同一门课里把两种协议都写一遍不是增加工作量是要你通过对比建立选型直觉。UDP 无连接、不保证送达、不维护状态发送端sendto()一扔就完事不关心对方在不在线所以延迟低、开销小TCP 面向连接传输前先走三次握手建立会话收发后还要靠序号和确认号保证顺序与可靠换来的是数据不错、不丢、不重代价是额外握手和确认往返。实际工程里怎么选DNS 查询必须快用 UDPHTTP 网页必须完整用 TCP视频通话丢一帧可以接受但不能卡用 UDP银行转账丢一个包万劫不复用 TCP。这个实验的资源设计非常典型UDP 部分专门模拟丢包和超时让你体会不可靠带来的“原生态”问题TCP 部分则让你写出能正常收发、甚至传文件的程序体会可靠性是怎么通过 API 闭环的。两份对比着做Socket 编程的框架就立住了。3. UDP Ping全流程服务端丢包模拟、客户端超时与RTT统计UDP 部分是整个实验的灵魂也是网络教材里“不可靠传输”最生动的一课。服务端代码资源里已经给了客户端要自己补全并且最后还要扩展出 RTT 统计和心跳逻辑。这一章我按从服务端到客户端、再到作业扩展的顺序拆开。3.1 服务端源码逐行拆解random.randint如何模拟30%丢包实验文档给的 UDPPingerServer.py 是一段完整的服务器实现把需要用到的模块和核心调用逐行过一遍。import random from socket import * serverSocket socket(AF_INET, SOCK_DGRAM) serverSocket.bind((, 12000)) print(UDP Pinger Server is running on port 12000) while True: rand random.randint(0, 10) message, address serverSocket.recvfrom(1024) message message.upper() if rand 4: continue serverSocket.sendto(message, address)逻辑说明serverSocket.bind((, 12000))把套接字绑定到本机所有网卡的 12000 端口空字符串表示接受任意本地 IP 的包。recvfrom(1024)接收数据报1024 是缓冲区上限返回值是两个message是字节串内容address是客户端 IP 和端口。收到后转大写再判断随机数决定是否响应。参数说明random.randint(0, 10)生成 0 到 10 之间的整数rand 4时continue直接跳过后面的发送。按文档描述是 30% 丢包但实际算一下0 到 10 共 11 个值小于 4 的有 0、1、2、3 四个真实丢包率约 36%。这是一个值得留意的细节——实验文档说 30%代码实际是 36%你在交作业说明里写清楚真实概率反而能体现你真的读懂了这段代码。这个无限循环就是典型的 UDP 服务器形态不维护连接状态每次循环独立处理一个数据报丢了就丢了不回执、不重传。3.2 客户端完整实现settimeout、recvfrom与RTT计算文档要求客户端向服务器发 10 次 Ping每次最多等 1 秒收不到就打印“请求超时”。下面是可直接运行在 PyCharm 里的完整客户端实现import socket import time server_address (127.0.0.1, 12000) client_socket socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_socket.settimeout(1.0) for sequence_number in range(1, 11): send_time time.time() message fPing {sequence_number} {send_time}.encode(utf-8) client_socket.sendto(message, server_address) try: response, server_addr client_socket.recvfrom(1024) rtt time.time() - send_time print(fPing {sequence_number}: {response.decode(utf-8).strip()} RTT {rtt:.6f}s) except socket.timeout: print(fPing {sequence_number}: 请求超时) client_socket.close()逻辑说明循环从 1 到 10每次构造格式为Ping 序号 时间戳的 ASCII 消息发送前记录send_time收到响应后立即算差值这就是该数据包的 RTT。settimeout(1.0)是 UDP 客户端最重要的设置没有它当服务器故意丢掉数据包时recvfrom会无限期阻塞程序永远卡住。设置了超时后recvfrom会抛socket.timeout异常程序捕获后打印“请求超时”继续发下一个包。参数说明time.time() 1.0的期望写法是直接用settimeout(1.0)单位为秒浮点数recvfrom缓冲区和服务器一致用 1024服务器地址可以是127.0.0.1或localhost联调阶段跑在同一台机器上没有区别。3.3 消息格式Ping sequence_number time在算哪笔账文档里明确给定了消息格式Ping sequence_number time三个字段之间用空格分隔全部是 ASCII 字符。sequence_number 从 1 递增到 10time 是客户端发送消息时的time.time()时间戳。这个格式不是随便定的因为它直接把“时延测量”这个本地理应很简单的问题变成了网络环境下的闭环验证。发送端记录了send_time接收端收到后把消息转大写返回客户端再记一个response_time两者相减就是 RTT。这段时延包含客户端到服务器的传播时延、服务器处理时间、服务器到客户端的回程时延以及可能的排队时延。在校园网这种丢包率极低的环境RTT 通常在几毫秒到几十毫秒。如果跑出几百毫秒多半是跨路由或无线干扰如果你把服务器和客户端分别放在两台机器上跑这里的数值才有真实参考意义。3.4 作业扩展一最小最大平均RTT与丢包率统计作业要求把逐包 RTT 改成标准 ping 的汇总报告。我在 3.2 代码基础上加统计逻辑import socket import time server_address (127.0.0.1, 12000) client_socket socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_socket.settimeout(1.0) rtt_list [] lost_count 0 total_count 10 for sequence_number in range(1, total_count 1): send_time time.time() message fPing {sequence_number} {send_time}.encode(utf-8) client_socket.sendto(message, server_address) try: response, _ client_socket.recvfrom(1024) rtt time.time() - send_time rtt_list.append(rtt) print(fPing {sequence_number}: RTT {rtt:.6f}s) except socket.timeout: lost_count 1 print(fPing {sequence_number}: 请求超时) if rtt_list: print(f最小RTT: {min(rtt_list):.6f}s) print(f最大RTT: {max(rtt_list):.6f}s) print(f平均RTT: {sum(rtt_list) / len(rtt_list):.6f}s) print(f丢包率: {lost_count / total_count * 100:.1f}%) client_socket.close()逻辑说明把每次成功响应的 RTT 放进列表超时则累计lost_count。结束后分别用min、max、sum计算汇总指标丢包率是丢失包数除以总发送数。注意边界条件如果 10 个包全部超时rtt_list为空直接做除法会抛ZeroDivisionError所以先判断非空再输出。参数说明total_count 10是实验固定值你也可以改成 20 或更观察更平滑丢包率输出保留一位小数与 ping 工具的展示习惯一致。3.5 作业扩展二UDP心跳与单向丢包检测作业的另一个扩展是实现 UDP 心跳核心思路和 Ping 类似但语义不同Ping 是客户端主动探活心跳是服务器被动监听并且要能报告“哪些包丢了”。# 心跳服务端 heartbeat_server.py import socket import time expected_seq 1 heartbeat_socket socket.socket(socket.AF_INET, socket.SOCK_DGRAM) heartbeat_socket.bind((, 12001)) print(Heartbeat server listening on port 12001) heartbeat_socket.settimeout(3.0) while True: try: data, addr heartbeat_socket.recvfrom(1024) seq, sent_time data.decode(utf-8).split() seq int(seq) rtt time.time() - float(sent_time) print(f心跳包 {seq} RTT {rtt:.6f}s) if seq expected_seq: lost seq - expected_seq print(f检测到 {lost} 个心跳包丢失) expected_seq seq 1 except socket.timeout: print(心跳超时客户端可能已停止)逻辑说明服务端维护一个expected_seq期望序列号如果收到的序列号大于期望值说明中间若干跳没到差值就是丢包数。这个思路与 TCP 的累计确认有异曲同工之处但 UDP 没有重传机制只能靠接收方记录。settimeout(3.0)是给整个接收循环兜底若心跳中断超过 3 秒就判定客户端异常下线。这在工程上对应运维监控里的“健康检查”场景。4. TCP Socket编程connect、listen、accept与数据收发TCP 部分和 UDP 完全是两套思维。UDP 是“发完不管”TCP 是“先握手再说话”。这一章把客户端和服务端的标准模板过一遍再解决多客户端、多次交流和文件传输三个高频作业题。4.1 TCP客户端五步走创建套接字、connect、send、recv、close实验给了一段基础客户端代码完整可跑版本如下import socket tcp_client socket.socket(socket.AF_INET, socket.SOCK_STREAM) ip input(请输入ip地址) port int(input(请输入端口号)) tcp_client.connect((ip, port)) data input(请输入要发送的数据) tcp_client.send(data.encode(gbk)) recv_data tcp_client.recv(1024) print(recv_data.decode(gbk)) tcp_client.close()逻辑说明connect((ip, port))会触发三次握手这一步在 UDP 里完全不存在的。握手成功后send(data.encode(gbk))把字符串按 GBK 转成字节流发出去recv(1024)阻塞等待服务器回包。注意这里第 4 步和第 5 步是有顺序依赖的如果服务器不回包客户端会一直阻塞在recv所以在实际作业扩展里往往需要把接收放到循环里或者加超时。参数说明port必须转成int否则connect会把端口当作字符串拼接报错ip输入127.0.0.1可本机自测输入服务器局域网 IP 可跨机通信输入公网 IP 则取决于路由和防火墙。recv的 1024 是单次接收最大字节数如果服务器返回超过 1024 字节需要循环接收这与 UDP 的recvfrom行为不同。4.2 TCP服务端bind、listen、accept与收发数据服务端代码是 Socket 编程的核心难点它比客户端多出三个概念绑定、监听、接受连接。import socket tcp_server socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_server.bind((192.168.1.3, 8080)) tcp_server.listen(2) print(TCP server listening on 192.168.1.3:8080) new_client, addr tcp_server.accept() print(f客户端已连接: {addr}) recv_data new_client.recv(1024) print(收到:, recv_data.decode(gbk)) data input(请输入要发送给client的数据) new_client.send(data.encode(gbk)) new_client.close() tcp_server.close()逻辑说明bind把服务套接字固定到指定 IP 和端口listen(2)把套接字变为被动连接参数是等待队列长度即最多允许 2 个未accept的连接排队accept()是阻塞调用返回的新套接字new_client专门负责与这个客户端通信原始tcp_server继续等待下一个连接。收发和 4.1 一样encode(gbk)与decode(gbk)必须成对出现。参数说明bind的 IP 要与客户端connect的 IP 保持一致。实验里写的是192.168.1.3实际你要改成自己机器的局域网 IP否则另一台机器连不上。联调阶段我一般直接改成127.0.0.1本机打自己。4.3 编码与乱码gbk还是utf-8的坑实验里所有send和recv都用了gbk这在 Windows 上没问题因为 Windows 中文版默认 ANSI 就是 GBK。但坑在跨平台如果你在 macOS 或 Linux 上运行服务端客户端在 Windows 上运行服务端decode(gbk)会抛UnicodeDecodeError因为 Linux 默认编码是 UTF-8中文在 UTF-8 下是 3 字节GBK 是 2 字节解码必然失败。我的习惯是所有 Socket 应用统一用 UTF-8 编码两端保持一致就不会乱码。如果你必须沿用实验的gbk那么发送端和接收端必须同时用gbk并且纯数字和英文字符串是不受影响的部分中文才是重灾区。4.4 作业扩展多客户端、多次交流与文件传输这三个作业是递进关系。多客户端是让一个服务端能接住多个连接多次交流是让一次连接内能收发多轮文件传输则是在实现前两条的基础上增加数据分段和完整性判断。多客户端服务的标准解法是accept循环加线程import socket import threading def handle_client(client_socket, addr): print(f新连接: {addr}) while True: recv_data client_socket.recv(1024) if not recv_data: break print(f收到 {addr}: {recv_data.decode(utf-8)}) client_socket.send(recv_data.upper()) client_socket.close() tcp_server socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_server.bind((0.0.0.0, 8080)) tcp_server.listen(5) print(Server listening on port 8080) while True: client, addr tcp_server.accept() thread threading.Thread(targethandle_client, args(client, addr)) thread.start()逻辑说明handle_client里套一层while True持续recv这就实现了多次交流当对端关闭套接字时recv返回空字节串退出循环accept的while True保证一个线程服务一个客户端同时支持多个。监听参数listen(5)比基础版的listen(2)更实用应付 5 个排队连接。文件传输的关键是分段读取和发送完整确认。下面这段是我常用的方案import socket import os def send_file(client_socket, filepath): file_size os.path.getsize(filepath) client_socket.send(f{file_size}.encode(utf-8)) client_socket.recv(1024) # 等待ACK避免粘包 with open(filepath, rb) as f: while True: data f.read(4096) if not data: break client_socket.send(data) client_socket.send(bEOF)逻辑说明先发文件大小再等接收方回一个 ACK这是简单粗暴的防粘包方式避免接收方把大小信息和文件内容一次性读走。之后循环用read(4096)读文件块并发送读完发送EOF结束标记。接收端循环recv累加长度直到相等或读到 EOF。send在阻塞模式下不保证一次把 4096 字节发完所以稳妥做法是用client_socket.sendall(data)它会内部循环保证全部发送。参数说明4096不是随便定的本地文件传输常见 4096 或 8192 性能较好超过 MTU 后会在内核分成多个 IP 包但应用层不必关心分片细节只用控制好recv循环的结束条件。5. Socket编程避坑指南本机联调时最容易翻车的五个地方Socket 实验的代码量不大但翻车率极高。以下五条都是我在帮学生调试和实际开发中反复踩过的坑每条按现象、原因、解决三个层次写。5.1 服务端重启报OSError地址已被使用现象服务端程序关闭后立即重启控制台抛OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。原因服务端套接字进入 TIME_WAIT 状态端口还在被内核占用。TCP 主动关闭连接的一方会保留该连接状态约 2 分钟这是由 TCP 规范决定的主要为了处理延迟到达的旧报文。解决绑定前设置SO_REUSEADDR允许复用 TIME_WAIT 状态的地址tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)必须放在bind之前。UDP 服务端重启一般不报这个错因为 UDP 没有连接状态。5.2 客户端卡死在recv不打印超时也不退出现象客户端recvfrom或recv一直阻塞程序像死了一样停在原地。原因绝大多数情况是服务端模拟丢包时响应没有回来而客户端没有设置超时。实验明确要求等 1 秒如果漏了settimeout遇到丢包就直接挂起。TCP 场景下则是服务端逻辑里没进send分支或者recv之前在等待输入。解决UDP 客户端必须settimeout(1.0)TCP 同源问题可以用带超时的连接方式比如实验要求里说的“查阅 Python 文档设置套接字超时”。调试期我通常会再打印一行sendto successful确认卡点是在发送后还是接收前用排除法缩范围。5.3 粘包与半包接收方读到两段拼接数据现象TCP 客户端发送两次服务端一次性recv到两段数据拼接在一起的字节。原因TCP 是字节流协议没有消息边界。send(hello)两次底层可能合并成一个recv返回值bhellohello这是“粘包”反过来一次send大文件recv(1024)可能只读到前 1024 字节这是“半包”。解决UDP 天然不粘包因为每个数据报有边界TCP 必须自己约定消息边界。常见做法有三种固定长度报文、分隔符如换行符、先传长度。我在 4.4 文件传输里用的就是先传文件大小再等 ACK 的“长度内容”方案。作业里如果只需要交简单收发代码只要说明“本场景不涉及粘包因为一次只发一条短消息”也能过关。5.4 换机器就连不上防火墙弹窗和IP配置错误现象本机127.0.0.1跑得好好的放到两台机器上客户端connect超时或拒绝连接。原因常见原因有两层。第一Windows 防火墙默认拦外部入站连接客户端发 SYN 过来服务端不响应第二服务端bind的 IP 写成127.0.0.1只能本机访问局域网内其他机器根本到不了。解决调试阶段先固定本机跑通再换真实网段的 IP。服务端bind((0.0.0.0, 8080))表示监听所有网卡客户端connect时填服务端局域网 IP。防火墙我一般做端口放行命令行以管理员执行netsh advfirewall firewall add rule namesocket8080 dirin actionallow protocolTCP localport8080UDP 同理换protocolUDP。在校园网检查一下同一 VLAN 互ping是否通很多实验机之间 ICMP 被禁但 TCP/UDP 是放行的。5.5 recv返回空字节对端关闭了连接现象客户端正常退出后服务端下一次recv返回空数据程序没任何提示随后陷入死循环或报连接被重置。原因recv在阻塞模式下对端正常关闭套接字时返回b这是一个合法返回值不是异常。很多新手忘了判断直接decode空字节输出空字符串继续循环浪费了大量调试时间。解决每次recv后先判断返回值recv_data new_client.recv(1024) if not recv_data: print(f客户端 {addr} 已断开连接) break这个判断在 4.4 的多客户端代码里已经示范了是所有 TCP 服务端循环的标准做法。6. 用Wireshark验证Socket程序抓包看三次握手和UDP丢包程序能跑通只是第一步代码忽然不按预期走的时候靠print调参非常低效。我自己的验证习惯是直接抓包把传输层的行为拉到眼前。装好 Wireshark 后选择正在通信的网卡然后设置过滤器。UDP Ping 联调时过滤udp.port 12000再发 10 次包能直观看到客户端发出的Ping 1 172342...请求以及服务端返回的大写消息。最关键的观察是丢包模拟发生时抓包界面上只有请求包没有响应包你会看到rand 4的continue是如何在网络上表现为静默。这个现象和你在代码里看到的if rand 4: continue完美对应网络异常不再是黑匣子。TCP 联调时过滤tcp.port 8080能看到三次握手的三个报文客户端发SYN服务端回SYN-ACK客户端再回ACK。如果只看到SYN而一直等不到SYN-ACK说明服务端根本没有收到或防火墙丢了包如果三次握手完成但应用层数据没出现说明代码卡在了send之前。Wireshark 的厉害之处在于它能验证“连接到底建立没有”比盯着控制台猜代码快得多。我还习惯用netstat -ano辅助验证端口状态在命令行执行netstat -ano | findstr 8080能看到LISTENING状态说明服务端绑定成功ESTABLISHED说明 TCP 连接已经建立。这比print更直接因为端口状态由内核维护代码逻辑骗不了人。从那以后我每次联调 Socket 程序都强制走一遍“本机跑通 → 抓包确认握手 → netstat 确认端口 → 换机器验证”的流程这个流程帮我避开了大量防火墙和 IP 错配的坑。希望帮到你。本文还有配套的精品资源点击获取