Ультрабыстрые приложения на Node.js и Socket.io

Библиотека Socket.io позволяет реализовать синхронизированную коммуникацию внутри приложения. То есть, в режиме реального времени. Проще говоря, socket.io поможет создать живой чат на сайте. Или, например, браузерную игру, за развитием сюжета которой пользователь сможет следить без перезагрузки веб-страницы!

Что делает socket.io?

socket.io основывается на нескольких технологиях, обеспечивающих коммуникацию в режиме реального времени. Наиболее известная из них – это WebSocket.

Этот API JavaScript поддерживается всеми современными браузерами. Он обеспечивает синхронизированный двунаправленный обмен данными между клиентом и сервером.

Давайте ещё раз вернёмся к основам. Большая часть коммуникационных каналов интернета не синхронизированы. Он всегда был таким: клиент запрашивает, а сервер отвечает.

Что делает socket.io?

Коммуникации обычно не синхронизированы: клиент запрашивает, а сервер отвечает

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

WebSocket – это технология, которая создаёт некое подобие «канала» коммуникации между клиентом и сервером, который остаётся постоянно открытым. Браузер и сервер остаются соединёнными друг с другом и могут обмениваться сообщениями в любом направлении.

Что делает socket.io? - 2

С WebSocket канал связи между клиентом и сервером остаётся открытым.

Не путайте WebSocket и AJAX!

AJAX помогает клиенту и серверу обмениваться информацией без перезагрузки страницы. Но при этом клиент всегда посылает запрос, а сервер отвечает. Сервер не может сам принять решение об отправке данных клиенту. С WebSocket это стало возможным!

Socket.io позволяет просто использовать WebSocket. А поскольку не все браузеры могут создавать WebSocket, данная библиотека позволяет использовать другие механизмы синхронизации, которые поддерживаются браузером.

Загляните в раздел поддерживаемых браузеров на официальном сайте socket.io. Здесь можно увидеть, что библиотека определяет, какая технология подходит клиенту:

  • WebSocket;
  • Adobe Flash Socket;
  • Длинные опросы (long pooling) AJAX;
  • AJAX multipart streaming;
  • Iframe;
  • Опросы JSONP.

Например, если браузер не поддерживает WebSocket, но в нем установлен Flash, то socket.io будет использовать последнюю технологию для взаимосвязи в реальном времени. Если нет, библиотека применит другие техники, такие как длинные опросы AJAX.

Клиент постоянно опрашивает сервер, есть ли обновления для него. Это не самый эффективный метод, но он работает. Также может применяться невидимый iframe, который постоянно перезагружается, чтобы получить обновления с сервера.

Благодаря всем этим технологиям библиотека socket.io поддерживает большое количество браузеров, в том числе и устаревших:

  • Internet Explorer 5.5+;
  • Safari 3+;
  • Google Chrome 4+;
  • Firefox 3+;
  • Opera 10.61+;
  • Safari для iPhone и iPad;
  • Стандартный браузер Android.

Отправка и получение сообщений с помощью socket.io

Перейдём к делу: как использовать библиотеку socket.io?

Установка socket.io

При первом использовании библиотеки я потратил 15 минут, чтобы установить ее. Для этого нужно выполнить эту команду:

npm install socket.io

Первый код: клиент входит в систему

При работе с библиотекой socket.io используется одновременно два файла:

  • Серверный файл (например, app.js): он управляет соединениями сайта и клиентских приложений (браузеров).
  • Клиентский файл (например, index.html): он соединяет клиента с сервером и отображает результаты в браузере.

Сервер (app.js)

Вначале мы загружаем на сервер содержимое страницы index.html и отображаем ее клиенту, После этого грузим библиотеку socket.io и управляем ее событиями.

var http = require('http');
var fs = require('fs');

// Загружаем файл index.html и отображаем его клиенту
var server = http.createServer(function(req, res) {
    fs.readFile('./index.html', 'utf-8', function(error, content) {
        res.writeHead(200, {"Content-Type": "text/html"});
        res.end(content);
    });
});

// Загружаем socket.io
var io = require('socket.io').listen(server);

// Когда клиент соединяется, выводим сообщение в консоль
io.sockets.on('connection', function (socket) {
    console.log('A client is connected!');
});


server.listen(8080);

