Создание многопользовательской игры с использованием Socket.io при помощи NodeJS и React

Важно начать проект с уникальной идеи, но куда важнее выбрать правильное направление.

“Будущее принадлежит тем, кто осваивает больше навыков и совмещает их в потоке творчества”, — Роберт Грин, “Мастерство”.

Это руководство детально объясняет, как ПРАВИЛЬНО создать пошаговую многопользовательскую игру при помощи Socket.io и React. Здесь будет рассмотрено как создание серверной части проекта при помощи ExpressJS, так и клиентской с помощью ReactJS.

К чему очередное руководство?

Это очень важно прояснить. В сети существует великое множество руководств, описывающих “Начало использования socket.io”, но негодование вызывает тот факт, что все они ведут к созданию приложений чатов. Здесь же мы “Начнём использовать socket.io с построения масштабируемого проекта”, который уж точно не будет чатом. 🙂

Это руководство раскроет больше подробностей относительно инфраструктуры кода вместо того, чтобы зацикливаться на UI/UX. Поэтому прошу набраться терпения, если UI покажется вам не столь привлекательным.

Что такое socket.io?

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

На заметку: в нашем случае Websocket и socket.io будут использоваться поочерёдно (несмотря на то, что они в некоторых аспектах отличаются), если не будет указано иного. 

Почему WebSocket, а не HTTP?

Для многопользовательских игр в реальном времени нам одновременно необходим клиент для отправки пакетов информации на сервер и сервер для отправки/рассылки данных. При помощи HTTP этого добиться не получится, поскольку клиенту для получения данных необходимо сперва отправить на сервер запрос. В многопользовательских играх такой сценарий не годится.

Что подразумевается под “правильно”?

Правильно в данном случае означает начать с базы кода, которая в дальнейшем может быть легко расширена, и не заморачиваться одновременно с мелкими проектами. В этом подходе рассматриваются общие практики, которым можно следовать и в более модульных проектах. Однако это ни в коей мере не говорит о том, что создавать WebSocket нужно именно так. Это лишь моё личное мнение, вы же можете с лёгкостью поменять в проекте всё, что захотите. 😀

О чём проект? ⚡⚡

Итак, перейдём к сути руководства. Здесь объясняется создание “многопользовательской игры с помощью socket.io” на примере реального проекта. Таким образом гораздо проще увидеть проект в действии, а также узнать, как работает код и его инфраструктура. А создаётся в этом проекте…

Многопользовательский симулятор набора футбольной команды.

Что происходит в этой игре? ⚡

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

Достаточно просто, не так ли? Далее же мы рассмотрим подробную разбивку инфраструктуры кода, стоящую за всем этим. 

Архитектура сервера ⚡⚡⚡

Архитектура игры

Диаграмма выше демонстрирует взаимосвязь всех компонентов, так сказать, с высоты птичьего полёта. 

И HTTP, и Websocket-сервер в этом руководстве работают с NodeJS. Мы используем Redis DB, поскольку socket.io поддерживает его интеграцию по умолчанию. Кроме того, в этом случае операции чтения/записи осуществляются гораздо быстрее, так как данные хранятся во внутренней памяти. MongoDB жеиспользуется в качестве более постоянного решения для хранения. Результаты игры и команды пользователей каждой комнаты сохраняются в MongoDB по завершении каждого раунда отбора. Там же хранятся идентификационные данные пользователей, если они решают зарегистрироваться (в этом проекте регистрация и авторизация являются необязательными).

WebCrawler написан на Python3 с использованием библиотеки Scrapy. Набор данных по футболистам был взят с https://sofifa.com и содержит более 20 000 игроков, включая их рейтинги, статистику, стоимость, клубы и т.д. Здесь также реализован опциональный анализ данных с помощью блокнота jupyter, позволяющий экспериментировать с этими данными, но его рассмотрение в тему текущего руководства не входит.

Структура каталогов (ExpressJS + MongoDB + socket.io)

NodeJS не навязывает структуру кода, что даёт большую гибкость при проектировании. Однако при этом вы можете сильно ошибиться, что приведёт к сложностям при обслуживании и масштабировании проектов. Структура конкретно нашего проекта может использоваться при работе с сокетами в сочетании с NodeJS.

Давайте рассмотрим её подробно.

.{src}
├── controller
│   ├── authController.js      # Обрабатывает запросы авторизации
│   ├── searchController.js    # Обрабатывает поисковые запросы
│   ├── userController.js      # Обрабатывает операции в профиле    #пользователя
│   └── ...
│
├── database
│   ├── db.js                  # Инициализирует соединение с DB
│   └── ...
│
├── middlewares
│   ├── authenticated.js       # Декодирует и проверяет JWT-токен
│   ├── error.js               # Стандартный обработчик ошибок
│   ├── logger.js              # Контролирует уровни логирования
│   └── ...
│
├── models
│   ├── roomsModels.js         # Модель DB для комнат
│   ├── usersModel.js          # Модель DB для пользователей
│   └── ...
│
├── schema
│   ├── rooms.js               # Схема DB для комнат
│   ├── users.js               # Схема DB для пользователей
│   └── ...
│
├── socker
│   ├── roomManager.js         # Слушатели сокетов/обработка #источников
│   ├── sockerController.js    # Контролирует соединения сокетов
│   └── ...
│
├── app.js                     # Начальный файл проекта
├── env.js                     # Хранилище переменных среды
├── routes.js                  # Инициализатор всех маршрутов
└── ...

Бэкенд разделён на разные директории в соответствии с требованиями проекта. Если вы хотите пропустить или поменять местами определённые модули, то сделать это так же просто, как добавить словарь.

