В этом сообщении блога мы обсудим WebSockets и то, как их можно использовать для создания высокопроизводительных многопользовательских веб-приложений. Мы будем освещать:

  • Что такое WebSockets и чем они отличаются от HTTP-соединений
  • Почему WebSockets необходимы для требовательных приложений
  • Как соединения WebSocket создаются и используются для отправки информации
  • Как реализовать WebSockets в многопользовательской игре в крестики-нолики

Мне всегда было интересно, как на моем телефоне работают мгновенные сообщения и уведомления. Телефоны постоянно пингуют какой-то сервер, чтобы узнать, есть ли новое сообщение? Это может сработать, но кажется неэффективным как для сервера, так и для моего телефона; хотя это, безусловно, объяснило бы время автономной работы моего телефона ... Кроме того, мой телефон всегда ожидает, что кто-то отправит ему какие-то данные? Это вызывает некоторые проблемы с безопасностью; кто-нибудь может отправить данные на мой телефон? До того, как в 2011 году были представлены WebSockets, основным подходом к хранению данных в реальном времени был длительный опрос.

ПРАЙМЕР ПО ДЛИТЕЛЬНОМУ ОПРОСУ

Прежде чем мы перейдем к WebSockets, нам нужно понять проблемы, связанные с длинным опросом. До появления WebSockets веб-приложения были построены на основе предположения, что серверы получают запросы и отправляют соответствующие ответы. Обратите внимание, что сервер не может инициировать отправку данных клиенту. Серверы должны были полностью реагировать на запросы, поступающие от клиента. С этой моделью клиент-сервер, длинный опрос использовался как решение для поддержания актуальных данных на клиенте. Длинный опрос - это цикл отправки HTTP-запроса от клиента к серверу, а сервер не отправлять ответ, пока не появятся новые данные. Когда клиент получает этот ответ от сервера, он немедленно отправляет другой запрос на сервер. Этот бесконечный цикл работает для поддержания данных в реальном времени, но также требует от клиента / браузера и сервера, поскольку каждый HTTP-запрос и ответ также отправляют не- незначительное количество метаданных, заголовков и файлов cookie. Для приложений, которые требуют частой отправки обновленных данных через очень короткие промежутки времени, таких как онлайн-игры или финансовые тикеры, накладные расходы на HTTP-запросы недопустимы. Вот тут-то и пригодятся WebSockets.

ЧТО ТАКОЕ ВЕБ-РОКЕТЫ?

WebSocket API, более часто называемый WebSockets, представляет собой протокол связи, который обеспечивает механизм для отправки данных между клиентом и сервером без накладных расходов на HTTP-запросы и длительный опрос. Соединения WebSocket обеспечивают три основных преимущества по сравнению с длительным опросом.

  1. Теперь сервер может инициализировать отправку данных клиенту, ему не нужно ждать запроса.
  2. Заголовки, файлы cookie и другие метаданные не нужно передавать вместе с каждой передачей данных между клиентом и сервером.
  3. Сервер может открывать соединения WebSocket для нескольких клиентов.

Так как именно WebSockets все это реализует? Он начинается с того, что клиент отправляет HTTP-запрос на сервер, называемый запросом подтверждения связи WebSocket. Запрос рукопожатия содержит заголовок для обновления соединения до соединения WebSocket. Когда сервер принимает рукопожатие, между клиентом и сервером устанавливается соединение WebSocket. Вновь установленное соединение WebSocket является двунаправленным, поэтому клиент и сервер могут отправлять сообщения друг другу напрямую. Помните, что с HTTP-запросами сервер должен был ждать входящего запроса перед отправкой ответа, теперь любая сторона может отправлять данные другой!

Помимо снижения накладных расходов, WebSockets позволяет серверам подключаться к нескольким клиентам. Теперь, когда сервер может отправлять сообщения, не дожидаясь запроса клиента, мы можем использовать сервер для передачи информации всем подключенным клиентам. Это открывает возможности для создания гибких многопользовательских приложений, требующих от каждого клиента получения данных в реальном времени. Например, чаты, онлайн-игры, финансовые тикеры и (для одержимых людей вроде меня) спортивные результаты и статистика в реальном времени.

