Использование WebSocket с Python

Что такое WebSocket?

Обычно для обмена данными в интернете используется протокол передачи гипертекста (HTTP, HyperText Transfer Protocol). Он работает по алгоритму запрос/ответ. Когда веб-браузеру нужны данные с веб-сервера, он выполняет запрос, на который веб-сервер возвращает данные через ответ:

Чтобы поддерживать актуальную версию веб-страницы, браузеру необходимо достаточно часто выполнять запросы для получения данных с веб-сервера. Этот процесс называется опросом (polling). Если данные нужно обновлять постоянно, он становится чрезмерно затратным, поскольку и клиент, и сервер постоянно выполняют запросы и ответы. С другой стороны, постоянный опрос бесполезен, если данные на сервере меняются редко (или нерегулярно). Но если уменьшить частоту опроса, браузер может пропустить важные обновления данных.

Гораздо больше возможностей предлагает технология WebSocket, которая предоставляет канал связи на основе сокетов между веб-браузером и веб-сервером. Такое полностью дуплексное соединение, позволяющее и клиенту, и серверу одновременно отправлять и получать данные, весьма полезно для веб-приложений, работающих в режиме реального времени.

Алгоритм работы WebSoket показан на изображении ниже.

  • Сначала веб-браузер инициирует запрос к веб-серверу, отправляя HTTP-заголовок “can we upgrade to websocket”.
  • Если веб-сервер поддерживает WebSocket, он возвращает ответ с заголовком “OK to upgrade to websocket”.
  • Затем устанавливается долговременное двунаправленное соединение с сокетом.
  • И клиент, и сервер смогут обмениваться данными, используя это сокет-соединение.

С WebSocket также можно использовать для подключения к сокетам порты 80 (http) и 443 (https). Это позволяет работать с WebSocket даже через прокси-сервер и брандмауэр.

Создание сервера WebSocket

Для создания сервера WebSocket в Python можно использовать пакет websockets, установив его с помощью команды pip:

$ pip install websockets

Вводим следующий блок кода в файл, который назовем server.py:

import asyncio
import websockets

async def handler(websocket):
while True:
try:
message = await websocket.recv()
print('Message received from client: ', message)
await websocket.send("Message from server: " + message)
except Exception as e:
print(e)
break

async def main():
async with websockets.serve(handler, "", 8001): # listen at port 8001
await asyncio.Future() # run forever

if __name__ == "__main__":
asyncio.run(main())

Приведенный выше код создает сервер WebSocket, прослушивающий порт 8001 (можно использовать и порт 80) и ожидающий подключения. Сразу после подключения к клиенту, он будет постоянно ожидать входящих сообщений. Получив от клиента сообщение, сервер отправляет его обратно клиенту.

Тестирование сервера

Для тестирования сервера введем следующую команду Python:

$ python server.py

Клиент WebSocket (1-й вариант)

После запуска сервера создадим клиент WebSocket. Проще всего написать клиент WebSocket с помощью пакета websocket-client:

$ pip install websocket-client

Создайте новый файл client.py и заполните его следующими инструкциями:

from websocket import create_connection

ws = create_connection("ws://localhost:8001")

while True:
msg = input('Enter a message: ')
if msg == 'quit':
ws.close()
break
ws.send(msg)
result = ws.recv()
print ('> ', result)

В приведенном выше примере вы подключаетесь к серверу WebSocket, используя этот URL: ws://localhost:8001. Он запускает бесконечный цикл, ожидающий ввода строки пользователем. Как только строка введена, она отправляется на сервер. После ввода пользователем “quit” соединение разрывается и прерывается бесконечный цикл.

Тестирование клиента

Чтобы протестировать клиента, введите:

$ python client.py

После ввода строки нажмите Enter. Строка будет отправлена на сервер, а затем обратно клиенту:

На стороне сервера появится отправленное клиентом сообщение:

Клиент WebSocket (2-й вариант)

При втором варианте создания клиента используется тот же пакет websockets, что и для сборки сервера:

import asyncio
import websockets

