Перейти к содержанию

WebSocket

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

Протокол WebSocket

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

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

  • Управляющие данными (посылают данные);
  • Управляющие соединением (проверяют установку соединения через PING или закрывают его).

Node.js и socket.io

Для использования в Node.js WebSocket необходимо установить npm модуль socket.io.

npm install socket.io --save

Рассмотрим пример.

app.js

const express = require('express'),
  app = express(),
  http = require('http').createServer(app),
  io = require('socket.io')(http)

const host = '127.0.0.1'
const port = 7000

let clients = []

io.on('connection', socket => {
  console.log(`Client with id ${socket.id} connected`)
  clients.push(socket.id)

  socket.emit('message', "I'm server")

  socket.on('message', message => console.log('Message: ', message))

  socket.on('disconnect', () => {
    clients.splice(clients.indexOf(socket.id), 1)
    console.log(`Client with id ${socket.id} disconnected`)
  })
})

app.use(express.static(__dirname))

app.get('/', (req, res) => res.render('index'))

//получение количества активных клиентов
app.get('/clients-count', (req, res) => {
  res.json({ count: io.clients().server.engine.clientsCount })
})

//отправка сообщения конкретному клиенту по его id
app.post('/client/:id', (req, res) => {
  if (clients.indexOf(req.params.id) !== -1) {
    io.sockets.connected[req.params.id].emit('private message', `Message to client with id ${req.params.id}`)
    return res.status(200).json({ message: `Message was sent to client with id ${req.params.id}` })
  } else return res.status(404).json({ message: 'Client not found' })
})

http.listen(port, host, () => console.log(`Server listens http://${host}:${port}`))

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Socket.io example</title>
    <script src="/node_modules/socket.io-client/dist/socket.io.js"></script>
    <script>
      let socket = io()

      socket.on('message', message => console.log('Message from server: ', message))
      socket.on('private message', message => console.log('Private message from server: ', message))

      function sendMessageToServer() {
        socket.emit('message', "I'm client")
      }
    </script>
  </head>
  <body>
    <button onclick="sendMessageToServer()">Send message to server</button>
  </body>
</html>

Для подключения WebSocket на клиентской стороне используется модуль socket.io-client, экземпляру которого передается адрес сервера, с которым необходимо установить соединение по WebSocket.

При установке соединения между клиентом и сервером Node.js по WebSocket генерируется событие connection, которое обрабатывается с помощью метода on() модуля socket.io. Передаваемая вторым параметром методу on() callback-функция единственным параметром принимает экземпляр соединения (далее просто сокет).

io.on('connection', socket => {})

Каждое соединение имеет свой уникальный идентификатор, зная который можно отправить сообщение конкретному клиенту (см. в примере маршрут /client/:id).

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

socket.on('disconnect', () => {})

Для отправки данных от сервера Node.js к клиенту (и наоборот), используется метод emit(), которые принимает следующие параметры:

  • имя события;
  • данные, которые необходимо отправить (могут быть отправлены в виде REST-аргументов);
  • callback-функция (передается последним параметром), которая будет вызвана, когда вторая сторона получит сообщение.

Обработка отправляемых данных на стороне получателя происходит с использованием уже знакомого метода on(), первым параметром принимающего имя события, указанного в emit(), вторым - callback-функцию с переданными данными в качестве ее параметров.

//получатель
let socket = io()

socket.on('message', message => console.log('Message from server: ', message))

//отправитель
io.on('connection', socket => {
  socket.emit('message', "I'm server")
})

Для отправки данных всем клиентам, используйте метод emit() применительно к объекту io.sockets.

io.sockets.emit('message', 'Message for all clients')

Чтобы узнать текущее количество соединений, используйте метод clients(), вызываемый применительно к свойству sockets экземпляра модуля socket.io (см. в примере маршрут /clients-count).

В качестве необязательного параметра методу clients() можно передать имя "комнаты", количество соединений для который вы хотите узнать.

Пространства и "комнаты"

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

Пространства позволяют изолировать одни сокеты от других.

app.js

const express = require('express'),
  app = express(),
  http = require('http').createServer(app),
  io = require('socket.io')(http)

const host = '127.0.0.1'
const port = 7000

const nmspc1 = io.of('/your-namespace1')
const nmspc2 = io.of('/your-namespace2')

nmspc1.on('connection', socket => {
  console.log(`Client ${socket.id} connected to /your-namespace1`)
})

nmspc2.on('connection', socket => {
  console.log(`Client ${socket.id} connected to /your-namespace2`)
})

app.use(express.static(__dirname))

app.get('/', (req, res) => res.render('index'))

http.listen(port, host, () => console.log(`Server listens http://${host}:${port}`))
let socket = io('/your-namespace1')

В приведенном примере с помощью метода of() на сервере определяются два пространства: /users и /orders. На клиентской стороне подключение к тому или иному пространству происходит в зависимости от текущего маршрута. Таким образом, при отправке данных, например, из пространства /users, об этом будут оповещены только сокеты этого пространства. По умолчанию все сокеты находятся в пространстве /.

Также и в пределах пространства можно распределять сокеты по так называемым "комнатам".

io.on('connection', socket => {
  socket.join('Room №1')
})

Чтобы отнести сокет к определенной "комнате" используется метод пространства join(), который принимает имя "комнаты" (задается пользователем модуля socket.io). Для вынесения сокета из комнаты используйте метод leave().

Отправка данных в "комнату" осуществляется с помощью метода to().

io.to('Room №1').emit('message', 'Message form Room №1')

Обработка инициируемых в пределах "комнаты" событий осуществляется с использованием метода in().

io.in('Room №1').on('message', message => console.log('Room №1. Message: ', message))