跨平台TCP中继服务器非阻塞Socket异常疑问
问题描述
我正在编写一款可同时部署在Windows和Linux平台的TCP中继服务器(共用代码库),涉及两个Socket操作,需要明确以下内容:
异常场景确认
- 非阻塞Socket的
recv()在无数据可读时触发的具体异常; sendall()无法完成全部数据发送时触发的具体异常。
细节确认
- 处理非阻塞Socket的
recv()时,是否需要同时检查socket.EWOULDBLOCK和socket.EAGAIN两种errno? sendall()失败时会返回何种errno(即e.args[0])?
核心顾虑
sendall()失败时是否会返回EAGAIN或EWOULDBLOCK?若如此会引发问题。
附当前代码:
try: socket1.setblocking(False) socket2.setblocking(False) while True: try: sock1_data = socket1.recv(1024) if sock1_data: socket2.sendall(sock1_data) except socket.error as e: if e.args[0] != socket.EAGAIN and e.args[0] != socket.EWOULDBLOCK: raise e try: sock2_data = socket2.recv(1024) if sock2_data: socket1.sendall(sock2_data) except socket.error as e: if e.args[0] != socket.EAGAIN and e.args[0] != socket.EWOULDBLOCK: raise e except: pass finally: if socket2: socket2.close() if socket1: socket1.close()
问题解答
1. 非阻塞recv()无数据时的异常
非阻塞模式下,recv()无数据可读时会抛出socket.error异常:
- Linux下,errno是
EAGAIN或EWOULDBLOCK(二者数值相同); - Windows下,errno仅为
EWOULDBLOCK。
2. sendall()无法完成发送时的异常
sendall()会循环调用底层send()直到所有数据发完,中途出错直接抛出socket.error:
- 非阻塞模式下,若发送缓冲区满导致无法继续写入,会抛出带
EAGAIN/EWOULDBLOCK的异常; - 若遇到连接断开、网络故障等问题,会抛出对应errno的异常(比如
ECONNRESET)。
3. 是否需要同时检查EAGAIN和EWOULDBLOCK?
必须同时检查。虽然Linux上两个errno值相同,但Windows只定义了EWOULDBLOCK,没提供EAGAIN。不同时检查的话,Windows环境下会漏判非阻塞recv()的正常无数据场景,导致错误抛出异常。
4. sendall()失败时的errno
- 缓冲区满导致发送中断:返回
EAGAIN(Linux)或EWOULDBLOCK(Windows); - 连接类错误:返回
ECONNRESET(对方重置连接)、EPIPE(连接已关闭)等; - 其他网络错误:返回对应标准errno(如
ENETUNREACH表示网络不可达)。
核心顾虑回应
是的,非阻塞Socket调用sendall()确实会返回EAGAIN/EWOULDBLOCK——当发送缓冲区已满,无法继续写入数据时就会触发。你的当前代码有隐患:sendall()抛出的这类异常会被外层的except:直接捕获忽略,导致数据发送不完整却被当成正常情况处理,最终丢数据。
代码改进建议
要给sendall()相关逻辑添加异常处理,区分EAGAIN/EWOULDBLOCK和其他错误,同时还要缓存未发送的数据后续重试(否则会丢数据):
import socket try: socket1.setblocking(False) socket2.setblocking(False) # 新增:缓存未发送的数据 send_buf1 = b"" send_buf2 = b"" while True: # 处理socket1接收并转发到socket2 try: sock1_data = socket1.recv(1024) if sock1_data: send_buf2 += sock1_data # 尝试发送缓存的数据 while send_buf2: try: sent = socket2.send(send_buf2) send_buf2 = send_buf2[sent:] except socket.error as e: if e.args[0] not in (socket.EAGAIN, socket.EWOULDBLOCK): raise e break # 缓冲区满,退出循环等待下次可写 except socket.error as e: if e.args[0] not in (socket.EAGAIN, socket.EWOULDBLOCK): raise e # 处理socket2接收并转发到socket1 try: sock2_data = socket2.recv(1024) if sock2_data: send_buf1 += sock2_data # 尝试发送缓存的数据 while send_buf1: try: sent = socket1.send(send_buf1) send_buf1 = send_buf1[sent:] except socket.error as e: if e.args[0] not in (socket.EAGAIN, socket.EWOULDBLOCK): raise e break except socket.error as e: if e.args[0] not in (socket.EAGAIN, socket.EWOULDBLOCK): raise e except Exception as e: # 替换原有的pass,添加错误日志便于排查 print(f"服务器异常: {str(e)}") finally: if 'socket2' in locals() and socket2: socket2.close() if 'socket1' in locals() and socket1: socket1.close()
另外,当前的空循环方式效率极低,建议改用selectors模块实现IO多路复用,监听Socket的可读/可写事件,避免无意义的CPU占用。
内容的提问来源于stack exchange,提问作者chakmeshma
相关产品推荐
相关产品推荐