async def main():
async with websockets.connect("ws://localhost:8001") as ws:
while True:
msg = input('Enter a message: ')
if msg == 'quit':
ws.close()
break
await ws.send(msg)
result = await ws.recv()
print('> ', result)
asyncio.run(main())

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

Модифицированный сервер для широковещательной передачи всем клиентам

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

Теперь создадим более полноценные сервер и клиент со следующими возможностями.

  • Клиент предлагает пользователю ввести строку для отправки на сервер.
  • Сервер будет транслировать полученные сообщения всем подключенным клиентам.
  • Клиент должен иметь возможность получать сообщения от сервера в любое время после того, как сервер получит сообщение.

Создание сервера

Для сервера сделать это довольно просто:

import asyncio
import websockets

# сохраняет всех клиентов, подключенных к серверу
client_list = []

async def handler(websocket):
client_list.append(websocket)
while True:
try:
message = await websocket.recv()
print('Message received from client: ', message)
await broadcast(message)
except Exception as e:
print(e)
client_list.remove(websocket)
break

async def broadcast(message):
for client in client_list:
# транслирует сообщение другому клиенту
await client.send(message)

async def main():
async with websockets.serve(handler, "", 8001):
await asyncio.Future() # работает бесконечно

if __name__ == "__main__":
asyncio.run(main())

В приведенном выше примере создаем список client_list для хранения всех подключенных клиентов. Определяем новую функцию async с именем broadcast(), которая будет отправлять полученное сообщение всем клиентам из client_list. В случае отключения клиента соединение удаляется из списка.

Создание клиента

Немного сложнее создать клиента. Сначала я использовал для этого следующий код:

import asyncio
import websockets

async def receive_messages(websocket):
while True:
try:
message = await websocket.recv()
print(">", message)
except websockets.exceptions.ConnectionClosed:
break

async def send_messages(websocket):
while True:
user_input = input(':')
await websocket.send(user_input)
await asyncio.sleep(0) # передача управления циклу event

async def main():
uri = "ws://localhost:8001"
async with websockets.connect(uri) as websocket:
receive_task = asyncio.create_task(receive_messages(websocket))
send_task = asyncio.create_task(send_messages(websocket))
await asyncio.gather(receive_task, send_task)

asyncio.run(main())

В этом примере есть три функции async:

  • receive_messages() служит для получения сообщений с сервера, позволяет запустить бесконечный цикл прослушивания входящих сообщений.
  • send_messages() запрашивает у пользователя ввод строки, отправляемой на сервер. После этого у пользователя запрашивается очередное сообщение.
  • main() служит для подключения к серверу и асинхронного запуска функций receive_messages() и send_messages().

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

В конце концов я решил использовать низкоуровневый threading API  —  _thread для синхронизации между приемом и отправкой сообщений. Вместо асинхронной версии websockets я импортировал синхронную функцию connect() для подключения к серверу, а затем использовал пакет _thread, чтобы организовать прием сообщений сервера в отдельном потоке и отправку сообщений в основном потоке.

Вот что получилось в файле с именем client2.py:

import websockets
from websockets.sync.client import connect
import _thread

def receive_messages(websocket):
while True:
try:
message = websocket.recv()
print(">", message)
except websockets.exceptions.ConnectionClosed:
break

with connect("ws://localhost:8001") as ws:
_thread.start_new_thread(receive_messages,(ws,)) # прослушивание с сервера
while True:
toSend = input()
ws.send(toSend)

Запускаем два экземпляра клиента в двух окнах терминала:

После ввода текста в первом клиенте и нажатия Enter, строка будет отправлена на сервер, а затем передана всем подключенным клиентам:

Аналогично, если ввести текст во втором клиенте и нажать Enter, сообщение также будет получено первым клиентом:

В любое время любой клиент может ввести текст, а все остальные клиенты смогут получать эти сообщения.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Wei-Meng Lee: Using WebSocket in Python

Предыдущая статьяКак уменьшить объем шаблонного кода в тестах Kotlin
Следующая статьяСоздание кольцевой диаграммы на JavaScript