欢迎来到 Python 学习计划的第 72 天!🎉
昨天我们学习了 异步 Socket 编程,掌握了高并发服务器的实现。但网络环境是不可靠的:连接可能超时、服务器可能宕机、数据包可能丢失。
今天我们将学习如何构建健壮的网络应用——Socket 错误处理与超时机制。这是区分"玩具代码"和"工程代码"的关键一步!
🎯 今日学习目标
📚 Socket 异常体系详解
异常继承关系
完整的异常分类
Python 中所有 Socket 异常都继承自 OSError:
异常类型 | 说明 | 继承关系 |
|---|
OSError
| 所有 Socket 异常的基类 | 最顶层 |
socket.error
| OSError 的别名(已弃用) | = OSError |
socket.timeout
| TimeoutError 的别名 | 继承自 OSError |
连接异常组 | | |
ConnectionError
| 连接相关异常的基类 | 继承自 OSError |
ConnectionRefusedError
| 连接被拒绝(ECONNREFUSED) | 继承自 ConnectionError |
ConnectionResetError
| 连接被重置(ECONNRESET) | 继承自 ConnectionError |
ConnectionAbortedError
| 连接被中止(ECONNABORTED) | 继承自 ConnectionError |
BrokenPipeError
| 管道断裂(EPIPE) | 继承自 ConnectionError |
地址相关异常 | | |
socket.herror
| 地址相关错误(gethost*错误) | 继承自 OSError |
socket.gaierror
| getaddrinfo() 错误 | 继承自 OSError |
权限异常 | | |
PermissionError
| 权限不足(EACCES) | 继承自 OSError |
异常捕获最佳实践
import socketdef handle_socket_error(operation): """异常捕获最佳实践""" try: # 执行操作 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('example.com', 8888)) # 1. 首先捕获具体的、可恢复的异常 except socket.timeout: print("⏱️ 超时异常 - 可以重试") except ConnectionRefusedError: print("❌ 连接被拒绝 - 可能需要检查服务器") except socket.gaierror as e: print(f"❌ 地址解析失败: {e} - 检查 DNS/主机名") except socket.herror as e: print(f"❌ 主机名错误: {e}") except PermissionError: print("❌ 权限不足 - 需要管理员权限") # 2. 然后捕获宽泛的连接异常 except ConnectionError as e: print(f"❌ 连接错误: {type(e).__name__}: {e}") # 3. 最后捕获通用的 OS 错误 except OSError as e: print(f"❌ OS 错误 (errno={e.errno}): {e}") # 常见 errno: # 98/10048 - 地址已被使用 (EADDRINUSE) # 111/10061 - 连接被拒绝 (ECONNREFUSED) # 4. 最后用通用异常捕捉未预期的错误 except Exception as e: print(f"❌ 未知异常: {type(e).__name__}: {e}") finally: # 5. 一定要在 finally 中清理资源 if 'sock' in locals(): try: sock.close() except: pass
🔍 常见异常详解与处理
1. 连接类异常
ConnectionRefusedError - 连接被拒绝
import sockettry: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('127.0.0.1', 8888))except ConnectionRefusedError: print("❌ 连接被拒绝") # 常见原因: # 1. 服务器未启动 # 2. 端口号错误 # 3. 防火墙阻止 # 4. 绑定地址限制(如只监听 localhost)
ConnectionResetError - 连接被重置
try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('remote.server.com', 8888)) sock.send(b"data")except ConnectionResetError: print("❌ 连接被重置") # 常见原因: # 1. 对方强制关闭连接 # 2. 网络不稳定,中间节点重置 # 3. 防火墙处理超时连接 # 4. 对方主机故障
BrokenPipeError - 管道断裂
try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('127.0.0.1', 8888)) # 对方关闭了连接 sock.send(b"data") # 这里会抛异常except BrokenPipeError: print("❌ 管道断裂") # 常见原因: # 1. 对方已关闭连接 # 2. 发送到已关闭的 Socket # 解决方案: # 1. 检查对方连接状态 # 2. 重新连接 # 3. 或停止发送数据
2. 地址相关异常
socket.gaierror - 地址解析失败
import sockettry: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 尝试连接到无效的主机名 sock.connect(('invalid-hostname-12345.com', 8888))except socket.gaierror as e: print(f"❌ getaddrinfo 错误: {e}") # 常见原因: # 1. 主机名不存在 # 2. DNS 查询失败 # 3. 网络不通 # 解决方案: # 1. 检查主机名拼写 # 2. 检查 DNS 设置 # 3. 尝试使用 IP 地址
socket.herror - 主机名错误
try: # gethost* 函数的错误 socket.gethostbyname('invalid-hostname-12345.com')except socket.herror as e: print(f"❌ 主机错误: {e}")
3. 超时异常
import socketsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sock.settimeout(5)try: sock.connect(('slow.server.com', 8888))except socket.timeout: print("⏱️ 连接超时") # 解决方案: # 1. 增加超时时间 # 2. 检查网络连接 # 3. 考虑重试连接 # 4. 检查对方服务是否响应缓慢
4. 权限和资源异常
import socket# PermissionError - 权限不足try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('0.0.0.0', 80)) # 需要 root 权限except PermissionError: print("❌ 权限不足") # 解决方案: # 1. 使用 sudo 运行 # 2. 改用高于 1024 的端口 # 3. 配置系统权限# OSError - 文件描述符溢出try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('0.0.0.0', 8888))except OSError as e: if e.errno == 98 or e.errno == 10048: # Linux / Windows print("❌ 端口已被占用") elif "too many open files" in str(e): print("❌ 文件描述符溢出") else: print(f"❌ OS 错误: {e}")
5. 数据处理异常
# UnicodeDecodeError - 解码错误data = b'\xff\xfe\x00'try: message = data.decode('utf-8')except UnicodeDecodeError: print("❌ 解码错误 - 数据格式不匹配") # 解决方案: message = data.decode('utf-8', errors='replace') # 用 ? 替换 print(f"容错后: {message}") # 输出:???
⏱️ 超时机制详解
异常处理流程
同步 Socket 超时
方法 1:单个 Socket 超时
import socketsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 设置超时时间(秒)sock.settimeout(5.0)# 获取当前超时设置timeout = sock.gettimeout()print(f"当前超时: {timeout}秒")# 修改超时sock.settimeout(10.0)# 设置为阻塞模式(无超时)sock.settimeout(None) # 或 sock.setblocking(True)# 设置为非阻塞模式sock.settimeout(0) # 或 sock.setblocking(False)# 非阻塞 Socket 操作时会立即返回,如果操作未就绪则抛异常try: sock.connect(('127.0.0.1', 8888))except BlockingIOError: print("⏱️ 操作未完成,Socket 是非阻塞模式")
方法 2:全局默认超时
import socket# 为所有新创建的 Socket 设置默认超时socket.setdefaulttimeout(10.0)# 获取默认超时default_timeout = socket.getdefaulttimeout()print(f"默认超时: {default_timeout}秒")# 新创建的 Socket 会使用默认超时sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)print(f"Socket 超时: {sock1.gettimeout()}") # 10.0# 可以为单个 Socket 覆盖默认超时sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sock2.settimeout(5.0) # 使用 5 秒而不是默认的 10 秒
方法 3:按操作设置不同超时
import socketdef connect_and_recv(host, port): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接超时:5 秒 sock.settimeout(5) try: sock.connect((host, port)) print("✅ 连接成功") except socket.timeout: print("⏱️ 连接超时") return None # 接收超时:30 秒(比连接超时长) sock.settimeout(30) try: data = sock.recv(1024) return data except socket.timeout: print("⏱️ 接收超时") return None finally: sock.close()
Socket 三种模式对比
模式 | 设置方法 | 超时设置 | 行为 | 使用场景 |
|---|
阻塞 | setblocking(True)
| settimeout(None)
| 操作阻塞直到完成或超时 | 大多数情况 |
非阻塞 | setblocking(False)
| settimeout(0)
| 操作立即返回,未就绪则抛 BlockingIOError | select/poll 配合使用 |
超时 | setblocking(True)
| settimeout(n)
| 操作阻塞最多 n 秒,超时则抛 socket.timeout | |
import socketsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 方式 1:设置为阻塞模式sock.setblocking(True)# 等价于:# sock.settimeout(None)# 方式 2:设置为非阻塞模式sock.setblocking(False)# 等价于:# sock.settimeout(0)# 方式 3:设置超时sock.settimeout(5.0)
异步 Socket 超时
import asyncioasync def async_timeout_examples(): """异步超时示例""" # 方式 1:wait_for 包装协程 try: reader, writer = await asyncio.wait_for( asyncio.open_connection('127.0.0.1', 8888), timeout=5.0 ) print("✅ 连接成功") except asyncio.TimeoutError: print("⏱️ 连接超时") except ConnectionRefusedError: print("❌ 连接被拒绝") # 方式 2:单独超时读操作 try: data = await asyncio.wait_for( reader.read(1024), timeout=10.0 ) except asyncio.TimeoutError: print("⏱️ 读取超时") # 方式 3:单独超时写操作 try: writer.write(b"data") await asyncio.wait_for( writer.drain(), timeout=10.0 ) except asyncio.TimeoutError: print("⏱️ 写入超时")asyncio.run(async_timeout_examples())
🔄 重连和重试策略
重试策略对比
📝 生产级示例
健壮的客户端
import socketimport timeclass SimpleClient: def __init__(self, host, port, timeout=5): self.host = host self.port = port self.timeout = timeout self.sock = None def connect(self, retries=3): for i in range(retries): try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(self.timeout) self.sock.connect((self.host, self.port)) print("✅ 连接成功") return True except Exception as e: print(f"❌ 连接失败: {e}") if self.sock: self.sock.close() if i < retries - 1: time.sleep(2 ** i) return False def send(self, data): try: msg = data.encode() if isinstance(data, str) else data self.sock.sendall(msg) return True except Exception as e: print(f"❌ 发送失败: {e}") return False def recv(self, bufsize=1024): try: data = self.sock.recv(bufsize) return data.decode() if data else None except Exception as e: print(f"❌ 接收失败: {e}") return None def close(self): if self.sock: self.sock.close()# 使用client = SimpleClient('127.0.0.1', 8888)if client.connect(): client.send("Hello") response = client.recv() print(f"收到: {response}") client.close()
健壮的服务器
import socketimport threadingclass SimpleServer: def __init__(self, host='0.0.0.0', port=8888): self.host = host self.port = port self.server = None def start(self): try: self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server.bind((self.host, self.port)) self.server.listen(5) print(f"✅ 服务器启动: {self.host}:{self.port}") while True: try: client, addr = self.server.accept() print(f"✅ 连接: {addr}") thread = threading.Thread( target=self.handle_client, args=(client, addr) ) thread.daemon = True thread.start() except KeyboardInterrupt: break except Exception as e: print(f"❌ 错误: {e}") finally: self.stop() def handle_client(self, client, addr): client.settimeout(30) try: while True: data = client.recv(1024) if not data: break print(f"📨 收到: {data.decode()}") client.sendall(data) # Echo except Exception as e: print(f"❌ 客户端 {addr} 错误: {e}") finally: client.close() def stop(self): if self.server: self.server.close() print("✅ 服务器已停止")# 使用server = SimpleServer()try: server.start()except KeyboardInterrupt: print("👋 关闭服务器")
📋 注意事项清单
┌─────────────────────────────────────────────────────────┐│ 健壮网络编程注意事项 │├─────────────────────────────────────────────────────────┤│ ││ ✓ 始终设置超时时间(settimeout / wait_for) ││ ✓ 实现重试机制(指数退避) ││ ✓ 捕获特定异常(ConnectionRefusedError 等) ││ ✓ 使用异常链保留原始错误(raise ... from ...) ││ ✓ 确保资源关闭(finally / with / async with) ││ ✓ 记录详细日志(traceback / __cause__) ││ ✓ 处理连接断开(BrokenPipeError / ConnectionReset) ││ │└─────────────────────────────────────────────────────────┘
🎉 模块学习路线
┌─────────────────────────────────────────────────────────┐│ Socket 编程基础模块(第 70-73 天) │├─────────────────────────────────────────────────────────┤│ ││ 第 70 天 ✓ Socket API 基础:创建 TCP 与 UDP 连接 ││ │ • Socket 概念与原理 ││ │ • TCP vs UDP 对比 ││ │ ││ ▼ ││ 第 71 天 ✓ Python 3.14 中的异步 Socket 编程 ││ │ • asyncio 与 Socket 结合 ││ │ • 高并发服务器实现 ││ │ ││ ▼ ││ 第 72 天 ✓ Socket 错误处理与超时机制 ││ │ • 网络异常处理 ││ │ • 超时设置与重试 ││ │ • 异常链与日志 ││ │ ││ ▼ ││ 第 73 天 基于 Socket 的简单聊天应用实现 ││ • 综合实战项目 ││ • 多客户端聊天室 ││ │└─────────────────────────────────────────────────────────┘
📌 明日预告:基于 Socket 的简单聊天应用实现
明天我们将进入 Socket 编程模块最后一天,进行综合实战!
- 主题:基于 Socket 的简单聊天应用实现
- 核心问题:
- 如何构建多客户端聊天服务器?
- 如何实现消息广播机制?
- 如何处理客户端断开和重连?
- 如何设计简单的通信协议?
- 如何结合今天的错误处理机制?
💡 提前思考:
- 服务器如何知道哪个客户端发送了消息?
- 如果客户端突然断开,服务器如何清理资源?
- 如何防止恶意客户端发送大量数据?