Вот мы добрались и до обработки нескольких клиентов на одном сервере.
с помощью модуля select. Сегодня наши клиенты будут общаться в общем чате, как в настоящем Телеграме.
Потоки ввода/вывода
Чтобы постичь основы highload в Python используют три базовых подхода:
- Отдельный поток на каждого клиента. Почти совсем не хайлоад, т.к. этот вариант потребляет достаточно много системных ресурсов, поэтому, при увеличении нагрузки на сервер, его использование будет опасным решением
- Неблокирующие сокеты. В Питоне с третей версии для реализации таких советов предусмотрен метод setblocking(), в который передается параметр, равный 0.
- Применение системных вызовов select() и poll() из модуля select — самое интересное. Системный вызов select() поддерживается всеми программными платформами, подразумевающими сетевое взаимодействие
Модуль select
Системный вызов select() можно использовать для опроса — или мультиплексирования — обработки нескольких потоков ввода-вывода, не используя потоки управления или дочерние процессы. В системах UNIX эти вызовы можно применять для работы с сокетами, каналами и многими другими типами файлов. В Windows — только для работы с сокетами.
select(r, w, e [, timeout])
В первых трех аргументах передаются списки с целочисленными дескрипторами файлов или с объектами, обладающими методом fileno(), который возвращает дескриптор файла
r — список объектов, которые передают информацию серверу (эти клиенты передают информацию на сервер)
w — список объектов, которые считывают информацию от сервера, но ничего ему не передают и не запрашивают у него (эти клиенты только принимают данные, то, что передает им сервер)
e — исключения
timeout — Таймаут необходим, чтобы проверять сокет на наличие подключений новых клиентов, на наличие данных
Системный вызов select() возвращает кортеж списков с объектами, находящимися в требуемом состоянии
Как это работает?
Массив дескрипторов клиентских сокетов можно передавать в качестве аргумента функции select, а она в свою очередь предоставляет список сокетов, которые:
- Готовы принять новые данные;
- Имеют новые данные для чтения;
- Содержат ошибки выполнения.
Конструкция select — это даже не функция, а системный вызов. Она заложена не в сам Python, а в операционную систему. Таким образом, традиционный подход с простым перебором заменяется на более эффективный, с особым оптимизированным алгоритмом.
Пример использования
Яркой демонстрацией модуля select является отправка/прием сообщений между сервером и несколькими клиентами. Для этого мы сначала создадим служебный файл, запускающий несколько «клиентов» с использованием модуля subprocess. Пусть это будет service.py
[code]
from subprocess import Popen, CREATE_NEW_CONSOLE
import os
process_list = [] #сюда будут попадать все клиентские процессы
while True:
user = input(«Запустить 10 клиентов (start) / Закрыть клиентов (close) / Выйти (quit) «)
if user == ‘quit’: #если пользователь ввел quit, то останавливаем цикл
break
elif user == ‘start’: #если пользователь ввел start, то запускаем процессы в консоли
for _ in range(10):
process_list.append(Popen(‘python client.py’,
creationflags=CREATE_NEW_CONSOLE)) #CREATE_NEW_CONSOLE — открываем каждый процесс в новой консоли
print(‘ Запущено 10 клиентов’)
elif user == ‘close’: #если пользователь ввел close, то дропаем процессы
for process in process_list:
process.kill()
process_list.clear() #очищаем список
Далее мы напишем серверную часть — server.py [code]
# серверная часть (server.py)
import select
from socket import socket, AF_INET, SOCK_STREAM
def clients_read(r_clients, clientlist): # чтение клиентских запросов
responses = {} # Словарь ответов сервера вида {сокет: запрос}
#for sock in clients:
for sock in r_clients:
try:
data = sock.recv(1024).decode(‘utf-8’)
responses[sock] = data #записываем ключ в словаре responses в виде данных
except:
print(‘Клиент {} {} отключился’.format(sock.fileno(), sock.getpeername()))
clientlist.remove(sock)
return responses
def clients_write(requests, w_clients, all_clients): # ответы клиентам на их запросы
for sock in w_clients:
if sock in requests:
try:
# Подготовить и отправить ответ сервера
response = requests[sock].encode(‘utf-8’)
# Ответ сделаем чуть непохожим на оригинал
sock.send(response.lower())
except: # Сокет недоступен, клиент отключился
print(‘Клиент {} {} отключился’.format(sock.fileno(), sock.getpeername()))
sock.close()
all_clients.remove(sock)
def mainserver():
address = (‘localhost’, 8888)
clients = []
s = socket(AF_INET, SOCK_STREAM)
s.bind(address)
s.listen(5)
s.settimeout(0.2) # Таймаут для операций с сокетом
while True:
try:
conn, addr = s.accept() # Проверка подключений
except OSError as e:
pass # timeout вышел
else:
print(«Получен запрос на соединение от %s» % str(addr))
clients.append(conn)
finally:
# Проверить наличие событий ввода-вывода
wait = 10
r = []
w = []
try:
r, w, e = select.select(clients, clients, [], wait)
except:
pass # Ничего не делать, если какой-то клиент отключился
requests = clients_read(r, clients) # Принимаем запросы клиентов
if requests: #если есть запрос
clients_write(requests, w, clients) # то формируем ответ и отправляем его
print(‘Server is RUN’)
mainserver()
Касаемо функций — clients_read() обходит список советов на чтение и формируется словарь запросов в формате, а clients_write() обходит сокеты на запись. Запросы обрабатываются на основе присланных клиентских данных, обрабатываются и отсылаются сервером на основании словаря запросов requests. Основная функция сервера — mainserver(). Она открывает сокет на прослушивание с соответствующим таймаутом для операций ввода/вывода. Проверка подключений осуществляется функцией accept() и добавляет новое подключение в список clients, изначально пустой. Далее функцией select мы читаем данные из клиентских сокетов и записываем данные в них (для подключений которые попали в список clients). Сама функция select() возвращает кортеж из трех списков: файловые дескрипторы на чтение, на запись и имеющие исключение. На очереди — клиентская часть (client.py)
[code]
# Клиентская часть (client.py)
from socket import *
#from select import select
import sys
PLACE = (‘localhost’, 8888) #куда подключаемся
def client():
with socket(AF_INET, SOCK_STREAM) as sock: # Создать сокет TCP # конструкция with (менеджер контекста) означает, что сокет будет автоматически закрыт
sock.connect(PLACE) # Коненктимся с сервером
while True:
message = input(‘Ваше сообщение: ‘)
sock.send(message.encode(‘utf-8’)) # отправляем, кодировав в байты
receive = sock.recv(1024).decode(‘utf-8’) # принимаем, декодировав в юнион
print(‘Ответ:’, receive)
if __name__ == ‘__main__’:
client()
В результате, при запуске служебного файла service.py получим 10 разных клиентов в 10 окнах консоли, обрабатываемыми одним сервером с помощью модуля select
