Basics of Python and Django. / Библиотека socketio. / Используем веб-сокеты с библиотекой socketio.

Используем веб-сокеты с библиотекой socketio.

Промышленные инструменты для вебсокетов https://www.emqx.io/ centrifugo.

Создадим образ для веб-сервера Dockerfile-nginx.

FROM nginx:latest
RUN mkdir /app
COPY ./default.conf /etc/nginx/conf.d/default.conf

Конфигурация виртуального хоста default.conf.

server { 
 listen 80;
 server_name localhost;

  location / {
    root /app;
    try_files $uri /index.html;
  }

}

Создадим образ для сокет-сервера Dockerfile-socketio.

FROM python:3.6
ENV PYTHONUNBUFFERED 1
RUN mkdir /app
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN apt update
RUN pip install -r requirements.txt
CMD python server.py

Создадим сборщик контейнеров docker-compose.yaml.

version: '3.5'
services: 
    socketio:
        build: 
            context: .
            dockerfile: Dockerfile-socketio
        volumes:
            - .:/app
        container_name: socket-io-server
        ports:
            - 8050:5000

    socketio-nginx:
        build: 
            context: .
            dockerfile: Dockerfile-nginx
        volumes:
            - .:/app
        ports:
            - 8088:80
        container_name: socket-io-nginx

Установим клиентские зависимости.

npm init
npm install jquery socket.io @types/socket.io --save

Создадим index.html

<!DOCTYPE html>
<html>
    <head><title>Socket io</title>
        <script src="/node_modules/socket.io/client-dist/socket.io.min.js"></script>
        <script src="/node_modules/jquery/dist/jquery.min.js"></script>
    </head>
    <body>
        <h1>Test socketio</h1>

        <button id="button">Test</button>

        <script>
           const socket = io('ws://localhost:8050', {transports:['websocket']});  
           socket.on('connect', () => {
                socket.on('message', msg => {
                    console.log(msg);
                })
           });

           $('#button').on('click',() => {
               socket.emit('test_message',{data: 'ping'});
           });

        </script>
        </body>
</html>

Создадим простой сокет-сервер server.py.

import eventlet
import socketio
sio = socketio.Server(cors_allowed_origins='*',async_mode='eventlet')
app = socketio.WSGIApp(sio)

@sio.event
def connect(sid, environ):
    print('connect ', sid)

@sio.event
def test_message(sid, data):
    print('message ', data)
    sio.emit('message', {'data': 'pong'}, broadcast=True, include_self=True)
    # sio.emit('my event', {'data': 'foobar'}, room=user_sid)

@sio.event
def disconnect(sid):
    print('disconnect ', sid)

if __name__ == '__main__':
    eventlet.wsgi.server(eventlet.listen(('', 5000)), app)

Возможности библиотеки

Пространства имен.

При коннекте можно указывать неймспейс.

ws://example.com:8000/chat

И потом с ним работать.

@sio.event(namespace='/chat')
def my_custom_event(sid, data):
    pass

Отправка сообщения определенному клиенту

sio.emit('my event', {'data': 'foobar'}, room=user_sid)

Добавление соединений в комнаты

@sio.event
def begin_chat(sid):
   sio.enter_room(sid, 'chat_users')

 @sio.event
 def exit_chat(sid):
     sio.leave_room(sid, 'chat_users')

После чего становится возможным отправка сообщения в комнату.

sio.emit('my reply', data, room='chat_users', skip_sid=sid)

Одно соединение может быть добавлено в множество комнат.

Сессия пользователя.

Есть возможность сохранять специфичные данные пользователя для каждого соединения.

@sio.event
def connect(sid, environ):
    sio.save_session(sid, {'username': 'Dima'})

@sio.event
def message(sid, data):
    session = sio.get_session(sid)

Брать данные сессии можно и так:

    with sio.session(sid) as session:
        session['username'] = username

Взаимодействие со сторонними процессами.

Часто возникает необходимость передать данные сокет-серверу из других приложений или процессов.

Для этого можно использовать посредника в виде БД Redis.

pip install redis

Создадим новый контейнер.

version: '3.5'
services: 
    socketio-server:
        ...
        depends_on:
            - "socketio-redis"

    ...


    socketio-redis:
        image: "redis:alpine"
        ports:
            - "6999:6379"

Зададим менеджер сообщений при создании объекта приложения сервера.

import eventlet
eventlet.monkey_patch()
mgr = socketio.RedisManager('redis://socketio-redis:6379/0')
sio = socketio.Server(cors_allowed_origins='*',async_mode='eventlet',client_manager=mgr)

socketio-redis - в докере можно пользоваться именами контейнеров для обозначений их хостов

Создадим переодическую рассылку серверного времени в отдельном файле timer.py.

import socketio
import time
from datetime import datetime
mgr = socketio.RedisManager('redis://localhost:6999/0', write_only=True)

while True:
    time.sleep(2)
    now = datetime.now()
    current_time = now.strftime("%H:%M:%S")
    mgr.emit('message', data={'time': current_time})
    print(current_time)

Сделаем обновление на клиенте.

    <div id="time"></div>

    <script>
       const socket = io('ws://localhost:8050', {transports:['websocket']});

       socket.on('connect', () => {

            socket.on('message', msg => {

                $('#time').html(msg.time);
            })
       });
     ...

Добавим новый контейнер.

....

socketio-server:
    ...
    command: ["python", "server.py"]

socketio-pinger:
    build: 
        context: .
        dockerfile: Dockerfile-socketio
    volumes:
        - .:/app
    container_name: socket-io-pinger
    depends_on:
        - "socketio-redis"
    command: ["python", "pinger.py"]

При этом удалив команду запуска приложения из Dockerfile-socketio и перенесем в docker-compose.