Что делает приведенный выше код:

  • Отправляет файл index.html, когда клиент просит изменить страницу в браузере.
  • Готовится получать запросы через socket.io. Когда мы устанавливаем соединение с помощью библиотеки, то выводим сообщение об этом в консоль.

Допустим, что пользователь открывает в браузере веб-страницу, на которой находится приложение (в данном случае http://localhost:8080). Мы отправляем ему файл index.html, страница загружается. Код JavaScript этого файла соединяется с сервером. Но на этот раз не через http, а через socket.io (WebSocket). Клиент поддерживает два типа соединений:

  • "Классическое" HTTP-соединение с сервером, которое используется для загрузки веб-страницы index.html.
  • Соединение, происходящее "в реальном времени" для открытия канала через WebSocket с помощью socket.io.

Клиент (index.html)

Файл index.html отправляется сервером Node.js. Это классический HTML-файл, который содержит JavaScript-код для соединения с сервером в режиме реального времени.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Socket.io</title>
    </head>
 
    <body>
        <h1>Communication with socket.io!</h1>

        <script src="https://www.internet-technologies.ru/wp-content/uploads/socket.io/socket.io.js"></script>
        <script>
            var socket = io.connect('http://localhost:8080');
        </script>
    </body>
</html>

Я специально расположил JavaScript- код в конце HTML-кода, чтобы избежать задержки загрузки страницы из-за JavaScript.

В первом блоке кода мы получаем файл socket.io.js для клиента. Он автоматически предоставляется сервером Node.js через модуль socket.io (поэтому путь к файлу выбран не случайно):

<script src="https://www.internet-technologies.ru/wp-content/uploads/socket.io/socket.io.js"></script>

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

Затем выполним действие на стороне клиента для установки соединения с сервером. Пока я сделал самое простое: соединился с сервером. Он располагается на моём компьютере, что объясняет адрес http://localhost:8080.

var socket = io.connect('http://localhost:8080');

Тестируем код!

Запустите приложение:

node app.js

Затем в браузере откройте страницу с Node.js: http://localhost:8080

После этого загрузится основная страница. Затем компьютер откроет соединение с socket.io, а сервер выведет отладочную информацию в консоли:

$ node app.js
   info  - socket.io started
   debug - client authorized
   info  - handshake authorized Z2E7aqIvOPPqv_XBn421
   debug - setting request GET /socket.io/1/websocket/Z2E7aqIvOPPqv_XBn421
   debug - set heartbeat interval for client Z2E7aqIvOPPqv_XBn421
   debug - client authorized for 
   debug - websocket writing 1::
A client is connected!

Отлично! Значит, код работает.

Отправка и получение сообщений

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

  • Сервер хочет отправить сообщение клиенту.
  • Клиент хочет отправить сообщение серверу.

Сервер хочет отправить сообщение клиенту.

Я предлагаю рассмотреть вариант, когда сервер отправляет сообщение клиенту о том, что соединение установлено. Добавьте этот код в файл app.js:

io.sockets.on('connection', function (socket) {
        socket.emit('message', 'You are connected!');
});

Когда соединение установлено, клиенту отправляется сообщение с помощью socket.emit(). Эта функция принимает два параметра:

  • Тип сообщения, которое нужно передать. Это позволит различать типы сообщений. Например, в игре можно отправлять типы сообщений "move_player" или "attack_player".
  • Содержимое сообщения. Здесь можно указать всё, что хотите.

Если нужно передать различные типы данных в сообщении, группируйте их в объекты, как показано в следующем примере:

socket.emit('message', { content: 'You are connected!', importance: '1' });

В файле index.html (на стороне клиента) мы будем отслеживать входящие сообщения:

<script>
    var socket = io.connect('http://localhost:8080');
    socket.on('message', function(message) {
        alert('The server has a message for you: ' + message);
    })
</script>

С помощью socket.on() перехватываются сообщения типа message. Когда они приходят, вызывается функция, которая отображает простое диалоговое окно.

Попробуйте. Вы увидите, что при загрузке index.html отображается диалоговое окно, сообщающее, что соединение успешно установлено.

Сервер хочет отправить сообщение клиенту.

Клиент отображает сообщение от сервера в диалоговом окне

Клиент хочет отправить сообщение серверу.

На стороне клиента (в файле index.html) я добавлю кнопку "Poke the server". Когда мы нажмём на неё, серверу будет отправлено сообщение. Вот полный код:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Socket.io</title>
    </head>
 
    <body>
        <h1>Communicating with socket .io!</h1>

        <p><input type="button" value="Poke the server" id="poke" /></p>

        <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
        <script src="https://www.internet-technologies.ru/wp-content/uploads/socket.io/socket.io.js"></script>
        <script>
            var socket = io.connect('http://localhost:8080');
            socket.on('message', function(message) {
                alert('The server has a message for you: ' + message);
            })

            $('#poke').click(function () {
                socket.emit('message', 'Hi server, how are you?');
            })
        </script>
    </body>
</html>

Чтобы перехватить событие нажатия на кнопку, я использую jQuery. Вы можете использовать JavaScript.

Добавьте в код следующий блок:

$('#poke').click(function () {
    socket.emit('message', 'Hi server, how are you?');
})

Это простой код. При нажатии на кнопку на сервер отправляет сообщение типа message (вместе с контентом). Если нужно получить его на стороне сервера, придётся реализовать отслеживание сообщений типа message:

io.sockets.on('connection', function (socket) {
    socket.emit('message', 'You are connected!');

    // Когда сервер получает сообщение типа “message” от клиента
    socket.on('message', function (message) {
        console.log('A client is speaking to me! They’re saying: ' + message);
    }); 
});

Запустите код. Нажмите на кнопку, размещенную на веб-странице, и понаблюдайте за консолью сервера. Вы должны увидеть следующий текст.

A client is talking to me! They’re saying: Hi server, how are you?
Клиент хочет отправить сообщение серверу.

Когда клиент нажимает на кнопку, сервер мгновенно реагирует в консоли

Коммуникация с несколькими клиентами

В предыдущих примерах мы работали с одним сервером и одним клиентом. На практике у вас будет сразу несколько клиентов, соединённых с приложением Node.js. Чтобы воссоздать эту ситуацию локально, откройте две вкладки в браузере. На каждой из них перейдите по адресу http://localhost:8080. В результате сервер увидит соединения двух различных клиентов.

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

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

Это как раз то, что о чем я расскажу дальше.

Отправка сообщения всем клиентам (рассылка)

Когда вызывается socket.emit() на стороне сервера, то отсылается сообщение только клиенту, с которым вы общаетесь в данный момент. Но можно отправить сообщение всем другим клиентам (кроме того, к которому вы сейчас подключены).

Рассмотрим следующий сценарий:

  1. Клиент A посылает сообщение серверу.
  2. Сервер анализирует его.
  3. Сервер решает сделать рассылку этого сообщения и отправляет его другим клиентам B и C.
Отправка сообщения всем клиентам (рассылка)

В рассылке сервер отправляет сообщение всем другим соединённым клиентам

Например, клиент A пишет сообщение в чате и отправляет его на сервер. А чтобы его увидели другие клиенты, оно должно быть разослано.

socket.broadcast.emit('message', 'Message to all units. I repeat, message to all units.');

Для этого нужно вызвать socket.broadcast.emit(), и сообщение будет отправлено другим пользователям. Добавьте приведенный ниже код рассылки в файл app.js:

io.sockets.on('connection', function (socket) {
    socket.emit('message', 'You are connected!');
    socket.broadcast.emit('message', 'Another client has just connected!');

    socket.on('message', function (message) {
        console.log('A client is speaking to me! They're saying: ' + message);
    }); 
});

Теперь попробуйте открыть в браузере несколько веб-страниц по адресу http://localhost:8080. Вы увидите, что, когда к серверу подключается новый клиент, другие страницы реагируют на это сообщением: "Another client has just connected!"

Переменные сессии

При многопользовательском подключении трудно идентифицировать каждого клиента. Идеальным вариантом была бы использовать переменные сессии. Но они недоступны в socket.io.

Хотя переменными сессии можно управлять из другой библиотеки через промежуточный слой session.socket.io.

Чтобы показать вам, как использовать промежуточный слой управления сессиями, пришлось бы написать целую главу книги. Но я предложу упрощённую технику.

Нам нужно, чтобы сервер запоминал информацию о каждом соединённом клиенте. Благодаря этому клиенту не придётся при каждой отправке сообщения напоминать нам кто он!

Чтобы добавить переменную сессии на стороне сервера, необходим следующий код:

socket.myvariable = myvariable;

В этом примере мы храним данные в переменной объекта socket. Чтобы получить эту информацию, нужно просмотреть содержимое socket.myvariable:

console.log(socket.myvariable);

Теперь попытаемся представить реальный случай использования. Когда клиент соединяется с сервером, веб-страница запрашивает его имя. Сервер сохраняет логин пользователя в переменной сессии, чтобы использовать его, когда клиент нажмёт на кнопку "Poke the server".

Давайте посмотрим, какие изменения нам нужно внести.

Веб-страница (index.html) отправляет сообщение, содержащее имя пользователя

Когда веб-страница загружается, запрашивается логин пользователя. Мы отправляем его на сервер с помощью сообщения типа "little_newbie" . Это сообщение содержит имя посетителя:

var username = prompt('What's your username?');
socket.emit('little_newbie', username);

Сервер (app.js) хранит имя пользователя

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

socket.on('little_newbie', function(username) {
    socket.username = username;
});

Сервер (app.js) помнит имя пользователя, когда мы отправляем ему сообщение

Чтобы сервер помнил пользователя, когда он нажимает на кнопку, мы дополним функцию обратного вызова. Она вызывается при получении сервером сообщения типа "message":

socket.on('message', function (message) {
    console.log(socket.username + ' is speaking to me! They're saying: ' + message);
});

Как только сервер получает сообщение, мы запрашиваем переменную сессии с именем пользователя из клиентского объекта socket.

Тест кода

Попробуйте открыть два окна браузера и ввести разные имена пользователей. Затем нажмите на кнопку “Poke the server”. В консоли сервера вы увидите имя пользователя, который кликнул по кнопке.

Тест кода

Несколько пользователей подключены: сервер помнит их имена!

Полный код

Полный код файла index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Socket.io</title>
    </head>
 
    <body>
        <h1>Communicating with socket.io!</h1>

        <p><input type="button" value="Poke the server" id="poke" /></p>


        <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
        <script src="https://www.internet-technologies.ru/wp-content/uploads/socket.io/socket.io.js"></script>
        <script>
            var socket = io.connect('http://localhost:8080');

            // Посетителя просят ввести имя пользователя...
            var username = prompt('What's your username?');
            
            // Оно отправляется в сообщении типа "little_newbie" (чтобы отличать его от сообщений типа "message")
            socket.emit('little_newbie', username);

            // Диалоговое окно отображается, когда сервер отправляет нам сообщение типа "message"
            socket.on('message', function(message) {
                alert('The server has a message for you: ' + message);
            })

            // Когда нажимается кнопка, сообщение типа "message" отправляется на сервер
            $('#poke').click(function () {
                socket.emit('message', 'Hi server, how are you?');
            })
        </script>
    </body>
</html>

… а вот код серверного файла app.js:

var http = require('http');
var fs = require('fs');

// Загружаем файл index.html для отображения клиенту
var server = http.createServer(function(req, res) {
    fs.readFile('./index.html', 'utf-8', function(error, content) {
        res.writeHead(200, {"Content-Type": "text/html"});
        res.end(content);
    });
});

// Загрузка socket.io
var io = require('socket.io').listen(server);

io.sockets.on('connection', function (socket, username) {
    // Когда клиент подключается, им отправляется сообщение
    socket.emit('message', 'You are connected!');
    // Другим клиентам сообщается, что пришёл кто-то новый
    socket.broadcast.emit('message', 'Another client has just connected!');

    // Как только получено имя пользователя, оно сохраняется в переменной
    socket.on('little_newbie', function(username) {
        socket.username = username;
    });

    // Когда сообщение типа "message" получено (нажатие на кнопку), оно записывается в консоль
    socket.on('message', function (message) {
        // Имя пользователя посетителя, который нажал на кнопку, извлекается из переменной сессии
        console.log(socket.username + ' is speaking to me! They're saying: ' + message);
    }); 
});


server.listen(8080);

Помните, что это простое приложение, которое позволяет протестировать функционал библиотеки socket.io.

Резюме

  • io – это модуль Node.js, который позволяет посетителям постоянно (в режиме реального времени) общаться с сервером.
  • Библиотека Socket.io основана на технологии WebSocket.
  • Разработанное нами приложение может отправлять запросы на сервер в любое время без перезагрузки страницы. При этом посетители смогут получать сообщения отправляемые сервером в любой момент!
  • Сервер и клиент отправляют друг другу сообщения с помощью emit() и отслеживают их с помощью socket.on().

Сергей Бензенкоавтор-переводчик статьи «Ultra fast applications using Node.js»

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