Python关闭Server后如何彻底释放端口实现同端口重启服务
问题说明
需要实现如下服务启停逻辑:同进程内可以在指定端口创建服务,调用stop方法关闭服务后,不需要退出Python进程,即可在相同端口重新创建新的服务实例。
预期运行代码逻辑如下:
>>> s0 = Server(port='5000', handle=print) >>> s0.stop() >>> # 换端口启动第二个服务可以正常运行 >>> s1 = Server(port='5001', handle=print) >>> s1.stop() >>> # 预期:重新在5000端口启动服务不报错、可正常接收请求 >>> # 实际问题:5000端口仍被s0占用,要么抛异常要么新服务收不到请求,必须退出Python进程才能释放端口,删除对象也无法解决 >>> s2 = Server(port='5000', handle=print)
此前尝试过Flask框架、SimpleXMLRPCServer实现,参考过BaseHTTPServer的停止方案,都存在端口无法及时释放的问题。
问题根因
端口无法释放、同端口重启服务失败,基本都是两个原因导致的:
- 停止服务时只终止了请求处理的事件循环,没有显式关闭底层绑定端口的监听socket,内核层面端口仍被当前进程持有
- 没有开启socket的地址复用配置:TCP协议规定socket主动关闭后会进入
TIME_WAIT状态,持续1-2分钟,默认配置下这个状态的端口不允许被重新绑定
可落地实现方案
实现时只要对应解决两个根因即可,核心要点:
- 初始化服务socket时,手动设置
SO_REUSEADDR选项,允许端口在TIME_WAIT状态下被重新绑定 - 停止服务时,先终止事件循环,再显式关闭监听socket,彻底释放端口占用
- 建议将服务运行在独立守护线程,避免阻塞主线程,同时防止线程残留占用端口
标准库实现示例(无需第三方依赖)
基于Python标准库http.server实现,完全满足预期逻辑:
import socket import threading from http.server import HTTPServer, BaseHTTPRequestHandler class Server: def __init__(self, port, handle): self.port = int(port) self.handle_func = handle self.http_server = None self.server_thread = None # 定义请求处理器 class _RequestHandler(BaseHTTPRequestHandler): def do_GET(self): # 调用外部传入的处理逻辑 handle(self.path, dict(self.headers)) self.send_response(200) self.end_headers() self.wfile.write(b"Service OK") # 屏蔽默认访问日志,不需要可以删除 def log_message(self, format, *args): return # 初始化服务时先不绑定端口,预留设置socket选项的时机 self.http_server = HTTPServer( ("0.0.0.0", self.port), _RequestHandler, bind_and_activate=False ) # 核心配置:开启地址复用 self.http_server.socket.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) # 完成端口绑定、启动监听 self.http_server.server_bind() self.http_server.server_activate() # 启动独立守护线程跑服务 self.server_thread = threading.Thread( target=self.http_server.serve_forever, daemon=True ) self.server_thread.start() def stop(self): if not self.http_server: return # 第一步:停止请求处理事件循环 self.http_server.shutdown() # 第二步:显式关闭监听socket,释放端口 self.http_server.server_close() self.http_server = None self.server_thread = None
功能验证
运行如下测试代码,可以看到三个服务都能正常响应,s2复用5000端口不需要重启进程:
import time import requests if __name__ == "__main__": # 启动5000端口第一个服务 s0 = Server(port=5000, handle=print) time.sleep(0.3) resp = requests.get("http://127.0.0.1:5000/s0") print(f"s0响应状态码:{resp.status_code}") s0.stop() time.sleep(0.2) # 启动5001端口服务 s1 = Server(port=5001, handle=print) time.sleep(0.3) resp = requests.get("http://127.0.0.1:5001/s1") print(f"s1响应状态码:{resp.status_code}") s1.stop() time.sleep(0.2) # 重新在5000端口启动服务 s2 = Server(port=5000, handle=print) time.sleep(0.3) resp = requests.get("http://127.0.0.1:5000/s2") print(f"s2响应状态码:{resp.status_code}") s2.stop()
Flask框架适配要点
如果使用Flask实现,额外注意两个配置即可:
- 启动服务时关闭自动重载:
app.run(use_reloader=False),避免自动重载机制启动的子进程残留占用端口 - 不要使用Flask默认的服务器启动逻辑,需要自定义WSGI服务器,手动给socket设置
SO_REUSEADDR选项,停止时依次调用shutdown()、server_close()方法释放端口。
常见踩坑点
- 仅调用
shutdown()不调用server_close():事件循环停了但socket没关,端口会一直被进程占用 - 不设置
SO_REUSEADDR:就算正常关闭socket,也要等1-2分钟TIME_WAIT超时才能重新绑定同端口 - 服务跑在非守护线程:主线程执行完其他逻辑后,服务线程还在后台运行,端口不会释放
内容的提问来源于stack exchange,提问作者Lazy Bum
相关产品推荐
相关产品推荐

