Вебсокеты: боевое применение

Реализация клиента на Javascript

Протокол веб-сокет создан уже давно (приобрёл статус RFC в 11.12.2011) и поддерживается большинством браузеров.
Чтобы узнать поддерживает ли ваш браузер веб-сокеты перейдите по .

Работа в браузерах с вебсокетам проходит в несколько этапов:

  • Установка соединения или рукопожатие (handshake).
  • Создание обработчиков событий: onopen (соединение создано), onclose(соединение закрыто), onmessage (пришло сообщение от сервера), onerror (ошибка при работе веб-сокетов).
  • Отправка сообщений (фреймов) на сервер.

Тестировать веб-сокеты мы будем на сервере websocket.org «ws://echo.websocket.org», который будет принимать от нас сообщения и отвечать на них повторением сообщением.
Этот сайт как раз существует, что лучше понять веб-сокеты, он понимает кросс-доменные запросы, поэтому страницу с JavaScript будем размещать у себя на локальном компьютере.

Этап. Рукопожатие

Чтобы создать соединение по веб-сокету достаточно создать объект WebSocket, в котором указывается урл для подключения.

Используйте протокол «ws://», если нужно не шифрованное соединение или протокол «wss://» для шифрованного соединения.

Этап. Создание обработчиков событий.

После того как мы создали объект WebSocket необходимо повесить функции-обработчики на события.

Если нужно повесить несколько функций на событие используем методы «addEventListener» и «removeEventListener». Пример:

Этап. Отправка сообщений на сервер

По веб-сокету сообщения отправляются в виде строки. Пример отправки простого текстового сообщения.

Обработка приходящих данных лежит уже на стороне сервера. Чаще для удобства работы по вебсокету отправляют JSON данные серилизованные в строку и обрабатывают приходящие данные как строка в JSON-e. Пример использования:

Удобный способ отправки сообщений по веб-сокету служит протокол «JSON-RPC» (ссылка). Это очень простой протокол, который облегчит взаимодействие браузера и сервера. Пример использования JSON RPC:

Параметры json-rpc объекта:

  • jsonrpc — версия протокола, может быть «2.0» или «1.0»
  • id — идентификатор запроса. Используется для идентификации ответа от сервера по своем запросу. Т.е. если отправить два запроса, то ответ от сервера по каждому запросу прийдёт в разное время, для этого и нужен id. На сервере необходимо учитывать этот параметр и в ответ прислать именно нужный id.
  • method — наименование метода, любая строка, к примеру «get», «hello», «set» и др.
  • params — параметры связанные с этим методом, тип переменной может быть любой, всё зависит от сервера.

Чтобы закрыть соединение используем метод close().

Node.js и socket.io¶

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

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

app.js

index.html

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

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

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

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

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

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

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

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

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

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

Закрытие подключения

Обычно, когда сторона хочет закрыть соединение (браузер и сервер имеют равные права), они отправляют «фрейм закрытия соединения» с кодом закрытия и указывают причину в виде текста.

Метод для этого:

  • – специальный WebSocket-код закрытия (не обязателен).
  • – строка с описанием причины закрытия (не обязательна).

Затем противоположная сторона в обработчике события получит и код и причину , например:

– это не любое число, а специальный код закрытия WebSocket.

Наиболее распространённые значения:

  • – по умолчанию, нормальное закрытие,
  • – невозможно установить такой код вручную, указывает, что соединение было потеряно (нет фрейма закрытия).

Есть и другие коды:

  • – сторона отключилась, например сервер выключен или пользователь покинул страницу,
  • – сообщение слишком большое для обработки,
  • – непредвиденная ошибка на сервере,
  • …и так далее.

Полный список находится в .

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

Поддержка служб IIS/IIS Express

Windows Server 2012 или более поздней версии и Windows 8 или более поздней версии с IIS и IIS Express 8 или более поздней версии поддерживают протокол WebSocket.

Примечание

Соединения WebSockets всегда включены при использовании IIS Express.

