socketserver — Фреймворк для сетевых серверов

Исходный код: Lib/socketserver.py.


Модуль socketserver упрощает задачу написания сетевых серверов.

Существует четыре основных класса конкретных серверов:

class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)

При этом используется интернет-протокол TCP, который обеспечивает непрерывный поток данных между клиентом и сервером. Если bind_and_activate равен true, конструктор автоматически пытается вызвать server_bind() и server_activate(). Остальные параметры передаются базовому классу BaseServer.

class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)

Здесь используются дейтаграммы, которые представляют собой дискретные пакеты информации, которые могут прийти не по порядку или быть потеряны во время транспортировки. Параметры те же, что и для TCPServer.

class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate=True)

Эти более редко используемые классы похожи на классы TCP и UDP, но используют сокеты домена Unix; они недоступны на платформах, отличных от Unix. Параметры те же, что и для TCPServer.

Эти четыре класса обрабатывают запросы synchronously; каждый запрос должен быть завершен, прежде чем может быть запущен следующий запрос. Это не подходит, если каждый запрос выполняется долго, потому что требует много вычислений, или потому что возвращает много данных, которые клиент обрабатывает медленно. Решением является создание отдельного процесса или потока для обработки каждого запроса; для поддержки асинхронного поведения можно использовать смешанные классы ForkingMixIn и ThreadingMixIn.

Создание сервера требует нескольких шагов. Во-первых, необходимо создать класс обработчика запросов, подклассифицировав класс BaseRequestHandler и переопределив его метод handle(); этот метод будет обрабатывать входящие запросы. Во-вторых, вы должны инстанцировать один из классов сервера, передав ему адрес сервера и класс обработчика запросов. Рекомендуется использовать сервер в операторе with. Затем вызовите метод handle_request() или serve_forever() объекта сервера для обработки одного или многих запросов. Наконец, вызовите server_close() для закрытия сокета (если вы не использовали оператор with).

При наследовании от ThreadingMixIn для поведения потокового соединения, вы должны явно объявить, как вы хотите, чтобы ваши потоки вели себя при внезапном завершении работы. Класс ThreadingMixIn определяет атрибут daemon_threads, который указывает, должен ли сервер ждать завершения потока. Вы должны явно установить этот флаг, если хотите, чтобы потоки вели себя автономно; по умолчанию стоит False, что означает, что Python не завершит работу до тех пор, пока не завершатся все потоки, созданные ThreadingMixIn.

Классы серверов имеют одинаковые внешние методы и атрибуты, независимо от того, какой сетевой протокол они используют.

Примечания по созданию сервера

На диаграмме наследования имеется пять классов, четыре из которых представляют синхронные серверы четырех типов:

+------------+
| BaseServer |
+------------+
      |
      v
+-----------+        +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+        +------------------+
      |
      v
+-----------+        +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+        +--------------------+

Обратите внимание, что UnixDatagramServer происходит от UDPServer, а не от UnixStreamServer — единственное различие между IP и потоковым сервером Unix - это семейство адресов, которое просто повторяется в обоих классах серверов Unix.

class socketserver.ForkingMixIn
class socketserver.ThreadingMixIn

С помощью этих классов-микс-ин можно создавать форкинговые и потоковые версии каждого типа сервера. Например, ThreadingUDPServer создается следующим образом:

class ThreadingUDPServer(ThreadingMixIn, UDPServer):
    pass

Класс mix-in стоит на первом месте, поскольку он переопределяет метод, определенный в UDPServer. Установка различных атрибутов также изменяет поведение базового серверного механизма.

ForkingMixIn и классы Forking, упомянутые ниже, доступны только на платформах POSIX, поддерживающих fork().

socketserver.ForkingMixIn.server_close() ожидает завершения всех дочерних процессов, за исключением случаев, когда атрибут socketserver.ForkingMixIn.block_on_close имеет значение false.

socketserver.ThreadingMixIn.server_close() ожидает завершения всех недемонических потоков, за исключением случаев, когда атрибут socketserver.ThreadingMixIn.block_on_close равен false. Используйте демонические потоки, установив ThreadingMixIn.daemon_threads в True, чтобы не ждать завершения потоков.

Изменено в версии 3.7: socketserver.ForkingMixIn.server_close() и socketserver.ThreadingMixIn.server_close() теперь ожидает завершения всех дочерних процессов и недемонических потоков. Добавьте новый атрибут класса socketserver.ForkingMixIn.block_on_close, чтобы отказаться от поведения, существовавшего до версии 3.7.

class socketserver.ForkingTCPServer
class socketserver.ForkingUDPServer
class socketserver.ThreadingTCPServer
class socketserver.ThreadingUDPServer

