Содержание страницы
20.19. socketserver – Фреймворк для сетевых серверов¶socketserver – A framework for network servers
Модуль 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, не завершатся.
Классы серверов имеют одни и те же внешние методы и атрибуты, независимо от того, какой сетевой протокол они используют.
20.19.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 для другого способа управления этим.
20.19.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 секунд.
- 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.
20.19.3. Объекты RequestHandler¶RequestHandler Objects
Класс обработчика запросов должен определять новый метод handle() и может переопределять любые из следующих методов. Для каждого запроса создаётся новый экземпляр.
- RequestHandler.finish()¶
- Вызывается после метода handle() для выполнения необходимых действий по очистке. Реализация по умолчанию ничего не делает. Если setup() или handle() вызывают исключение, эта функция не будет вызвана.
- 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 можно читать или записывать соответственно для получения данных запроса или возврата данных клиенту.
20.19.4. Примеры¶Examples
20.19.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("%s wrote:" % self.client_address[0])
print(self.data)
# просто отправляет обратно те же данные, но в верхнем регистре
self.request.send(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("%s wrote:" % self.client_address[0])
print(self.data)
# Аналогично, self.wfile – это файлоподобный объект, используемый для записи ответа
# клиенту
self.wfile.write(self.data.upper())
Разница в том, что вызов readline() во втором обработчике будет вызывать recv() несколько раз, пока не встретит символ новой строки, в то время как единственный вызов recv() в первом обработчике просто вернёт то, что было отправлено клиентом в одном вызове send().
Это клиентская сторона:
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)
# Подключиться к серверу и отправить данные
sock.connect((HOST, PORT))
sock.send(bytes(data + "\n","utf8"))
# Получить данные от сервера и завершить работу
received = sock.recv(1024)
sock.close()
print("Sent: %s" % data)
print("Received: %s" % 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: b'HELLO WORLD WITH TCP'
$ python TCPClient.py python is nice
Sent: python is nice
Received: b'PYTHON IS NICE'
20.19.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("%s wrote:" % 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","utf8"), (HOST, PORT))
received = sock.recv(1024)
print("Sent: %s" % data)
print("Received: %s" % received)
Результат работы примера должен выглядеть точно так же, как для примера TCP-сервера.
20.19.4.3. Асинхронные примеси¶Asynchronous Mixins
Для создания асинхронных обработчиков используйте классы ThreadingMixIn и ForkingMixIn.
Пример для класса ThreadingMixIn:
import socket
import threading
import socketserver
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
data = self.request.recv(1024)
cur_thread = threading.current_thread()
response = bytes("%s: %s" % (cur_thread.getName(), data),'ascii')
self.request.send(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))
sock.send(message)
response = sock.recv(1024)
print("Received: %s" % response)
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.setDaemon(True)
server_thread.start()
print("Server loop running in thread:", server_thread.name)
client(ip, port, b"Hello World 1")
client(ip, port, b"Hello World 2")
client(ip, port, b"Hello World 3")
server.shutdown()
Результат работы примера должен выглядеть примерно так:
$ python ThreadedTCPServer.py
Server loop running in thread: Thread-1
Received: b"Thread-2: b'Hello World 1'"
Received: b"Thread-3: b'Hello World 2'"
Received: b"Thread-4: b'Hello World 3'"
Класс ForkingMixIn используется так же, за исключением того, что сервер будет создавать новый процесс для каждого запроса.