ВНЕДРЕНИЕ ВЕБ-РОКЕТОВ В МНОГОПОЛЬЗОВАТЕЛЬСКОЙ ИГРЕ

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

  1. Сделайте игру в крестики-нолики, используя HTML / CSS / JS (сокетов пока нет!)
  2. Создайте сервер, обслуживающий нашу (интерфейсную) игру
  3. Добавить функциональность WebSocket на сервер
  4. Реорганизуйте наш интерфейс, чтобы использовать соединения WebSocket
  5. Играй в нашу игру!

Настраивать

Прежде чем мы начнем писать код, давайте настроим структуру папок и файлов нашего проекта. Мы будем использовать пару пакетов Node.js на нашем сервере, поэтому выполните следующие команды терминала в папке этого проекта:

npm init -y
npm install express ws

Окончательная структура файла будет выглядеть так:

project_folder/
    client/
        index.html
        index.js
        styles.css
    server/
        server.js
    package.json

Создайте интерфейс

Начните с использования HTML, CSS и JavaScript, чтобы создать простую игру в крестики-нолики в браузере. В теле нашего файла index.html создайте три строки по три кнопки в каждой. Дайте кнопкам начальный текст тире:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Tic-Tac-Toe</title>
</head>
<body>
   <div>
     <button id="11">-</button>
     <button id="12">-</button>
     <button id="13">-</button>
   </div>
   <div>
     <button id="21">-</button>
     <button id="22">-</button>
     <button id="23">-</button>
   </div>
   <div>
     <button id="31">-</button>
     <button id="32">-</button>
     <button id="33">-</button>
   </div>
</body>
</html>

Если вы откроете файл index.html сейчас, вы увидите нашу доску 3x3 с очень маленькими кнопками. Позже мы будем использовать идентификаторы на этих кнопках для синхронизации двух наших игроков через WebSockets! Давайте добавим немного CSS, чтобы придать нашей игре некоторый стиль. Сначала импортируйте таблицу стилей в файл index.html. В заголовке html файла добавьте:

<link rel="stylesheet" href="./styles.css"></link>

Затем мы можем добавить стиль к каждой из наших кнопок:

/* styles.css */
button {
 width: 100px;
 height: 100px;
 font-size: 25px;
}

Перезагрузите файл index.html, и кнопки должны стать больше и больше походить на настоящую игру в крестики-нолики! Получайте удовольствие от этой части и сделайте игру своей! Если вы попытаетесь нажать кнопку, ничего не произойдет! Давай изменим это сейчас. Начните с импорта файла index.js в index.html после закрывающего тега основного текста. Окончательный файл index.html должен выглядеть так:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <link rel="stylesheet" href="./styles.css"></link>
 <title>Tic-Tac-Toe</title>
</head>
<body>
   <div>
     <button id="11">-</button>
     <button id="12">-</button>
     <button id="13">-</button>
   </div>
   <div>
     <button id="21">-</button>
     <button id="22">-</button>
     <button id="23">-</button>
   </div>
   <div>
     <button id="31">-</button>
     <button id="32">-</button>
     <button id="33">-</button>
   </div>
</body>
<script src="./index.js"></script>
</html>

Теперь мы можем добавить в нашу игру код Javascript. Обновите файл index.js, чтобы установить прослушиватель событий для каждой кнопки, который изменит свой текст на «X» или «O»:

// index.js
// wait for the window to load
window.onload = () => {
 // grab all of the button elements off of the DOM
 const collectionOfButtons = document.querySelectorAll('button');
 
 // boolean that will track if it's the 'X' player's turn
 let xTurn = true;
 
 // add an on-click event listener for each button to update its text to X or O
 collectionOfButtons.forEach((buttonElement) => {
   buttonElement.addEventListener('click', (event) => {
     // when clicked, update the button's text to an 'X' or an 'O' & disable the button
     event.target.innerHTML = xTurn ? 'X' : 'O';
     event.target.disabled = true;
 
     // change the turn tracker boolean
     xTurn = !xTurn;
   });
 });
};