Эти классы предварительно определяются с помощью классов mix-in.

Чтобы реализовать сервис, вы должны вывести класс из BaseRequestHandler и переопределить его метод handle(). Затем вы можете запускать различные версии сервиса, комбинируя один из классов сервера с классом обработчика запросов. Класс обработчика запросов должен быть разным для служб дейтаграмм и потоков. Это можно скрыть, используя подклассы обработчика StreamRequestHandler или DatagramRequestHandler.

Конечно, все равно нужно использовать голову! Например, нет смысла использовать forking-сервер, если сервис содержит в памяти состояние, которое может быть изменено различными запросами, поскольку модификации в дочернем процессе никогда не достигнут начального состояния, хранящегося в родительском процессе и передаваемого каждому дочернему. В этом случае вы можете использовать потоковый сервер, но вам, вероятно, придется использовать блокировки для защиты целостности общих данных.

С другой стороны, если вы создаете HTTP-сервер, где все данные хранятся вне системы (например, в файловой системе), синхронный класс сделает сервис «глухим» на время обработки одного запроса - что может быть очень долго, если клиент медленно получает все данные, которые он запросил. Здесь уместен потоковый или вилочный сервер.

В некоторых случаях может быть целесообразно обрабатывать часть запроса синхронно, а завершать обработку в вилочном дочернем процессе в зависимости от данных запроса. Это можно реализовать, используя синхронный сервер и выполнив явную вилку в методе handle() класса обработчика запроса.

Другой подход к обработке нескольких одновременных запросов в среде, которая не поддерживает ни потоки, ни fork() (или когда они слишком дороги или не подходят для сервиса), заключается в ведении явной таблицы частично завершенных запросов и использовании selectors для принятия решения о том, какой запрос обрабатывать следующим (или обрабатывать ли новый входящий запрос). Это особенно важно для потоковых сервисов, где каждый клиент потенциально может быть подключен в течение длительного времени (если нельзя использовать потоки или подпроцессы). Смотрите asyncore для другого способа управления этим.

Объекты сервера

class socketserver.BaseServer(server_address, RequestHandlerClass)

Это суперкласс всех объектов Server в модуле. Он определяет интерфейс, приведенный ниже, но не реализует большинство методов, что делается в подклассах. Два параметра хранятся в соответствующих атрибутах server_address и RequestHandlerClass.

fileno()

Возвращает целочисленный дескриптор файла для сокета, на котором прослушивается сервер. Эта функция чаще всего передается в selectors, чтобы обеспечить мониторинг нескольких серверов в одном процессе.

handle_request()

Обработка одного запроса. Эта функция вызывает следующие методы по порядку: get_request(), verify_request() и process_request(). Если пользовательский метод handle() класса обработчика вызовет исключение, то будет вызван метод handle_error() сервера. Если в течение timeout секунд запрос не получен, будет вызван метод handle_timeout() и возвращен handle_request().

serve_forever(poll_interval=0.5)

Обрабатывать запросы до явного запроса shutdown(). Опрашивать о выключении каждые poll_interval секунд. Игнорирует атрибут timeout. Он также вызывает service_actions(), который может быть использован подклассом или миксином для обеспечения действий, специфичных для данной службы. Например, класс ForkingMixIn использует service_actions() для очистки зомбированных дочерних процессов.

Изменено в версии 3.3: Добавлен вызов service_actions к методу serve_forever.

service_actions()

Этот метод вызывается в цикле serve_forever(). Этот метод может быть переопределен подклассами или классами-миксинами для выполнения действий, специфичных для данного сервиса, например, действий по очистке.

Добавлено в версии 3.3.

shutdown()

Скажите циклу serve_forever() остановиться и подождите, пока он это сделает. shutdown() должен быть вызван, пока serve_forever() выполняется в другом потоке, иначе он зайдет в тупик.

server_close()

Очистить сервер. Может быть переопределена.

address_family

Семейство протоколов, к которому принадлежит сокет сервера. Общими примерами являются socket.AF_INET и socket.AF_UNIX.

RequestHandlerClass

Класс обработчика запросов, предоставляемый пользователем; экземпляр этого класса создается для каждого запроса.

server_address

Адрес, по которому прослушивается сервер. Формат адресов зависит от семейства протоколов; подробности см. в документации к модулю socket. Для интернет-протоколов это кортеж, содержащий строку с адресом и целочисленный номер порта: ('127.0.0.1', 80), например.

socket

Объект сокета, на котором сервер будет прослушивать входящие запросы.

Классы сервера поддерживают следующие переменные класса:

allow_reuse_address

Будет ли сервер разрешать повторное использование адреса. По умолчанию это значение равно False, и может быть установлено в подклассах для изменения политики.