Включение WebSockets в службах IIS

Чтобы включить поддержку протокола WebSocket в Windows Server 2012 или более поздней версии:

Примечание

Эти действия не требуется выполнять при использовании IIS Express

  1. В меню Управление запустите мастер Добавить роли и компоненты или в окне Диспетчер серверов щелкните соответствующую ссылку.
  2. Выберите Установка ролей или компонентов. Выберите Далее.
  3. Выберите подходящий сервер (по умолчанию выбирается локальный сервер). Выберите Далее.
  4. Разверните Веб-сервер (IIS) в дереве Роли, разверните Веб-сервер, а затем Разработка приложений.
  5. Выберите протокол WebSocket. Выберите Далее.
  6. Если дополнительные функции не требуются, нажмите Далее.
  7. Нажмите кнопку Установить.
  8. По завершении установки выберите Закрыть, чтобы выйти из мастера.

Чтобы включить поддержку протокола WebSocket в Windows 8 или более поздней версии:

Примечание

Эти действия не требуется выполнять при использовании IIS Express

  1. Последовательно выберите Панель управления > Программы > Программы и компоненты > Включение или отключение компонентов Windows (в левой части экрана).
  2. Откройте следующие узлы: IIS > Службы Интернета > Компоненты разработки приложений.
  3. Выберите компонент Протокол WebSocket. Нажмите кнопку ОК.

Отключите WebSocket при использовании socket.io на Node.js

Если используется поддержка WebSocket в socket.io на Node.js, отключите модуль WebSocket IIS по умолчанию с помощью элемента в web.config или applicationHost.config. Если не выполнить этот шаг, модуль IIS WebSocket попытается обработать соединение WebSocket, а не Node.js и приложение.

Low-level API¶

Exceptions

exception

Exception raised when a handshake request or response is invalid.

exception

Exception raised when an operation is forbidden in the current state.

exception

Exception raised when an URI is invalid.

Opening handshake

The module deals with the WebSocket opening
handshake according to .

It provides functions to implement the handshake with any existing HTTP
library. You must pass to these functions:

  • A set_header function accepting a header name and a header value,
  • A get_header function accepting a header name and returning the header
    value.

The inputs and outputs of get_header and set_header are
objects containing only ASCII characters.

Some checks cannot be performed because they depend too much on the
context; instead, they’re documented below.

To accept a connection, a server must:

  • Read the request, check that the method is GET, and check the headers with
    ,
  • Send a 101 response to the client with the headers created by
    if the request is valid; otherwise, send a 400.

To open a connection, a client must:

  • Send a GET request to the server with the headers created by
    ,
  • Read the response, check that the status code is 101, and check the headers
    with .
(set_header)

Build a handshake request to send to the server.

Return the key which must be passed to .

(get_header)

Check a handshake request received from the client.

If the handshake is valid, this function returns the key which must be
passed to .

Otherwise, it raises an exception and the server
must return an error, usually 400 Bad Request.

This function doesn’t verify that the request is an HTTP/1.1 or higher GET
request and doesn’t perform Host and Origin checks. These controls are
usually performed earlier in the HTTP request handling code. They’re the
responsibility of the caller.

(set_header, key)

Build a handshake response to send to the client.

key comes from .

(get_header, key)

Check a handshake response received from the server.

key comes from .

If the handshake is valid, this function returns .

Otherwise, it raises an exception.

This function doesn’t verify that the response is an HTTP/1.1 or higher
response with a 101 status code. These controls are the responsibility of
the caller.

Data transfer

The module implements data framing as specified in
.

It deals with a single frame at a time. Anything that depends on the sequence
of frames is implemented in .

class (fin, opcode, data)

Alias for field number 2

Alias for field number 0

Alias for field number 1

(reader, mask, *, max_size=None)

Read a WebSocket frame and return a object.

reader is a coroutine taking an integer argument and reading exactly this
number of bytes, unless the end of file is reached.

mask is a telling whether the frame should be masked, ie.
whether the read happens on the server side.

