在单线程Python应用程序中如何关闭socketserver serve_forever()?

26

我知道socketserver有一个方法shutdown(),可以使服务器关闭,但这仅适用于多线程应用程序,因为需要从不同的线程调用关闭操作,而不能从运行serve_forever()的线程中调用。

我的应用程序一次只处理一个请求,因此我不使用单独的线程来处理请求,因此无法调用shutdown(),因为它会导致死锁(虽然文档没有说明,但在socketserver的源代码中明确说明了这一点)。

以下是我简化后的代码,以便更好地理解:

import socketserver

class TCPServerV4(socketserver.TCPServer):
  address_family = socket.AF_INET
  allow_reuse_address = True

class TCPHandler(socketserver.BaseRequestHandler):
  def handle(self):
    try:
       data = self.request.recv(4096)
    except KeyboardInterrupt:
       server.shutdown()

server = TCPServerV4((host, port), TCPHandler)
server.server_forever()

我知道这段代码不起作用,我只是想展示我想要实现的功能——在等待用户按下 CtrlC 时关闭服务器并退出应用程序。


单独的线程:https://dev59.com/bXVC5IYBdhLWcg3whRcw - Ciro Santilli OurBigBook.com
10个回答

27

您可以在处理程序中本地启动另一个线程,并从那里调用 shutdown

演示:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import SimpleHTTPServer
import SocketServer
import time
import thread

class MyHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
    def do_POST(self):
        if self.path.startswith('/kill_server'):
            print "Server is going down, run it again manually!"
            def kill_me_please(server):
                server.shutdown()
            thread.start_new_thread(kill_me_please, (httpd,))
            self.send_error(500)

class MyTCPServer(SocketServer.TCPServer):
    def server_bind(self):
        import socket
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)

server_address = ('', 8000)
httpd = MyTCPServer(server_address, MyHandler)
try:
    httpd.serve_forever()
except KeyboardInterrupt:
    pass
httpd.server_close()

注意:

  1. 要关闭服务器,请向 http://localhost:8000/kill_server 发送POST请求。
  2. 我创建了一个函数,调用 server.shutdown() 并从另一个线程运行它,以解决我们讨论的问题。
  3. 我使用了绑定套接字:“地址已在使用中”的建议,使套接字立即可用于重用(您可以再次运行服务器,而不会出现[Errno 98] Address already in use 错误)。对于库存TCPServer,您必须等待连接超时后才能重新运行服务器。

为什么要从serve_forever中捕获KeyboardInterrupt? - Jonathan
@Jonathan,这是使用server_close()正确释放套接字。因此,您可以重新启动服务器,而无需等待超时直到挂起的连接实际关闭。我已经重新测试了代码,在Python 2.7.3(Linux)上已经不再需要。感谢您的指出。 - dmitry_romanov

18

SocketServer库在处理继承属性时使用了一些奇怪的方法(猜测是由于使用旧式类)。如果您创建一个服务器并列出其受保护的属性,则会看到:

In [4]: server = SocketServer.TCPServer(('127.0.0.1',8000),Handler)
In [5]: server._

server._BaseServer__is_shut_down
server.__init__
server._BaseServer__shutdown_request
server.__module__
server.__doc__
server._handle_request_nonblock
如果您只是在请求处理程序中添加以下内容:
self.server._BaseServer__shutdown_request = True

服务器将关闭。这与调用server.shutdown()相同,只是不必等待(并死锁主线程)直到它关闭。


3
虽然不美观,但这仍然是我找到的最干净的方法:/ 标准库应该有一个“nowait”参数,使它可以在同一线程中调用。 - Romuald Brunet

5

实际上,正如源代码中所指出的,您应该在其他线程中调用shutdown

 def shutdown(self):
        """Stops the serve_forever loop.

        Blocks until the loop has finished. This must be called while
        serve_forever() is running in another thread, or it will
        deadlock.
        """
        self.__shutdown_request = True
        self.__is_shut_down.wait()

