4、Socket异常处理:常见异常类型与捕获策略
说实话,Socket编程里最让人头疼的,不是业务逻辑怎么写,而是那些突如其来的异常。我见过太多线上事故,都是因为异常处理没做好——连接断了没人管,数据发了一半卡死了,资源泄漏导致服务器撑不住。这一章,我就把我在企业级项目里踩过的坑、总结的经验,一次性讲清楚。
4.1 常见异常类型:你得认识它们
先说说最常见的几种异常。你写Socket代码时,八成都会遇到。
| 异常类型 | 触发场景 | 我的经验 |
|---|---|---|
BlockingIOError |
非阻塞模式下,操作无法立即完成 | 常见于高并发场景,socket设为非阻塞但读写时机不对 |
ConnectionResetError |
对端突然关闭连接(比如进程崩溃) | 我遇到过客户端断网后重连,服务端还在读旧socket |
ConnectionAbortedError |
连接被对端软件中止 | 通常是超时或协议层主动断开 |
TimeoutError |
操作超时(连接、读写等) | 网络抖动时最常见,别一超时就放弃 |
OSError |
底层系统错误(端口被占用、文件描述符耗尽) | 这是个大类,很多异常都是它的子类 |
注意:Python 3 里,
socket.error 已经被合并到 OSError。你捕获异常时,直接写 except OSError 就能覆盖大部分情况。但如果你需要区分具体类型,还是得按子类来。
4.2 异常捕获策略:别一把抓
很多新手喜欢写 except Exception,然后打印个日志就完事。这其实很危险。你想想看,如果连内存溢出、键盘中断都被你吞掉了,程序死都不知道怎么死的。
我个人习惯的做法是分层捕获:
import socket
import select
import errno
def handle_client_connection(client_sock):
try:
# 业务逻辑
data = client_sock.recv(4096)
if not data:
# 对端关闭连接
handle_graceful_close(client_sock)
return
process_data(data)
except BlockingIOError:
# 非阻塞模式下,没有数据可读是正常的
# 我一般会记录一下,然后继续轮询
log.debug("No data available yet, retrying...")
except ConnectionResetError:
# 对端强制断开,需要清理资源
log.warning("Connection reset by peer")
cleanup_socket(client_sock)
except TimeoutError:
# 超时了,可以重试或者断开
log.error("Operation timed out")
retry_or_close(client_sock)
except OSError as e:
# 其他系统级错误
if e.errno == errno.ECONNRESET:
log.warning("Connection reset (errno check)")
else:
log.critical(f"Unexpected OS error: {e}")
cleanup_socket(client_sock)
except Exception as e:
# 兜底,但一定要记录完整堆栈
log.exception(f"Unhandled exception: {e}")
cleanup_socket(client_sock)
我的建议:异常捕获要「宽进严出」。入口处可以宽泛一点,但每个分支都要明确处理。千万别写
except: pass——我曾经在一个项目里看到这种写法,结果线上服务挂了三天没人发现,因为异常全被吞了。
4.3 超时设置:别让程序死等
没有超时的Socket,就像没有刹车的车。你永远不知道网络什么时候会卡住。我记得有一次,一个爬虫程序因为目标服务器响应慢,所有线程都卡在 recv 上,最后把连接池撑爆了。
设置超时有三种方式:
- 全局超时:通过
socket.settimeout()设置,所有操作都受这个时间限制。 - 模块级超时:比如
select.select()的 timeout 参数,只影响当前调用。 - 应用层超时:自己维护一个时间戳,在循环里检查是否超时。
import socket
import time
# 方式一:全局超时
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0) # 所有操作最多等5秒
try:
sock.connect(("example.com", 80))
sock.sendall(b"GET / HTTP/1.1\r\n\r\n")
data = sock.recv(4096)
except socket.timeout:
print("操作超时了")
# 方式二:应用层超时(我比较推荐的方式)
sock.setblocking(False)
deadline = time.time() + 5.0
while time.time() < deadline:
try:
data = sock.recv(4096)
if data:
break
except BlockingIOError:
# 没数据,继续等
time.sleep(0.1)
continue
else:
print("应用层超时,主动放弃")
避坑指南:我曾经在非阻塞模式下忘了设置超时,结果
select.select() 返回空列表后,程序直接进入死循环。后来我加了一个最大重试次数,才把问题解决。记住:超时不是摆设,它是你程序的最后一道防线。
4.4 优雅关闭连接:别摔门就走
关闭连接这件事,看似简单,其实门道很多。直接 close() 就像摔门而出——你永远不知道门后面有没有人还在说话。
优雅关闭的核心是:告诉对端「我要走了」,然后等对端也确认「好的,我知道了」。
具体做法:
- 调用
shutdown(socket.SHUT_WR),告诉对端「我不再发数据了」。 - 继续接收对端的数据,直到
recv()返回空字节串。 - 调用
close()释放资源。
def graceful_close(sock):
try:
# 第一步:停止发送
sock.shutdown(socket.SHUT_WR)
# 第二步:等待对端关闭
while True:
try:
data = sock.recv(4096)
if not data:
break # 对端也关闭了
# 如果有剩余数据,可以处理或丢弃
log.debug(f"Received leftover data: {len(data)} bytes")
except socket.timeout:
# 超时了,不等了
log.warning("Timeout while waiting for peer to close")
break
except ConnectionResetError:
# 对端已经强制关闭
break
except OSError as e:
# 可能socket已经关闭了
log.debug(f"Socket already closed: {e}")
finally:
# 第三步:释放资源
try:
sock.close()
except OSError:
pass
我的习惯:在 finally 块里调用
close(),确保资源一定被释放。另外,shutdown() 和 close() 都可能抛出异常,所以每个调用都要单独 try-except。别嫌麻烦,线上环境里,资源泄漏比业务bug更难排查。
4.5 知识体系总览
说了这么多,我画了一张图帮你理清思路。异常处理不是孤立的,它贯穿Socket的整个生命周期。
你看,异常类型是基础,捕获策略是手段,超时设置是保障,优雅关闭是收尾。四者缺一不可。我在做企业级项目时,会把这张图贴在团队Wiki里,每次Code Review都对照着检查。
总结一下:异常处理不是「出了问题再修」,而是「提前想好怎么应对」。你写的每一行异常处理代码,都是在为线上稳定性买单。别偷懒,别侥幸。