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


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

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

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

При этом используется Интернет протокол TCP, который обеспечивает непрерывные потоки данных между клиентом и сервером. Если bind_and_activate истинно, конструктор автоматически пытается вызвать 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.

Вышеуказанные четыре класса обрабатывают запросы синхронно; каждый запрос должен завершиться до того, как может стартовать следующий запрос. Это не подходит, если для выполнения каждого запроса требуется много времени, потому что он требует большого объёма вычислений или потому, что он возвращает много данных, которые клиент медленно обрабатывает. Решение состоит в том, чтобы создать отдельный процесс или поток для обработки каждого запроса; классы миксины 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

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

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

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

socketserver.ThreadingMixIn.server_close() ожидает завершения всех потоков, не являющихся демонами, за исключением случаев, когда у атрибута socketserver.ThreadingMixIn.block_on_close ложное значение. Использовать демонические потоки, задав для 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

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

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

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

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

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

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

Server объекты

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 запросов. Как только очередь будет заполнена, дальнейшие запросы от клиентов получат ошибку «В соединении отказано». Значение по умолчанию обычно 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, и истек период тайм-аута, а запросы не получены. Действие по умолчанию для форкующих серверов состоит в том, чтобы собрать статус всех дочерних процессов, которые завершились, в то время как на серверах с потоковой передачей этот метод ничего не делает.

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: Добавлена поддержка протокола контекстного менеджера. Выход из менеджера контекста эквивалентен вызову server_close().

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

class socketserver.BaseRequestHandler

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

setup()

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

handle()

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

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

finish()

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

class socketserver.StreamRequestHandler
class socketserver.DatagramRequestHandler

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

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

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

Примеры

Пример socketserver.TCPServer

Серверная часть:

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    Класс обработчика запроса для нашего сервера.

    Он создаётся один раз для каждого подключения к серверу и должен переопределять
    метод handle() для реализации связи с клиентом.
    """

    def handle(self):
        # self.request - это TCP - сокет, подключённый к клиенту
        self.data = self.request.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # просто отправить обратно те же данные, но в верхнем регистре
        self.request.sendall(self.data.upper())

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

    # Создать серверный биндинг localhost на порту 9999
    with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
        # Активировать сервер; это будет продолжаться до тех пор, пока вы не прервёте
        # программу с помощью Ctrl-C
        server.serve_forever()

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

class MyTCPHandler(socketserver.StreamRequestHandler):

    def handle(self):
        # self.rfile - файлообразный объект, созданный обработчик; теперь мы можем
        # использовать, например, readline() вместо необработанных вызовов recv()
        self.data = self.rfile.readline().strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # Аналогично, self.wfile является файловым объектом, используемый для обратной
        # записи клиенту
        self.wfile.write(self.data.upper())

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

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

import socket
import sys

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

# Создать сокет (SOCK_STREAM означает TCP сокет)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    # Подключиться к серверу и отправить данные
    sock.connect((HOST, PORT))
    sock.sendall(bytes(data + "\n", "utf-8"))

    # Получение данных с сервера и завершение работы
    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):
    """
    Класс работает аналогично классу TCP обработчика, за исключением того, что
    self.request состоит из пары данных и клиентских сокет, и поскольку соединение
    отсутствует, адрес клиента должен быть задан явно при отправке данных обратно
    через 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 - тип сокета, используемый для UDP сокетов
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Как видно, вызов connect() отсутствует; UDP не имеет соединений. Вместо этого
# данные отправляются непосредственно получателю через 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__":
    # Порт 0 означает выбор произвольного неиспользуемого порта
    HOST, PORT = "localhost", 0

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

        # Начать поток с сервера, далее поток запустит еще один поток
        # для каждого запроса
        server_thread = threading.Thread(target=server.serve_forever)
        # Выйти из серверного потока, когда основной поток завершится
        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().