这是我发现有用的。谢谢。 - Tendai

4
我认为OP的意图是从请求处理程序关闭服务器,我认为他代码中的KeyboardInterrupt部分只是让事情更加混乱。在运行服务器的shell中按下Ctrl + C将成功关闭它,不需要做任何特殊的操作。你不能从不同的shell中按下Ctrl + C并期望它能工作,我认为这个想法可能是这个令人困惑的代码的来源。没有必要在handle()中处理KeyboardInterrupt,就像OP尝试的那样,或者像其他人建议的那样,在serve_forever()周围进行处理。如果两者都不做,它的工作效果如预期一样。
这里唯一的技巧——而且它很棘手——是在处理程序中告诉服务器关闭而不死锁。正如OP解释并在他的代码中展示的那样,他正在使用单线程服务器,因此建议在“其他线程”中关闭服务器的用户没有注意到这一点。
我查看了SocketServer代码,并发现BaseServer类在努力与此模块中可用的线程mixin配合使用时,实际上使其更难与非线程服务器一起使用,通过在serve_forever中的循环周围使用threading.Event。
因此,我为单线程服务器编写了serve_forever的修改版本,使得可以从请求处理程序关闭服务器。
import SocketServer
import socket
import select

class TCPServerV4(SocketServer.TCPServer):
    address_family = socket.AF_INET
    allow_reuse_address = True

    def __init__(self, server_address, RequestHandlerClass):
        SocketServer.TCPServer.__init__(self, server_address, RequestHandlerClass)
        self._shutdown_request = False

    def serve_forever(self, poll_interval=0.5):
        """provide an override that can be shutdown from a request handler.
        The threading code in the BaseSocketServer class prevented this from working
        even for a non-threaded blocking server.
        """
        try:
            while not self._shutdown_request:
                # XXX: Consider using another file descriptor or
                # connecting to the socket to wake this up instead of
                # polling. Polling reduces our responsiveness to a
                # shutdown request and wastes cpu at all other times.
                r, w, e = SocketServer._eintr_retry(select.select, [self], [], [],
                                       poll_interval)
                if self in r:
                    self._handle_request_noblock()
        finally:
            self._shutdown_request = False

class TCPHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        data = self.request.recv(4096)
        if data == "shutdown":
            self.server._shutdown_request = True

host = 'localhost'
port = 52000
server = TCPServerV4((host, port), TCPHandler)
server.serve_forever()

如果您向服务器发送字符串'shutdown',服务器将结束其serve_forever循环。您可以使用netcat进行测试:
printf "shutdown" | nc localhost 52000

我得到了一个错误:AttributeError: 'module' object has no attribute '_eintr_retry'。这个错误是由于SocketServer模块中没有_eintr_retry属性导致的,而我在调用select.select函数时使用了它。 - KumZ

3

我在Python 3.7.4中使用了这个。

def handle():
    setattr(self.server, '_BaseServer__shutdown_request', True)

2
如果在TCP处理程序中未捕获KeyboardInterrupt(或者重新引发它),它应该会传递到根调用,即server_forever()调用。
尽管我没有测试过这个方法,代码看起来应该像这样:
import socketserver  # Python2: SocketServer

class TCPServerV4(socketserver.TCPServer):
    address_family = socket.AF_INET
    allow_reuse_address = True

class TCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        data = self.request.recv(4096)

server = TCPServerV4((host, port), TCPHandler)

try:
    server.serve_forever()
except KeyboardInterrupt:
    server.shutdown()

