Документация Python неофициальный перевод
Содержание страницы

21.21. socketserver – Фреймворк для сетевых серверовsocketserver – A framework for network servers

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


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

Существует четыре базовых класса серверов: TCPServer использует протокол TCP, который обеспечивает непрерывные потоки данных между клиентом и сервером. UDPServer использует дейтаграммы – дискретные пакеты информации, которые могут прийти не по порядку или потеряться при передаче. Реже используемые классы UnixStreamServer и UnixDatagramServer похожи, но используют сокеты домена Unix; они недоступны на платформах, отличных от Unix. Дополнительные сведения о сетевом программировании можно найти в таких книгах, как UNIX Network Programming У. Ричарда Стивенса или Win32 Network Programming Ральфа Дэвиса.

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

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

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

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

21.21.1. Примечания по созданию сервераServer Creation Notes

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

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

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

Версии с порождением процессов и потоками для каждого типа сервера можно создать с помощью миксин-классов ForkingMixIn и ThreadingMixIn. Например, класс потокового UDP-сервера создаётся следующим образом:

class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass

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

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

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

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

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

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

21.21.2. Объекты сервераServer Objects

class socketserver.BaseServer

Это суперкласс всех объектов серверов в модуле. Он определяет интерфейс, приведённый ниже, но не реализует большинство методов – это делается в подклассах.

BaseServer.fileno()

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

BaseServer.handle_request()

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

BaseServer.serve_forever(poll_interval=0.5)

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

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

BaseServer.service_actions()

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

Новое в версии 3.3.

BaseServer.shutdown()

Даёт команду циклу serve_forever() остановиться и ожидает его завершения.

BaseServer.address_family

Семейство протоколов, к которому принадлежит сокет сервера. Типичные примеры: socket.AF_INET и socket.AF_UNIX.

BaseServer.RequestHandlerClass

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

BaseServer.server_address

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

BaseServer.socket

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

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

BaseServer.allow_reuse_address

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

BaseServer.request_queue_size

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

BaseServer.socket_type

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

BaseServer.timeout

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

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

BaseServer.finish_request()

Непосредственно обрабатывает запрос, создавая экземпляр RequestHandlerClass и вызывая его метод handle().

BaseServer.get_request()

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

BaseServer.handle_error(request, client_address)

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

BaseServer.handle_timeout()

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

BaseServer.process_request(request, client_address)

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

BaseServer.server_activate()

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

BaseServer.server_bind()

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

BaseServer.verify_request(request, client_address)

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

21.21.3. Объекты RequestHandlerRequestHandler Objects

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

RequestHandler.finish()

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

RequestHandler.handle()

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

Тип self.request различается для датаграммных и потоковых служб. Для потоковых служб self.request – это объект сокета; для датаграммных – self.request представляет собой пару из строки и сокета. Однако это можно скрыть, используя подклассы обработчика запросов StreamRequestHandler или DatagramRequestHandler, которые переопределяют методы setup() и finish() и предоставляют атрибуты self.rfile и self.wfile. self.rfile и self.wfile можно читать или записывать соответственно для получения данных запроса или возврата данных клиенту.

RequestHandler.setup()

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

21.21.4. ПримерыExamples

21.21.4.1. socketserver.TCPServer Примерsocketserver.TCPServer Example

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

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    Класс RequestHandler для нашего сервера.

    Он создаётся один раз на каждое подключение к серверу и должен
    переопределять метод 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
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

    # Запускаем сервер; он будет работать, пока вы
    # не прервёте программу с помощью 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-сокет)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

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

    # Получить данные от сервера и завершить работу
    received = str(sock.recv(1024), "utf-8")
finally:
    sock.close()

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

21.21.4.2. socketserver.UDPServer Примерsocketserver.UDPServer Example

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

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
    server = socketserver.UDPServer((HOST, PORT), MyUDPHandler)
    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-сервера.

21.21.4.3. Асинхронные миксиныAsynchronous Mixins

Для создания асинхронных обработчиков используйте классы 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):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((ip, port))
    try:
        sock.sendall(bytes(message, 'ascii'))
        response = str(sock.recv(1024), 'ascii')
        print("Received: {}".format(response))
    finally:
        sock.close()

if __name__ == "__main__":
    # Порт 0 означает выбор произвольного неиспользуемого порта
    HOST, PORT = "localhost", 0

    server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
    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 используется так же, за исключением того, что сервер будет создавать новый процесс для каждого запроса.