If max_size is set and the payload exceeds this size in bytes,
is raised.

This function validates the frame before returning it and raises
if it contains incorrect values.

(frame, writer, mask)

Write a WebSocket frame.

frame is the object to write.

writer is a function accepting bytes.

mask is a telling whether the frame should be masked, ie.
whether the write happens on the client side.

This function validates the frame before sending it and raises
if it contains incorrect values.

(data)

Parse the data in a close frame.

Return (code, reason) when code is an and reason a
.

Raise or if the
data is invalid.

(code, reason)

Serialize the data for a close frame.

This is the reverse of .

URI parser

The module implements parsing of WebSocket URIs
according to .

(uri)

This function parses and validates a WebSocket URI.

If the URI is valid, it returns a namedtuple (secure, host, port,
resource_name)

Otherwise, it raises an exception.

Передача данных

Поток данных в WebSocket состоит из «фреймов», фрагментов данных, которые могут быть отправлены любой стороной, и которые могут быть следующих видов:

  • «текстовые фреймы» – содержат текстовые данные, которые стороны отправляют друг другу.
  • «бинарные фреймы» – содержат бинарные данные, которые стороны отправляют друг другу.
  • «пинг-понг фреймы» используется для проверки соединения; отправляется с сервера, браузер реагирует на них автоматически.
  • также есть «фрейм закрытия соединения» и некоторые другие служебные фреймы.

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

Метод WebSocket может отправлять и текстовые и бинарные данные.

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

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

Это задаётся свойством , по умолчанию оно равно , так что бинарные данные поступают в виде -объектов.

Blob – это высокоуровневый бинарный объект, он напрямую интегрируется с , и другими тегами, так что это вполне удобное значение по умолчанию. Но для обработки данных, если требуется доступ к отдельным байтам, мы можем изменить его на :

FAQ

How to get the IP address of the client?

The remote IP address can be obtained from the raw socket.

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws, req) {
  const ip = req.socket.remoteAddress;
});

When the server runs behind a proxy like NGINX, the de-facto standard is to use
the header.

wss.on('connection', function connection(ws, req) {
  const ip = req.headers'x-forwarded-for'.split(',').trim();
});

How to detect and close broken connections?

Sometimes the link between the server and the client can be interrupted in a way
that keeps both the server and the client unaware of the broken state of the
connection (e.g. when pulling the cord).

In these cases ping messages can be used as a means to verify that the remote
endpoint is still responsive.

import { WebSocketServer } from 'ws';

function heartbeat() {
  this.isAlive = true;
}

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.isAlive = true;
  ws.on('pong', heartbeat);
});