Мы получаем HTMLCollection всех кнопок на нашей веб-странице и применяем к каждой из них прослушиватель событий при нажатии. При нажатии кнопка меняет свой текст на «X» или «O» и отключается, чтобы ее нельзя было нажимать более одного раза. Давайте обновим страницу index.html и сыграем в нашу игру локально! Вы должны иметь возможность чередовать размещение X и O на доске благодаря логической переменной xTurn. Теперь у нас есть некоторые базовые функции внешнего интерфейса, давайте приступим к созданию внутреннего интерфейса.

Создание экспресс-сервера

Прежде чем мы добавим WebSockets, нам нужно создать сервер. Вот где пригодятся экспресс-библиотека, а также собственные модули Node.js path и http.

// server.js
const express = require('express');
const path = require('path');
const http = require('http');
 
const app = express();
const server = http.createServer(app);
 
// serve up static files (index.html, styles.css, & index.js)
app.use(express.static(path.resolve(__dirname, '../client')));
 
server.listen(3000, () => { console.log('listening on port 3000'); });

Начало нашего экспресс-сервера - это просто «обслужить» интерфейс, который мы только что создали. Метод express.static делает все файлы в нашей клиентской папке доступными для браузера по запросу. Запустите сервер, запустив node server/server.js в терминале, а затем перейдите на localhost: 3000 / в своем браузере. Вы должны увидеть то же самое приложение, что и раньше, когда мы открыли файл index.html, но теперь сервер предоставляет файлы HTML, CSS и JS.

Добавление в WebSockets

Теперь, когда у нас есть сервер и интерфейс, нам нужно добавить функциональность WebSocket к ним обоим. В файле server.js:

  • Импортируйте библиотеку ws и используйте ее метод Server для создания сервера WebSockets.
  • Добавьте прослушиватель событий на сервер WebSocket для подключений. В обратном вызове при подключении параметр по умолчанию - это экземпляр соединения WebSocket. Добавьте эти соединения в Набор всех открытых соединений (setClients) и запишите в консоль, что есть новое соединение.
  • Затем добавьте прослушиватель событий для сообщения, поступающего от подключенного клиента. Когда это событие запускается, одно и то же сообщение будет отправлено каждому из клиентов.
  • Наконец, добавьте прослушиватель событий для отключающегося клиента. Когда клиент завершает свое соединение WebSockets, удалите его из набора клиентов и запишите, сколько активных соединений имеется на консоли.

Теперь файл server.js должен выглядеть так:

// server.js
const express = require('express');
const path = require('path');
const http = require('http');
const WebSocket = require('ws');
 
const app = express();
const server = http.createServer(app);
const wsServer = new WebSocket.Server({ server });
 
// a set to hold all of our connected clients
const setClients = new Set();
 
wsServer.on('connection', (socketConnection) => {
 // When a connection opens, add it to the clients set and log the number of connections
 setClients.add(socketConnection);
 console.log('New client connected, total connections is: ', setClients.size);
 
 // When the client sends a message to the server, relay that message to all clients
 socketConnection.on('message', (message) => {
   setClients.forEach((oneClient) => {
     oneClient.send(message);
   });
 });
 
 // When a connection closes, remove it from the clients set and log the number of connections
 socketConnection.on('close', () => {
   setClients.delete(socketConnection);
   console.log('Client disconnected, total connections is: ', setClients.size);
 });
});
 
// serve up static files
app.use(express.static(path.resolve(__dirname, '../client')));
 
server.listen(3000, () => { console.log('listening on port 3000'); });