1
不幸的是,它不起作用。我已经尝试过类似这样的东西。它会在终端中写入“异常发生”的警告并关闭连接(handle方法返回),但serve_forver仍在等待传入连接,一旦有一个连接被接受,应用程序将正常继续运行。要彻底退出应用程序,我需要按Ctrl-C,以便出现“异常发生”的警告,然后再次按Ctrl-C,以避免建立任何连接。 - samuelg0rd0n
我个人认为这是不可能的,我只有两个选择:要么将处理方法放在不同的线程中,要么不使用模块socketserver,因此手动创建服务器(socket、bind、listen等)。 - samuelg0rd0n
似乎对于http.server.HTTPServer,这种简单的方法是有效的。如果您使用Ctrl+C,则可以从同一线程正确关闭服务器。 - mihaipopescu

1

你可以尝试使用信号:

import signal
import os

# inside handler code that has decided to shutdown:
os.kill(os.getpid(), signal.SIGHUP)    # send myself sighup

0
我发现许多已发布的解决方案存在“未定义服务器”的问题。为了避免这种情况,可以在构造函数中保存服务器句柄。这样,就不需要第二个服务器来关闭服务器了。
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

class Handler(BaseHTTPRequestHandler):
    def __init__(self, request, client_address, server):
        # Save the server handle
        self.server = server
        BaseHTTPRequestHandler.__init__(self, request, client_address, server)

    # This can be do_POST or do_SET, whatever takes your fancy
    def do_GET(self):
        if 'exit' in self.path:
            # Commit suicide
            # Send OK
            self.send_response(200)
            self.send_header('Content-type', 'text/plain')
            self.end_headers()
        
            # Tell sender what is happening
            self.wfile.write(bytes('Shutting down server', 'utf-8'))
            # Shut down server
            self.server.shutdown()

# Start server
port_server = ThreadingHTTPServer(('localhost', 5000), Handler)
port_server.serve_forever()

0
我无法弄清楚如何在非线程服务器上完成这个任务。只有通过在一个线程中启动serve_forever循环,我才能使shutdown()函数正常工作,而且还是从另一个线程中调用的(具体来说,在self.__is_shut_down.wait()调用中遇到了阻塞/死锁问题,这也是另一个答案中提到的)。
class SimpleHttpServerAdapter:
    def init(self):
        self.__httpServer: HTTPServer | None = None

    def start(self):
        print("Starting simple HTTP server on port 8000")

        self.__httpServer = HTTPServer(("localhost", 8000), SimpleHTTPRequestHandler)

        def start_in_separate_thread(httpServer: HTTPServer | None):
            if httpServer:
                httpServer.serve_forever()

        thread = Thread(target=start_in_separate_thread, args=(self.__httpServer,))
        thread.start()

    def stop(self):
        print("Stopping simple HTTP server")

        def stop_in_separate_thread(httpServer: HTTPServer | None):
            if httpServer:
                httpServer.shutdown()

        thread = Thread(target=stop_in_separate_thread, args=(self.__httpServer,))
        thread.start()
        thread.join()

0
我不得不为C++的单元测试编写一个HTTP服务器。
我已经使用了这个适用于Python 3.x的新代码:
import os
import signal
from http.server import HTTPServer, SimpleHTTPRequestHandler

class MyHandler(SimpleHTTPRequestHandler):
    def do_GET(self):
        if self.path.startswith('/kill_server'):
            os.kill(os.getpid(), signal.SIGKILL)
        else:
            SimpleHTTPRequestHandler.do_GET(self)

server_address = ('', 1234)
httpd = HTTPServer(server_address, MyHandler)
try:
    httpd.serve_forever()
except Exception:
    httpd.shutdown()

将此脚本保存为my_http_server.py并调用HTTP服务器:
python3 my_http_server.py &

从浏览器内部,通过以下方式关闭服务器和进程:
http://127.0.0.1:1234/kill_server

对我来说,我必须在C++单元测试代码中使用这个服务器。 我使用C++代码将这个脚本保存为一个字符串,然后将其作为临时文件启动脚本:
system("python3 /tmp/my_http_server.py &");

然后在拆除单元测试时,发送一个GET请求到/kill_server已完成清理工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接