Большая часть поддиректорий характерна для проектов nodejs, поэтому подробно их объяснять я не стану. Комментариев, расположенных рядом с каждой из них, вполне достаточно, чтобы понять, что к чему.

Мы же сфокусируемся на поддиректории socker/. Здесь будет размещаться основной код socket.io.

Точка входа для socket.io (App.js)

import { socker } from './socker';
import express from 'express';
import http from 'http';
import { API_PORT, host } from './env';

const app = express();
const server = new http.Server(app);
socker(server);

app.listen(API_PORT, () => {
  logger.info(`Api listening on port ${Number(API_PORT)}!`);
});

server.listen(Number(API_PORT) + 1, () => {
  logger.info(`Socker listening on port ${Number(API_PORT) + 1}!`);
  logger.info(`Api and socker whitelisted for ${host}`);
});

Здесь создаются два сервера: app, слушающий HTTP-запросы, и server, слушающий Websocket-соединения. Во избежание путаницы рекомендуется держать их подключенными к разным портам.

Вероятно, вам интересно, что это за “socker” на строке 1 и 8.

Что такое socker?

Socker — это просто псевдоним функции (ведь мы создаём игру по набору футбольной команды). Эта функция прикрепляет Server (переданный в строке 8 app.js) к экземпляру engine.io на новый http.Server. Проще говоря, она прикрепляет движок socket.io к переданному ей серверу. 

import socketio from 'socket.io';

export default server => {
  const io = socketio.listen(server, {...options});

  io.on('connection', socket => {
    logger.info('Client Connected');
  });

  return io;
};

Вышеприведённый код объясняет немногое, и теперь возникают следующие вопросы:

  • Как взаимодействовать с подключенными клиентами?
  • Где расположены пространства имён?
  • Где находятся Rooms/Channels (комнаты/каналы)?
  • И где, в конце концов, сама игра?

Назначение пространств имён

Пространства имён являются важной функцией socket.io. Они представляют пул сокетов, подключенных в заданной области и идентифицированных по их пути вроде /classic-mode, /football-draft, /pokemon-draft и т. д. По сути, это создание разных конечных точек или путей, которое позволяет минимизировать количество ресурсов (TCP-соединений) и в то же время разделяет задачи внутри приложения посредством разделения каналов связи. По умолчанию socket.io подключается к пространству имён /

Назначение комнат/каналов

Внутри каждого пространства имён вы можете создать произвольные каналы или комнаты. В дальнейшем это позволит вам создавать соединения, чьи сокеты смогут join или leave (подключаться или отключаться). Здесь для создания разных комнат мы используем channels, которые пользователи создают или куда подключаются для совместной игры.

Пример присоединения к комнате:

import socketio from 'socket.io';

const io = socketio.listen(app);

const roomId = '#8BHJL'

io.on('connection', async socket => {
  // join() позволяет присоединиться к комнате/каналу
  // Здесь используется `await`, поскольку инструкция join в socketio использует смесь асинхронных и синхронных операций
  await socket.join(roomId);
  
  logger.info('Client Connected');
});

Операция join() проверяет, была ли уже создана требуемая roomId. Если нет, она создаёт её и добавляет к данной roomId игрока. Если же комната уже создана, то присоединяется к ней напрямую.

Полноценный пример, объединяющий использование пространств имён и каналов:

import socketio from 'socket.io';
import Room from './roomManager';

export default server => {
  const io = socketio.listen(server, {
    path: '/classic-mode',
  });

  logger.info('Started listening!');
  
  // Создаёт пространство имён
  const classicMode = io.of('/classic-mode');
  
  classicMode.on('connection', async socket => {
    // Получает параметры, переданные от клиента сокета
    const { username, roomId, password, action } = socket.handshake.query;
    
    // Инициализирует комнату для подключения сокета
    const room = new Room({ io: classicMode, socket, username, roomId, password, action });

    const joinedRoom = await room.init(username);
    logger.info('Client Connected');
    
    // Слушатели, открытые сервером
    if (joinedRoom) {
        room.showPlayers();
        room.isReady();
        room.shiftTurn();
    }

    room.onDisconnect();
  });

  return io;
};

Заключение ⚡

На этом Часть 1 заканчивается. Структура кода, показанная здесь, неплохо работает для проектов среднего размера. Если вы создаёте прототип, то можете опустить или объединить каталоги схем и моделей. При необходимости смело облегчайте проект. 🙂

А что, если его размер увеличится? Тогда текущая структура уже может не подойти. В этом случае можно создать подкаталоги для каждого требуемого сервиса и компонентов (user-authentication, _tests_, analytics и т. д.). Вы можете даже создать микросервисы, т.е. развёртывать каждый процесс или сервис отдельно, что позволит балансировать или масштабировать только сильно нагруженные процессы.

Старайтесь не перемудрить при создании проекта. Лучше создавайте и развёртывайте его поэтапно.

В начале это было весело, но сейчас ты уже перебарщиваешь.

Будь то шутка или инженерный проект, перебарщивать не стоит 🙂

Вот ссылки на проект:

Socket.IO
SOCKET.IO 2.0 IS HERE FEATURING THE FASTEST AND MOST RELIABLE REAL-TIME ENGINE ~/Projects/tweets/index.js var io =…socket.io

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

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Saurav Hiremath: Socket.io Project ~ Build it the right way using NodeJS and React (not a chat app) — Part 1

Предыдущая статьяPython: декоратор @retry
Следующая статьяЧто такое компилятор