Теперь наш сервер настроен для создания соединений WebSocket и ретрансляции сообщений между всеми подключенными клиентами. Последний шаг! Мы должны внести изменения в интерфейс, чтобы подключаться к серверу WebSockets, отправлять сообщения (содержащие идентификатор нажатой кнопки) и получать сообщения. Внесите следующие изменения в файл index.js:

  • Сначала установите соединение WebSocket, используя встроенный WebSocket API.
  • Добавьте прослушиватель событий «onopen», который регистрирует, когда установлено соединение WebSocket.
  • Теперь, когда есть соединение WebSocket с сервером, обновите прослушиватели событий при нажатии кнопок, чтобы они отправляли на сервер сообщение, содержащее идентификатор кнопки.
  • Настройте прослушиватель событий «onmessage» для соединения WebSocket. Когда сервер отправляет сообщение клиенту, сообщение будет содержать идентификатор нажатой кнопки. Убедитесь, что значение соответствующей кнопки - тире, и если да, измените ее текст на «O».
  • Наконец, обновите начальный прослушиватель событий при нажатии для каждой кнопки, чтобы изменить только innerHTML кнопок на «X». Теперь, когда клиент-клиент нажимает кнопку, он изменится на «X» и отправит идентификатор на сервер через соединение WebSocket. Когда другие клиенты получат идентификатор в сообщении, он изменит соответствующую кнопку на «O».

Теперь файл index.js должен выглядеть так:

// index.js
window.onload = () => {
 // connect to socket server through WebSocket API
 const socketConnection = new WebSocket('ws://www.localhost:3000/');
 // when connection opens, log it to the console
 socketConnection.onopen = (connectionEvent) => {
   console.log('websocket connection is open', connectionEvent);
 };
 
 // when a message is received from the socket connection,
 // the message will contain the id of a button that the other player clicked
 socketConnection.onmessage = (messageObject) => {
   // if the button is unclicked, changes its text to "O", and disable the button
   const buttonClicked = document.getElementById(messageObject.data);
   if (buttonClicked.innerHTML === '-') {
     buttonClicked.innerHTML = 'O';
     buttonClicked.disabled = true;
   }
 };
 
 // an event listener to log any errors with the socket connection.
 socketConnection.onerror = (error) => {
   console.log('socket error: ', error);
 };
 
 // put an event listener on every button that changes the text to "X",
 // disables the button, and sends a message through the socket connection
 // with the id of the clicked button
 const collectionOfButtons = document.querySelectorAll('button');
 collectionOfButtons.forEach((buttonElement) => {
   buttonElement.addEventListener('click', (event) => {
     // set the target's value to "X" and disable the button
     event.target.innerHTML = 'X';
     event.target.disabled = true;
 
     // send message (of the clicked button's id) through the socket connection
     socketConnection.send(event.target.id);
   });
 });
};

Перезагрузите сервер, нажав Ctrl + C в терминале, а затем перезапустите сервер, снова запустив node server/server.js. Откройте два окна браузера для localhost: 3000 / и посмотрите, как сервер WebSockets передает сообщения между двумя подключенными клиентами! Возможно, вы заметили пару вещей, которые «не работают» в нашей игре в крестики-нолики. Оба игрока / обозреватели играют так, как если бы они были игроком «X». И что еще важнее, им не нужно ждать друг друга, чтобы сделать еще один шаг! Я оставлю вам задачу по реализации дополнительной игровой логики! Поздравляем с использованием WebSockets для создания многопользовательской игры в крестики-нолики! Я надеюсь, что это откроет вам глаза на мощь WebSockets и на то, как их можно использовать для создания гибких многопользовательских приложений. Без WebSockets нашему приложению пришлось бы полагаться на клиентские запросы и длительный опрос для поддержания обновленных данных, что привело бы к ненужной нагрузке на браузер и сервер. Реализуя WebSockets, сервер может отправлять новую информацию каждому клиенту именно тогда, когда он этого хочет, и без накладных расходов, связанных с HTTP-запросами! Помимо ws, существует множество других библиотек для реализации WebSockets как на стороне клиента, так и на стороне сервера, в том числе:

Для дальнейшего чтения о протоколе WebSockets и собственном WebSockets API браузера: