Python socketserver 退出后为何不能立即释放 socket?
创建服务器前,先通过子类设置 allow_reuse_address = True,并保留现有的 with 语句。如果程序退出后暂时无法重新绑定端口,这通常是合适的处理方式。该设置允许符合条件的地址复用,但不会清除 TCP 的 TIME_WAIT 状态。
目前没有 socket 状态或重启报错信息,还不能直接认定原因是 TIME_WAIT。
为什么关闭了 socket,端口仍可能暂时不能复用?
原代码已经使用 with socketserver.TCPServer(...) as server:。退出上下文时,server_close() 会关闭监听 socket。正常的 Control-C 异常退出,以及当前单线程程序中未被拦截的 sys.exit(0),都会退出这个上下文。Python socketserver 文档、sys.exit 文档
监听 socket 与接受请求后产生的连接 socket 是不同的对象。连接关闭后,内核可能仍保留 TIME_WAIT 状态,影响后续的地址绑定。连接记录仍然存在,并不代表 Python 还持有监听 socket。Python socket 文档
在绑定前启用地址复用
定义一个服务器子类,并用它替换 socketserver.TCPServer:
class ReusableTCPServer(socketserver.TCPServer):
allow_reuse_address = True
TCPServer 默认在构造时绑定地址,因此需要在创建实例之前设置这个属性。该选项会让服务器在绑定前设置 SO_REUSEADDR。Python socketserver 源码
下面是完整示例。示例补齐了输入代码块中缺少的缩进,并处理了 recv() 返回空字节串的情况,避免客户端断开后接收循环一直运行。
#!/usr/bin/env python3
import sys
import socketserver
host = '0.0.0.0'
port = 12345
class ReusableTCPServer(socketserver.TCPServer):
allow_reuse_address = True
class MyTCPHandler(socketserver.BaseRequestHandler):
def handle(self):
pieces = []
total = 0
while total < 10_000:
chunk = self.request.recv(min(2000, 10_000 - total))
if not chunk:
break
pieces.append(chunk)
total += len(chunk)
if b'\n' in chunk:
break
self.data = b''.join(pieces)
print(f"Received from {self.client_address[0]}:")
print(self.data.decode("utf-8"))
self.request.sendall(self.data.upper())
# sys.exit(0)
def main():
try:
with ReusableTCPServer((host, port), MyTCPHandler) as server:
server.serve_forever()
except KeyboardInterrupt:
pass
return 0
if __name__ == '__main__':
sys.exit(main())
捕获 KeyboardInterrupt 是为了正常结束,避免显示异常堆栈;监听 socket 仍由 with 关闭。示例沿用原代码对 UTF-8 输入的假设。
如果只想处理一次请求后结束,可以把 server.serve_forever() 改为 server.handle_request(),并让处理器正常返回,无需在处理器中调用 sys.exit(0)。
修改后第一次启动仍失败怎么办?
Linux 的地址复用规则要求之前绑定端口的 socket 和新绑定的 socket 都设置 SO_REUSEADDR。如果旧程序没有启用该选项,它留下的连接状态可能仍会阻止修改后的程序首次绑定,这时可能需要等待旧状态结束。Linux socket(7) 手册
可以用以下命令区分仍在监听的进程与 TIME_WAIT 连接:
ss -ltnp 'sport = :12345'
ss -tan state time-wait 'sport = :12345'
第一条如果显示监听记录,应检查对应进程是否仍在运行;查看其他用户的进程信息可能需要相应权限。第二条用于查看本地端口为 12345 的 TIME_WAIT 记录。ss(8) 手册
退出处理还要注意什么?
不要在这个同步处理器中调用 self.server.shutdown()。如果 shutdown() 与 serve_forever() 在同一线程运行,会造成死锁。shutdown() 用于通知另一个线程中的服务循环停止;server_close() 负责清理服务器资源。Python shutdown 文档
Control-C 的退出路径不能代表所有信号。其他信号触发时是否执行 Python 清理逻辑,取决于信号处理方式。结束进程或关闭文件描述符也不能保证 TCP 状态立即消失。需要快速重启服务时,应处理地址复用问题,而不是反复调用 close()。
备注:内容仅供参考。
