Подтвердить что ты не робот

Shutdown socketserver serve_forever() в однопоточном приложении Python

Я знаю, что у 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()

Мне известно, что этот код не работает. Я просто хотел показать вам то, что я хотел бы выполнить, - завершить работу сервера и выйти из приложения во время ожидания входящих данных, когда пользователь нажимает Ctrl-C.

4b9b3361

Ответ 1

Вы можете запустить другой поток локально, в вашем обработчике и вызвать 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()

Несколько заметок:

  • Чтобы убить сервер, выполните POST запрос http://localhost:8000/kill_server.
  • Я создаю функцию, которая вызывает server.shutdown() и запускает ее из другого потока для решения проблемы, которую мы обсуждаем.
  • Я использую совет fooobar.com/questions/107918/..., чтобы сделать сокет мгновенно доступным для повторного использования (вы можете снова запустить сервер, не имея [Errno 98] Address, уже использующего ошибку). С запасом TCPServer вам придется ждать подключения к таймауту, чтобы снова запустить сервер.

Ответ 2

В библиотеке 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

Я полагаю, что намерение OP состоит в том, чтобы отключить сервер от обработчика запросов, и я думаю, что аспект KeyboardInterrupt его кода просто путают вещи.

Нажатие ctrl-c из оболочки, где работает сервер, удастся закрыть ее, не делая ничего особенного. Вы не можете нажать ctrl-c из другой оболочки и ожидать, что она будет работать, и я думаю, что это может быть причина, из-за которой возникает этот запутанный код. Нет необходимости обрабатывать KeyboardInterrupt в handle() при попытке OP, или около serve_forever(), как это было предложено другим. Если вы этого не сделаете, он работает как ожидалось.

Единственный трюк здесь - и это сложно - говорит серверу о завершении работы с обработчиком без блокировки.

Как объясняет OP и показывает в своем коде, он использует однопоточный сервер, поэтому пользователь, который предложил закрыть его в "другом потоке", не обращает внимания.

Я выкопал код SocketServer и обнаружил, что класс BaseServer, стремящийся работать с потоковыми миксинами, доступными в этом модуле, на самом деле затрудняет использование с не-потоковыми серверами, используя threading.Event вокруг цикла в serve_forever.

Итак, я написал модифицированную версию 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

Ответ 4

Если вы не поймаете KeyboardInterrupt в обработчике tcp (или если вы его повторно поднимите), он должен просочиться в корневой вызов, в этом случае вызов 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()

Ответ 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()

Ответ 6

Вы всегда можете попробовать сигналы:

Импортный сигнал import os

# код внутреннего обработчика, который решил отключить:

os.kill(os.getpid(), signal.SIGHUP) # отправить себя sighup

Ответ 7

Я использовал это в Python 3.7.4

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