You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 12:33:34