const interval = setInterval(function ping() {
  wss.clients.forEach(function each(ws) {
    if (ws.isAlive === false) return ws.terminate();

    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', function close() {
  clearInterval(interval);
});

Pong messages are automatically sent in response to ping messages as required by
the spec.

Just like the server example above your clients might as well lose connection
without knowing it. You might want to add a ping listener on your clients to
prevent that. A simple implementation would be:

import WebSocket from 'ws';

function heartbeat() {
  clearTimeout(this.pingTimeout);

  // Use `WebSocket#terminate()`, which immediately destroys the connection,
  // instead of `WebSocket#close()`, which waits for the close timer.
  // Delay should be equal to the interval at which your server
  // sends out pings plus a conservative assumption of the latency.
  this.pingTimeout = setTimeout(() => {
    this.terminate();
  }, 30000 + 1000);
}

const client = new WebSocket('wss://echo.websocket.org/');

client.on('open', heartbeat);
client.on('ping', heartbeat);
client.on('close', function clear() {
  clearTimeout(this.pingTimeout);
});

SignalR

ASP.NET Core SignalR — это библиотека, которая упрощает добавление веб-функций в режиме реального времени в приложения. Она использует WebSocket, когда это возможно.

Для большинства приложений рекомендуется использовать SignalR вместо прямых соединений WebSocket. Служба SignalR обеспечивает резервный транспорт для сред, в которых соединения WebSocket недоступны. Она также предоставляет простую модель приложений на основе удаленного вызова процедур. В большинстве сценариев SignalR по производительности не уступает прямым соединениям WebSocket.

Для некоторых приложений gRPC в .NET предоставляет альтернативу WebSockets.

Обработка сообщений

Вся логика общения клиента с сервером реализована в слоте Widget::onTextMessageReceived – тут проверяется тип входящего сообщения (какое он имеет «действие») и вызываются соответствующие методы для его обработки.

void Widget::onTextMessageReceived(const QString &message)
{
    // Преобразуем полученное сообщение в JSON-объект
    QJsonObject messageData = QJsonDocument::fromJson(message.toUtf8()).object();

    QString action = messageData.value("action").toString();

    if (action == "Ping") {
        // В ответ на "Ping" клиент должен послать действие "Pong",
        // чтобы сервер понял, что клиент в онлайне
        sendPong();
    }
    else {
        int userId = messageData.value("userId").toInt();
        QString userName = messageData.value("userName").toString();
        Gender gender = Gender(messageData.value("gender").toInt());
        QString userColor = messageData.value("userColor").toString();

        if (action == "Authorized") {
            onUserAuthorized(userId, userName, gender);
            QJsonArray users = messageData.value("users").toArray();
            addUsers(users);
        }

        else if (action == "Connected") {
            onUserConnected(userId, userName, gender, userColor);
        }

        else if (action == "Disconnected") {
            onUserDisconnected(userId, userName, gender, userColor);
        }

        else if (action == "ConnectionLost") {
            onConnectionLost(userId, userName, gender, userColor);
        }

        else if (action == "PublicMessage") {
            QString text = messageData.value("text").toString();
            onPublicMessage(userId, userName, userColor, text);
        }

        else if (action == "PrivateMessage") {
            QString text = messageData.value("text").toString();
            onPrivateMessage(userId, userName, userColor, text);
        }

        else {
            // неизвестное действие, можно добавить оповещение 
            qWarning() << "unknown action: " << action;
        }
    }
}

Miscellaneous

Note: WebSocket codes, extensions, subprotocols, etc. are registered at the IANA WebSocket Protocol Registry.

WebSocket extensions and subprotocols are negotiated via headers during . Sometimes extensions and subprotocols are very similar, but there is a clear distinction. Extensions control the WebSocket frame and modify the payload, while subprotocols structure the WebSocket payload and never modify anything. Extensions are optional and generalized (like compression); subprotocols are mandatory and localized (like ones for chat and for MMORPG games).

Note: Extensions are explained in sections 5.8, 9, 11.3.2, and 11.4 of the spec.

Think of a subprotocol as a custom XML schema or doctype declaration. You’re still using XML and its syntax, but you’re additionally restricted by a structure you agreed on. WebSocket subprotocols are just like that. They do not introduce anything fancy, they just establish structure. Like a doctype or schema, both parties must agree on the subprotocol; unlike a doctype or schema, the subprotocol is implemented on the server and cannot be externally referred to by the client.

Note: Subprotocols are explained in sections 1.9, 4.2, 11.3.4, and 11.5 of the spec.

A client has to ask for a specific subprotocol. To do so, it will send something like this as part of the original handshake:

or, equivalently:

Now the server must pick one of the protocols that the client suggested and it supports. If there is more than one, send the first one the client sent. Imagine our server can use both and . Then, in the response handshake, it sends:

Warning: The server can’t send more than one header.
If the server doesn’t want to use any subprotocol, it shouldn’t send any header. Sending a blank header is incorrect. The client may close the connection if it doesn’t get the subprotocol it wants.

If you want your server to obey certain subprotocols, then naturally you’ll need extra code on the server. Let’s imagine we’re using a subprotocol . In this subprotocol, all data is passed as JSON. If the client solicits this protocol and the server wants to use it, the server needs to have a JSON parser. Practically speaking, this will be part of a library, but the server needs to pass the data around.

Note: To avoid name conflict, it’s recommended to make your subprotocol name part of a domain string. If you are building a custom chat app that uses a proprietary format exclusive to Example Inc., then you might use this: . Note that this isn’t required, it’s just an optional convention, and you can use any string you wish.

Создание объекта WebSocket

; при его создании автоматически происходит попытка открыть соединение с сервером.

Конструктор WebSocket принимает один обязательный и один необязательный параметр:

WebSocket WebSocket(
  in DOMString url,
  in optional DOMString protocols
);

WebSocket WebSocket(
  in DOMString url,
  in optional DOMString[] protocols
);
URL, с которым происходит соединение; это должен быть URL веб-сокет-сервера.
Необязательный
Может быть одной строкой протокола или массивом таких строк. Эти строки используют для индикации под-протоколов; таким образом, один сервер может реализовывать несколько под-протоколов веб-сокетов (к примеру, вам может потребоваться, чтобы сервер мог обрабатывать разные типы взаимодействий в зависимости от определённого под-протокола). Если вы не укажете строку протокола, то будет передана пустая строка.

В конструкторе могут возникать следующие исключения:

Порт, к которому проводится подключение, заблокирован.

Если ошибка случается во время попытки подключения, то в объект  сначала посылается простое событие с именем «error» (таким образом, задействуя обработчик ), потом — событие  (таким образом, задействуя обработчик ) чтобы обозначить причину закрытия соединения.

Однако, начиная с версии Firefox 11, типичным является получение в консоль от платформы Mozilla расширенного сообщения об ошибке и кода завершения, как то определено в  посредством .

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

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

Если нужно открыть соединение, поддерживающее несколько протоколов, можно передать массив протоколов:

Когда соединение установлено (что соответствует,  ),  сообщит, какой протокол выбрал сервер.

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

Комментарии

некоторые классы в System.Net.WebSockets пространстве имен поддерживаются в Windows 7, Windows Vista с пакетом обновления 2 (SP2) и Windows Server 2008. однако в Windows 8 и Windows Server 2012 поддерживаются только общедоступные реализации websocket клиента и сервера. классы и элементы класса в System.Net.WebSockets пространстве имен, которые поддерживаются в Windows 7, Windows Vista с пакетом обновления 2 (SP2) и Windows Server 2008, являются абстрактными классами. Это позволяет разработчику приложения наследовать и расширять эти абстрактные классы с помощью фактической реализации клиентских WebSocket.

IIS/IIS Express support

Windows Server 2012 or later and Windows 8 or later with IIS/IIS Express 8 or later has support for the WebSocket protocol.

Note

WebSockets are always enabled when using IIS Express.

Enabling WebSockets on IIS

To enable support for the WebSocket protocol on Windows Server 2012 or later:

Note

These steps are not required when using IIS Express

  1. Use the Add Roles and Features wizard from the Manage menu or the link in Server Manager.
  2. Select Role-based or Feature-based Installation. Select Next.
  3. Select the appropriate server (the local server is selected by default). Select Next.
  4. Expand Web Server (IIS) in the Roles tree, expand Web Server, and then expand Application Development.
  5. Select WebSocket Protocol. Select Next.
  6. If additional features aren’t needed, select Next.
  7. Select Install.
  8. When the installation completes, select Close to exit the wizard.

To enable support for the WebSocket protocol on Windows 8 or later:

Note

These steps are not required when using IIS Express

  1. Navigate to Control Panel > Programs > Programs and Features > Turn Windows features on or off (left side of the screen).
  2. Open the following nodes: Internet Information Services > World Wide Web Services > Application Development Features.
  3. Select the WebSocket Protocol feature. Select OK.

Disable WebSocket when using socket.io on Node.js

If using the WebSocket support in socket.io on Node.js, disable the default IIS WebSocket module using the element in web.config or applicationHost.config. If this step isn’t performed, the IIS WebSocket module attempts to handle the WebSocket communication rather than Node.js and the app.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector