Когда “еще один пуллинг каждый N секунд” стучится вам в код. Время подумать про вебсокеты a.k.a полнодуплексное соединение.

Речь пойдет про socket.io , не совсем web socket а скорее микс при участии web socket. Но очень удобный в использовании сразу из коробки. 

Кейс состоит в следующем: 

  • Есть многопользовательское приложение в котором пользователи запускают асинхронные операции на сервере. Другими словами нажимают на кнопку в приложении и ждут когда сервер выполнит все операции а походу еще и расскажет про текущее состояние.

Вроде бы можно обойтись лонг пуллингом, но некоторые действия хочется заблокировать пользователям которые находятся за другими мониторами на той же странице. Да и вообще говоря сама библиотека при отсутствии возможности ws/wss соединения будет использовать пуллинг. Так что вроде бы только плюсы, из минусов только еще один пакет на клиенте и сервере.

Для клиента необходима библиотека socket-io.client, для фронта все написано на React и само соединение можно установить/разорвать через хуки. Для тех кто использует redux-toolkit все можно сделать через RTK.

У вас должен быть базовый объект createApi({ … }) который описывает все эндпоинты и который потом, так же можно расширить.

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const baseApi = createApi({
    reducerPath: 'baseApi',
    baseQuery: fetchBaseQuery({ baseUrl: '/' }),
    endpoints: () => ({}),
});

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

const appReducer = combineReducers({
	…,
[baseApi.reducerPath]: baseApi.reducer,
	…
});

В middleware добавляем =>  baseApi.middleware

И расширяем свой базовый baseApi функционалом для создания вебсокета.

import { baseApi } from '../api';
import { io } from 'socket.io-client';

export const wsApi = baseApi.injectEndpoints({
    endpoints: build => ({
        subscribeToEvents: build.query<any, void>({
            queryFn: () => ({ data: [] }),
            async onCacheEntryAdded(_arg, { dispatch, updateCachedData, cacheEntryRemoved }) {
                // Path is a prefix that will be used right after domain name
                const socket = io(`${your_url}/events`, {
                    path: '/socket.io',
                });

                socket.on('disconnect', reason => {
                    if (reason === 'io server disconnect') {
                        // the disconnection was initiated by the server, you need to reconnect manually
                        socket.connect();
                    }
                    // else the socket will automatically try to reconnect
                });

                socket.on(‘EVENT_TYPE’, (event: ServerEvent) => {
                    // Here we should add the logic
                    updateCachedData(draft => {
                        draft.push(event);
                    });
                });

                await cacheEntryRemoved;
                socket.close();
            },
        }),
    }),
    overrideExisting: false,
});

export const { useSubscribeToEventsQuery } = wsApi;

injectEndpoints работает как раз для расширения эндпоинтов, только не забудьте добавить overrideExisting: false чтобы расширить а не переопределить существующий функционал.

useSubscribeToEventsQuery(); можно использовать как обычный хук, в том компоненте в котором желаете подписаться на события, например в App. Еще в api есть свойство keepUnusedDataFor для того чтобы задать время в секундах существования подключения/данных после последнего unsubscribe, по умолчанию 60 секунд.

На сервере используется Ts.Ed (Node js), но также существуют готовые библиотеки на других языках. Нужно проинсталлировать пакеты связанные с socket.io для сервера. И дела за малым, добавить конфиг:

socketIO: {
	path: '/socket.io',
	cors: {
		origin: '*' // put your servers
	}
} 

И добавить сервис который выполняет подключение/отключение клиента а так же отправляет и принимает сообщения.

import { IO, Nsp, Socket, SocketService, SocketSession } from "@tsed/socketio";
import * as SocketIO from "socket.io";

@SocketService("/events") // namespace right after path ‘socket.io’
export class MySocketService {

    @Nsp nsp: SocketIO.Namespace | undefined;

    // a map to keep clients by any id you like, a userId or whatever.
    public clients: Map<string, SocketIO.Socket> = new Map();


    constructor(@IO private io: SocketIO.Server) {
    }

    /**
     * Triggered when a new client connects to the Namespace.
     */
    $onConnection(@Socket socket: SocketIO.Socket, @SocketSession session: SocketSession) {
        this.clients.set(socket.id, socket);
    }

    // setup a method to send data to all clients
    // you can use this from any other service or controller.
    broadcast(someData: any): void {
        this.nsp.emit(‘EVENT_TYPE’, { … }: ServerEvent);
    }

    // method to send to a targeted client
    sendToSingleClient(idToSendTo: string, someData: any): void {
        const socket = this.clients.get(idToSendTo);
        if (!socket) return;
        socket.emit(‘EVENT_TYPE’, { … }: ServerEvent);
    }

}

Namespace в socket.io это путь "/event" после основного пути в настройках socketIO: { path: 'socket.io' }. Теперь можно в любом необходимом месте заинжектить сервис и отправить сообщение клиентам.

Вообщем то как-то так, после этого клиент может получать сообщения и выполнять необходимые side effects. ????????

Комментарии (9)


  1. polearnik
    10.12.2021 12:19
    +4

    я бы предостерег от использования пакета socket.io . На сервере не держал больше 200 соединений на ядро. Взамен могу предложить https://github.com/uNetworking/uWebSockets.js . Эта библиотека прокидывает вебсокет соединения напрямую к линукс ядру. ПО утверждению разработчика работает в 10 раз быстрее socket.io/заодно есть и http сервер


    1. IonianWind
      10.12.2021 13:21
      +1

      1. muturgan
        11.12.2021 13:14

        Вот это огонь :)

        Мне сейчас не очень удобно смотреть, я не до конца понял. Это уже готовая фича или пока предложение?


    1. LborV
      10.12.2021 23:44
      +1

      Я думаю не правильно приготовленно, до сих пор(аптайм около года) в проде крутится сервис на сокет ио с рпс более 2к


      1. muturgan
        11.12.2021 14:21

        Думаю это зависит от интенсивности использования специфичных фич socket.io. Банальный бродкаст думаю не даёт большого оверхеда. А вот комнаты и личные сообщения не реализованы в нативном ws и соответственно дают оверхед. А это уже зависит от бизнес задач а не от неумения готовить.


        1. LborV
          12.12.2021 10:05

          тут верно, впринципе комнаты и вот это всё считаю ересью и предпочитаю использовать сокет ио в качестве "замены" resta


      1. polearnik
        11.12.2021 16:04

        узнать бы еще как отловить причину по которой отсылка сообщений тормозит хотя проц и память не нагружены


  1. gameplayer55055
    10.12.2021 16:03

    Зачем нужен сокетио? Если что-то простое можно ванильное использовать. У ноды и у питона есть из коробки, по сути вебсокет это подкастрированный и видоизмененный http.

    Если нужны подписки смотрите на библиотеки websocket + wamp. Но для онлайн игр лучше сырой вебсокет и свои пакеты под себя


    1. monochromer
      10.12.2021 16:41
      +1

      У ноды и у питона есть из коробки

      А что там есть у Node.js из коробки?

       по сути вебсокет это подкастрированный и видоизмененный http

      Разве? HTTP используется только при инициализации (отправка заголовков `Connection: Upgrade` и `Upgrade: websocket`), а затем делается upgrade соединения.