WebSockets в действии: связь между Node и React в режиме реального времени

React.js

Перевод: сумасшедший технический ботаник

оригинал:blog.log Rocket.com/Веб-сокеты-…

img

Интернет прошел долгий путь, чтобы поддерживать полнодуплексную (или двустороннюю) связь между клиентами и серверами. Это основная цель протокола WebSocket: обеспечить постоянную связь в реальном времени между клиентом и сервером через одно соединение через сокет TCP.

У протокола WebSocket всего две повестки: 1) открыть рукопожатие, 2) помочь с передачей данных. После успешного рукопожатия сервера и клиента они могут свободно отправлять данные друг другу с небольшими накладными расходами.

Связь WebSocket происходит через один TCP-сокет с использованием протокола WS (порт 80) или WSS (порт 443). согласно сCan I Use, на момент написания почти все браузеры, кроме Opera Mini, поддерживают WebSockets.

статус-кво

Исторически сложилось так, что создание веб-приложений, требующих передачи данных в режиме реального времени, таких как игры или чаты, требовалоЗлоупотребление протоколом HTTPустановить двунаправленную передачу данных. Хотя существует множество способов реализации функций реального времени, ни один из них не является столь же эффективным, как WebSockets. HTTP-опрос, HTTP-стриминг, Comet, SSE — все они имеют свои недостатки.

HTTP-опрос

Первой попыткой решить проблему был периодический опрос сервера. Жизненный цикл длинного опроса HTTP выглядит следующим образом:

  1. Клиент делает запрос и продолжает ждать ответа.
  2. Сервер откладывает ответ до тех пор, пока не произойдет изменение, обновление или тайм-аут. Запрос остается "висящим" до тех пор, пока у сервера не появится что-то, что нужно вернуть клиенту.
  3. Когда на стороне сервера происходят какие-либо изменения или обновления, он отправляет ответ обратно клиенту.
  4. Клиент отправляет новый запрос на длительный опрос для прослушивания следующего набора изменений.

В длинных опросах много уязвимостей — накладные расходы на заголовок, задержки, тайм-ауты, кеширование и т. д.

HTTP-потоковая передача

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

Однако, когда ответ отправляется обратно клиенту, запрос никогда не завершается, сервер сохраняет соединение открытым и отправляет новые обновления по мере возникновения изменений.

События, отправленные сервером (SSE)

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

Мало того, что эти методы неэффективны, код для их поддержки также утомляет разработчика.

WebSocket

WebSockets предназначены для замены существующих технологий двусторонней связи. Существующие методы, описанные выше, не являются ни надежными, ни эффективными, когда речь идет о полнодуплексной связи в реальном времени.

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


Практическое руководство

Как упоминалось во введении, протокол WebSocket имеет только две программы. Давайте посмотрим, как WebSockets выполняет эти задачи. Для этого я профилирую сервер Node.js и подключаю его к клиенту, созданному с помощью React.js.

Повестка дня 1: WebSocket устанавливает рукопожатие между сервером и клиентом

Создать рукопожатие на уровне сервера

Мы можем использовать один порт для предоставления службы HTTP и службы WebSocket соответственно. В приведенном ниже коде показано создание простого HTTP-сервера. После создания мы привяжем сервер WebSocket к порту HTTP:

const webSocketsServerPort = 8000;
const webSocketServer = require('websocket').server;
const http = require('http');
// Spinning the http server and the websocket server.
const server = http.createServer();
server.listen(webSocketsServerPort);
const wsServer = new webSocketServer({
  httpServer: server
});

После создания сервера WebSocket нам нужно принять рукопожатие при получении запроса от клиента. Я сохраняю всех подключенных клиентов как объекты в коде и использую уникальный идентификатор пользователя при получении запросов от браузера.

// I'm maintaining all active connections in this object
const clients = {};

// This code generates unique userid for everyuser.
const getUniqueID = () => {
  const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
  return s4() + s4() + '-' + s4();
};

wsServer.on('request', function(request) {
  var userID = getUniqueID();
  console.log((new Date()) + ' Recieved a new connection from origin ' + request.origin + '.');
  // You can rewrite this part of the code to accept only the requests from allowed origin
  const connection = request.accept(null, request.origin);
  clients[userID] = connection;
  console.log('connected: ' + userID + ' in ' + Object.getOwnPropertyNames(clients))
});

Итак, что происходит, когда соединение принято?

При отправке обычного HTTP-запроса для установления соединения в заголовке запроса клиент отправляет*Sec-WebSocket-Key*. Сервер кодирует и хеширует это значение и добавляет предопределенный GUID. Он отвечает на рукопожатие, отправленное сервером*Sec-WebSocket-Accept*значение, созданное в .

Как только запрос принят на сервере (после необходимой проверки), рукопожатие завершается с кодом состояния101. Если вы видите код состояния, кроме в браузере101Все, кроме этого, означает, что обновление WebSocket не удалось, и будет использоваться обычная семантика HTTP.

*Sec-WebSocket-Accept*Поле заголовка указывает, готов ли сервер принимать соединения. Также, если ответ отсутствует*Upgrade*поле заголовка или*Upgrade*не равноwebsocket, это означает, что соединение WebSocket не удалось.

Успешное рукопожатие сервера выглядит так:

HTTP GET ws://127.0.0.1:8000/ 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: Nn/XHq0wK1oO5RTtriEWwR4F7Zw=
Upgrade: websocket

Создать рукопожатие на уровне клиента

На стороне клиента я использую тот же пакет WebSocket, что и на сервере, для установления соединения с сервером (API WebSocket в Web IDL стандартизируется W3C). Как только сервер примет запрос, мы увидим в консоли браузераWebSocket Client Connected.

Вот начальный шаблон для создания соединения с сервером:

import React, { Component } from 'react';
import { w3cwebsocket as W3CWebSocket } from "websocket";

const client = new W3CWebSocket('ws://127.0.0.1:8000');

class App extends Component {
  componentWillMount() {
    client.onopen = () => {
      console.log('WebSocket Client Connected');
    };
    client.onmessage = (message) => {
      console.log(message);
    };
  }
  
  render() {
    return (
      <div>
        Practical Intro To WebSockets.
      </div>
    );
  }
}

export default App;

Клиент отправляет следующие заголовки для установления рукопожатия:

HTTP GET ws://127.0.0.1:8000/ 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: vISxbQhM64Vzcr/CD7WHnw==
Origin: http://localhost:3000
Sec-WebSocket-Version: 13

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

Повестка дня 2: передача информации в режиме реального времени

内容修改的实时流。

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

  1. **Активность пользователя:** каждый раз, когда пользователь присоединяется или уходит, я рассылаю сообщение всем подключенным клиентам.
  2. **Изменения содержимого:** каждый раз, когда содержимое в редакторе изменяется, передается всем другим подключенным клиентам.

Этот протокол позволяет нам отправлять и получать сообщения в двоичных данных или в UTF-8 (примечание: при передаче и преобразовании в UTF-8 меньше накладных расходов).

Пока у нас есть событие сокетаonopen,oncloseа такжеonmessageПри хорошем понимании понять и внедрить WebSockets очень просто. Терминология одинакова для клиентской и серверной части.

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

На стороне клиента, когда присоединяется новый пользователь или меняется контент, мы используемclient.sendОтправьте сообщение на сервер, чтобы предоставить серверу новую информацию.

/* When a user joins, I notify the
server that a new user has joined to edit the document. */
logInUser = () => {
  const username = this.username.value;
  if (username.trim()) {
    const data = {
      username
    };
    this.setState({
      ...data
    }, () => {
      client.send(JSON.stringify({
        ...data,
        type: "userevent"
      }));
    });
  }
}

/* When content changes, we send the
current content of the editor to the server. */
onEditorStateChange = (text) => {
 client.send(JSON.stringify({
   type: "contentchange",
   username: this.state.username,
   content: text
 }));
};

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

Получить сообщение от сервера очень просто:

componentWillMount() {
  client.onopen = () => {
   console.log('WebSocket Client Connected');
  };
  client.onmessage = (message) => {
    const dataFromServer = JSON.parse(message.data);
    const stateToChange = {};
    if (dataFromServer.type === "userevent") {
      stateToChange.currentUsers = Object.values(dataFromServer.data.users);
    } else if (dataFromServer.type === "contentchange") {
      stateToChange.text = dataFromServer.data.editorContent || contentDefaultMessage;
    }
    stateToChange.userActivity = dataFromServer.data.userActivity;
    this.setState({
      ...stateToChange
    });
  };
}

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

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

const sendMessage = (json) => {
  // We are sending the current data to all connected clients
  Object.keys(clients).map((client) => {
    clients[client].sendUTF(json);
  });
}

connection.on('message', function(message) {
    if (message.type === 'utf8') {
      const dataFromClient = JSON.parse(message.utf8Data);
      const json = { type: dataFromClient.type };
      if (dataFromClient.type === typesDef.USER_EVENT) {
        users[userID] = dataFromClient;
        userActivity.push(`${dataFromClient.username} joined to edit the document`);
        json.data = { users, userActivity };
      } else if (dataFromClient.type === typesDef.CONTENT_CHANGE) {
        editorContent = dataFromClient.content;
        json.data = { editorContent, userActivity };
      }
      sendMessage(JSON.stringify(json));
    }
  });

Рассылка сообщения всем подключенным клиентам.

img

Что происходит, когда браузер закрыт?

В этом случае вызов WebSocketcloseсобытие, которое позволяет нам написать логику, завершающую текущее пользовательское соединение. В моем коде, когда пользователь покидает документ, остальным пользователям передается сообщение:

connection.on('close', function(connection) {
    console.log((new Date()) + " Peer " + userID + " disconnected.");
    const json = { type: typesDef.USER_EVENT };
    userActivity.push(`${users[userID].username} left the document`);
    json.data = { users, userActivity };
    delete clients[userID];
    delete users[userID];
    sendMessage(JSON.stringify(json));
  });

Исходник приложения находится на GitHubrepoсередина.

В заключение

WebSockets — один из самых интересных и удобных способов реализовать функциональность реального времени в вашем приложении. Это дает нам возможность в полной мере использовать преимущества полнодуплексной связи. Я настоятельно рекомендую сначала попробовать WebSockets, прежде чем пробовать Socket.IO и другие доступные библиотеки.

Удачного кодирования! 😊

Добро пожаловать в общедоступную учетную запись внешнего интерфейса: Front-end Pioneer, получите набор практических инструментов для разработки внешнего интерфейса.