request_queue_size

Размер очереди запросов. Если на обработку одного запроса уходит много времени, то все запросы, которые поступают, пока сервер занят, помещаются в очередь, вплоть до request_queue_size запросов. Как только очередь заполнится, дальнейшие запросы от клиентов будут получать ошибку «Connection denied». Значение по умолчанию обычно равно 5, но оно может быть переопределено подклассами.

socket_type

Тип сокета, используемого сервером; socket.SOCK_STREAM и socket.SOCK_DGRAM - два распространенных значения.

timeout

Продолжительность тайм-аута, измеряемая в секундах, или None, если тайм-аут не требуется. Если handle_request() не получает входящих запросов в течение периода таймаута, вызывается метод handle_timeout().

Существуют различные методы сервера, которые могут быть переопределены подклассами базовых классов сервера, например TCPServer; эти методы не полезны для внешних пользователей объекта сервера.

finish_request(request, client_address)

Фактически обрабатывает запрос, инстанцируя RequestHandlerClass и вызывая его метод handle().

get_request()

Должен принять запрос от сокета и вернуть кортеж, содержащий новый объект сокета, который будет использоваться для связи с клиентом, и адрес клиента.

handle_error(request, client_address)

Эта функция вызывается, если метод handle() экземпляра RequestHandlerClass вызывает исключение. Действие по умолчанию заключается в том, чтобы вывести отслеживание в стандартную ошибку и продолжить обработку дальнейших запросов.

Изменено в версии 3.6: Теперь вызывается только для исключений, производных от класса Exception.

handle_timeout()

Эта функция вызывается, если атрибут timeout был установлен в значение, отличное от None, и прошло время тайм-аута, в течение которого не было получено ни одного запроса. Действием по умолчанию для forking-серверов является сбор статуса всех дочерних процессов, которые завершились, в то время как для threading-серверов этот метод ничего не делает.

process_request(request, client_address)

Вызывает finish_request() для создания экземпляра RequestHandlerClass. При желании эта функция может создать новый процесс или поток для обработки запроса; это делают классы ForkingMixIn и ThreadingMixIn.

server_activate()

Вызывается конструктором сервера для активации сервера. Поведение по умолчанию для TCP-сервера просто вызывает listen() на сокете сервера. Может быть переопределена.

server_bind()

Вызывается конструктором сервера для привязки сокета к нужному адресу. Может быть переопределена.

verify_request(request, client_address)

Должна возвращать булево значение; если значение равно True, запрос будет обработан, а если False, запрос будет отклонен. Эта функция может быть переопределена для реализации контроля доступа для сервера. Реализация по умолчанию всегда возвращает True.

Изменено в версии 3.6: Добавлена поддержка протокола context manager. Выход из контекстного менеджера эквивалентен вызову server_close().

Объекты обработчика запросов

class socketserver.BaseRequestHandler

Это суперкласс всех объектов обработчика запросов. Он определяет интерфейс, приведенный ниже. Конкретный подкласс обработчика запросов должен определить новый метод handle() и может переопределить любой из других методов. Для каждого запроса создается новый экземпляр подкласса.

setup()

Вызывается перед методом handle() для выполнения любых необходимых действий по инициализации. Реализация по умолчанию ничего не делает.

handle()

Эта функция должна выполнить всю работу, необходимую для обслуживания запроса. Реализация по умолчанию ничего не делает. Для нее доступны несколько атрибутов экземпляра; запрос доступен как self.request; адрес клиента как self.client_address; и экземпляр сервера как self.server, в случае, если ей нужен доступ к информации о каждом сервере.

Тип self.request отличается для дейтаграммных и потоковых служб. Для потоковых служб self.request - это объект сокета; для дейтаграммных служб self.request - это пара string и socket.

finish()

Вызывается после метода handle() для выполнения любых необходимых действий по очистке. Реализация по умолчанию ничего не делает. Если setup() вызывает исключение, эта функция не будет вызвана.

class socketserver.StreamRequestHandler
class socketserver.DatagramRequestHandler

Эти подклассы BaseRequestHandler переопределяют методы setup() и finish() и предоставляют атрибуты self.rfile и self.wfile. Атрибуты self.rfile и self.wfile могут быть соответственно прочитаны или записаны, чтобы получить данные запроса или вернуть данные клиенту.

Атрибуты rfile обоих классов поддерживают интерфейс io.BufferedIOBase readable, а DatagramRequestHandler.wfile поддерживает интерфейс io.BufferedIOBase writable.

Изменено в версии 3.6: StreamRequestHandler.wfile также поддерживает записываемый интерфейс io.BufferedIOBase.

Примеры

socketserver.TCPServer Пример

Это сторона сервера:

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    The request handler class for our server.

    It is instantiated once per connection to the server, and must
    override the handle() method to implement communication to the
    client.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # just send back the same data, but upper-cased
        self.request.sendall(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    # Create the server, binding to localhost on port 9999
    with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
        # Activate the server; this will keep running until you
        # interrupt the program with Ctrl-C
        server.serve_forever()

Альтернативный класс обработчика запросов, который использует потоки (файлоподобные объекты, которые упрощают коммуникацию, предоставляя стандартный файловый интерфейс):

class MyTCPHandler(socketserver.StreamRequestHandler):

    def handle(self):
        # self.rfile is a file-like object created by the handler;
        # we can now use e.g. readline() instead of raw recv() calls
        self.data = self.rfile.readline().strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # Likewise, self.wfile is a file-like object used to write back
        # to the client
        self.wfile.write(self.data.upper())

Разница заключается в том, что вызов readline() во втором обработчике будет вызывать recv() несколько раз, пока не встретит символ новой строки, в то время как единственный вызов recv() в первом обработчике просто вернет то, что было отправлено от клиента за один вызов sendall().

Это клиентская сторона:

import socket
import sys

HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])

# Create a socket (SOCK_STREAM means a TCP socket)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    # Connect to server and send data
    sock.connect((HOST, PORT))
    sock.sendall(bytes(data + "\n", "utf-8"))

    # Receive data from the server and shut down
    received = str(sock.recv(1024), "utf-8")

print("Sent:     {}".format(data))
print("Received: {}".format(received))

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

Сервер:

$ python TCPServer.py
127.0.0.1 wrote:
b'hello world with TCP'
127.0.0.1 wrote:
b'python is nice'

Клиент:

$ python TCPClient.py hello world with TCP
Sent:     hello world with TCP
Received: HELLO WORLD WITH TCP
$ python TCPClient.py python is nice
Sent:     python is nice
Received: PYTHON IS NICE

socketserver.UDPServer Пример

Это сторона сервера:

import socketserver

class MyUDPHandler(socketserver.BaseRequestHandler):
    """
    This class works similar to the TCP handler class, except that
    self.request consists of a pair of data and client socket, and since
    there is no connection the client address must be given explicitly
    when sending data back via sendto().
    """

    def handle(self):
        data = self.request[0].strip()
        socket = self.request[1]
        print("{} wrote:".format(self.client_address[0]))
        print(data)
        socket.sendto(data.upper(), self.client_address)

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    with socketserver.UDPServer((HOST, PORT), MyUDPHandler) as server:
        server.serve_forever()

Это клиентская сторона:

import socket
import sys

HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])

# SOCK_DGRAM is the socket type to use for UDP sockets
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# As you can see, there is no connect() call; UDP has no connections.
# Instead, data is directly sent to the recipient via sendto().
sock.sendto(bytes(data + "\n", "utf-8"), (HOST, PORT))
received = str(sock.recv(1024), "utf-8")

print("Sent:     {}".format(data))
print("Received: {}".format(received))

Вывод примера должен выглядеть точно так же, как и в примере с TCP-сервером.

Асинхронные миксины

Для создания асинхронных обработчиков используйте классы ThreadingMixIn и ForkingMixIn.

Пример для класса ThreadingMixIn:

import socket
import threading
import socketserver

class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
        data = str(self.request.recv(1024), 'ascii')
        cur_thread = threading.current_thread()
        response = bytes("{}: {}".format(cur_thread.name, data), 'ascii')
        self.request.sendall(response)

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

def client(ip, port, message):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((ip, port))
        sock.sendall(bytes(message, 'ascii'))
        response = str(sock.recv(1024), 'ascii')
        print("Received: {}".format(response))

if __name__ == "__main__":
    # Port 0 means to select an arbitrary unused port
    HOST, PORT = "localhost", 0

    server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
    with server:
        ip, port = server.server_address

        # Start a thread with the server -- that thread will then start one
        # more thread for each request
        server_thread = threading.Thread(target=server.serve_forever)
        # Exit the server thread when the main thread terminates
        server_thread.daemon = True
        server_thread.start()
        print("Server loop running in thread:", server_thread.name)

        client(ip, port, "Hello World 1")
        client(ip, port, "Hello World 2")
        client(ip, port, "Hello World 3")

        server.shutdown()

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

$ python ThreadedTCPServer.py
Server loop running in thread: Thread-1
Received: Thread-2: Hello World 1
Received: Thread-3: Hello World 2
Received: Thread-4: Hello World 3

Класс ForkingMixIn используется аналогичным образом, за исключением того, что сервер будет порождать новый процесс для каждого запроса. Доступен только на POSIX платформах, поддерживающих fork().

Вернуться на верх