Websocket

License¶

Copyright (c) 2013-2014 Aymeric Augustin and contributors.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    * Neither the name of websockets nor the names of its contributors may
      be used to endorse or promote products derived from this software without
      specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Synchronization example¶

A WebSocket server can receive events from clients, process them to update the application state, and synchronize the resulting state across clients.

Here’s an example where any client can increment or decrement a counter. Updates are propagated to all connected clients.

The concurrency model of guarantees that updates are serialized.

Run this script in a console:


#!/usr/bin/env python

# WS server example that synchronizes state across clients

import asyncio
import json
import logging
import websockets

logging.basicConfig()

STATE = {"value" }

USERS = set()


def state_event():
    return json.dumps({"type" "state", **STATE})


def users_event():
    return json.dumps({"type" "users", "count" len(USERS)})


async def notify_state():
    if USERS  # asyncio.wait doesn't accept an empty list
        message = state_event()
        await asyncio.wait()


async def notify_users():
    if USERS  # asyncio.wait doesn't accept an empty list
        message = users_event()
        await asyncio.wait()


async def register(websocket):
    USERS.add(websocket)
    await notify_users()


async def unregister(websocket):
    USERS.remove(websocket)
    await notify_users()


async def counter(websocket, path):
    # register(websocket) sends user_event() to websocket
    await register(websocket)
    try
        await websocket.send(state_event())
        async for message in websocket
            data = json.loads(message)
            if data"action" == "minus"
                STATE"value" -= 1
                await notify_state()
            elif data"action" == "plus"
                STATE"value" += 1
                await notify_state()
            else
                logging.error("unsupported event: {}", data)
    finally
        await unregister(websocket)


start_server = websockets.serve(counter, "localhost", 6789)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

Then open this HTML file in several browsers.

Протокол WebSocket

Рукопожатие. Запрос браузера

Работа с веб-сокетами идёт в два этапа, сначала браузер отправляет серверу по HTTP протоколу запрос, за соединение (handshake — рукопожатие). Запрос выглядит примерно так:

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

  • GET, Host — стандартные заголовки
  • Connection, Upgrade — браузер хочет перейти на новый протокол
  • Origin — адрес с которого отправлен запрос. Мы можем это учитывать или нет.
  • Sec-WebSocket-Key — случайный ключ, который генерируется браузером в кодировке Base64, нужен чтобы понять, что ответ от сервера на подключение предназначен именно ему.
  • Sec-WebSocket-Version — версия протокол. Последняя версия 13.

Также есть дополнительные заголовки:

  • Sec-WebSocket-Extensions — расширения протокола, которые поддерживает браузер, можно указать несколько через точку с запятой. Если сервер поддерживает эти расширения, он должен ответить ответным заголовком с этим расширением. Например если указано «permessage-deflate» (сжимать фреймы по алгоритму Deflate), то в ответном сообщении, будет строка «Sec-WebSocket-Extensions: permessage-deflate». Походу только одно расширение. Подробнее .
  • Sec-WebSocket-Protocol — протокол по которому будет браузер отсылать данные. Т.к. по websocket-у можно отсылать любые данные в виде строки, указание протокола поможет легче парсить приходящие данные. Увы «JSON RPC» тут нету, зато есть куча других, например: soap, wamp и др., подробнее .

Рукопожатие. Ответ сервера

Для начала сообщим браузеру, что сервер понимает WebSocket-протокол. Для это отправим ответное сообщение:

В сообщении символ новой строки должен быть в Windows-стиле (\r\n), а в конце сообщения должно быть две новые строки (\r\n\r\n). Заголовок «Sec-WebSocket-Accept» вычисляется в зависимости от заголовка «Sec-WebSocket-Key» присланного браузером, порядок получения его таков:

  • Соединяем «Sec-WebSocket-Key» со строкой «258EAFA5-E914-47DA-95CA-C5AB0DC85B11», это строка прописана в для веб-сокетов.
  • Далее вычисляем бинарный SHA1 по полученной строке.
  • И наконец кодируем строку алгоритмом base64.

В PHP вычисление «Sec-WebSocket-Accept» будет выглядит так:

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

После того как браузер получает ответ, устанавливается постоянное TCP-соединение и обмен сообщениями между сервером и браузером осуществляется по бинарному протоколу ничего общего с HTTP не имеющего. Бинарные сообщения, которые пересылаются по этому протоколу именуют ещё «фреймами» (frame).

Формат фрейма по 32 бита, как в .

Формат фрейма по 16 бит.

Разберём первые 16 бит фрейма (далее заголовок фрейма):

  • Флаг FIN — Браузер может посылать сообщение частями, т.е. сообщение будет из несколько фреймов. Если фрейм фрагментированный, у всех фреймов кроме последнего будет 0, а у последнего 1. Если сообщение не фрагмантировано то флаг всегда будет в 1.
  • Флаги RSV1, RSV2, RSV3 почти всегда в 0, предназначены для расширений протокола.
  • Опкод — шестнадцатеричное число, указывает тип фрейма:
    • 0x1 — текстовой фрейм.
    • 0x2 — двоичный фрейм.
    • 0x3 — 0x7 — не используются, зарезервированы.
    • 0x8 — фрейм закрытия соединения
    • 0x9 — фрейм PING
    • 0xA — фрейм PONG.
    • 0xB — 0xF — не используются, зарезервированы.
    • 0x0 — обозначает фрейм-продолжение для фрагментированного сообщения
  • Флаг маски — если 1 то фрейм замаскирован
  • Длина сообщения — предварительная длина сообщения.

Длина сообщения указывается в байтах и вычисляется по схеме (ну и намудрили):

  • Если длина сообщения в заголовке фрейма 125 и ниже, то длина сообщения будет как указано в заголовке
  • Если длина сообщения в заголовке фрейма равна 126, то длиной сообщения будет следующии за заголовком 16 бит
  • Если длина сообщения в заголовке фрейма равна 127, то длиной сообщения будет следующии за заголовком 64 бита

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

Фрейм может быть замаскирован, а может быть и нет:

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

Если фрейм замаскирован, то флаг маски установливается в 1 и следующие 32 бита (4 байта) будет маска, а строка сообщения будет XOR закодировано. Это значит что над каждым байтом в сообщении будет выполнено побитовая операция «исключающее или» с байтом из маски. В PHP для это используется символ «^». Работает это примерно так «z» ^ «m» ^ «m» === «z». Пример на PHP:

Текст сообщения должен быть в кодировке UTF-8.

Примеры сообщений.

Клиент

Примитивное приложение-клиент будет реализовано с использованием модуля QtWidgets и написано на C++. Сообщения будут отображаться в обычном текстовом поле в режиме readonly (привет любителям лампового IRC :-)).

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

Режим приватного чата активируется двойным кликом по имени в списке пользователей, а закрывается этот режим специальной кнопкой , которая по умолчанию скрыта.

Сообщения отправляются нажатием на кнопку Return (Enter) на клавиатуре.

Прототип окна чата

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

Весь проект доступен на GitHub: https://github.com/wxmaper/SimpleChat-client

В рамках этой статьи рассмотрим лишь основные моменты.

Пинг пользователей

При вызове метода Worker::runAll запускаются все объявленные «работники» (их может быть несколько), а при их запуске вызывается функция Worker::onWorkerStart – здесь добавим код таймера для пинга пользователей.

Примечание

Протокол WebSocket имеет встроенную реализацию ping/pong из коробки, но мы напишем собственную, в которой сможем выполнять дополнительные действия. Однако клиент будет дополнительно оповещать сервер о наличии подключения, используя встроенную реализацию.

$worker->onWorkerStart = function($worker) use (&$connections)
{
    $interval = 5; // пингуем каждые 5 секунд


    Timer::add($interval, function() use(&$connections) {
        foreach ($connections as $c) {
            // Если ответ от клиента не пришел 3 раза, то удаляем соединение из списка
            // и оповещаем всех участников об "отвалившемся" пользователе
            if ($c->pingWithoutResponseCount >= 3) {
                $messageData = ;
                $message = json_encode($messageData);
                
                unset($connections); 
                $c->destroy(); // уничтожаем соединение
                
                // рассылаем оповещение
                foreach ($connections as $c) {
                    $c->send($message);
                }
            }
            else {
                $c->send('{"action":"Ping"}');
                $c->pingWithoutResponseCount++; // увеличиваем счетчик пингов
            }
        }
    });
};

Оповещение пользователя

Если кликнуть по имени пользователя, то имя вставляется в поле ввода сообщения и заворачивается в фигурные скобки: «{» и «}».

На сервере имеется паттерн обработки такого текста (функция $worker->onMessage), который заменяет фигурные скобки на теги «<b>» и «</b>», выделяя текст жирным шрифтом.

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

void Widget::onPublicMessage(int userId,
                             const QString &userName,
                             const QString &userColor,
                             const QString &text)
{
    if (text.contains("" + m_userName + "")) {
        qApp->beep();
        qApp->alert(this);
    }

    QString html = QString("%1 %3:"
                           " %4")
            .arg(datetime())
            .arg(userColor)
            .arg(userName)
            .arg(text)
            .arg(userId);

    ui->textBrowser->append(html);
}

По такой же схеме можно реализовать полноценную поддержку markdown, вставку смайликов и картинок. 

Расширяя функционал сервера и клиента можно также добавить:

  • поддержку чат-комнат и полноценных приватных диалогов;
  • сохранении истории сообщений в БД и её отправку при подключении или по запросу;
  • статусы пользователей («Работаю», «Отдыхаю», «Отошёл» и др.);
  • звуковые уведомления «Послать сигнал»;
  • редактирование и удаление сообщений;
  • цитирование сообщений других пользователей;
  • передачу файлов.

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

The Problem: Low Latency Client-Server and Server-Client Connections

The web has been largely built around the so-called request/response paradigm of HTTP. A client loads up a web page and then nothing happens until the user clicks onto the next page. Around 2005, AJAX started to make the web feel more dynamic. Still, all HTTP communication was steered by the client, which required user interaction or periodic polling to load new data from the server.

Technologies that enable the server to send data to the client in the very moment when it knows that new data is available have been around for quite some time. They go by names such as «Push» or «Comet». One of the most common hacks to create the illusion of a server initiated connection is called long polling. With long polling, the client opens an HTTP connection to the server which keeps it open until sending response. Whenever the server actually has new data it sends the response (other techniques involve Flash, XHR multipart requests and so called htmlfiles). Long polling and the other techniques work quite well. You use them every day in applications such as GMail chat.

However, all of these work-arounds share one problem: They carry the overhead of HTTP, which doesn’t make them well suited for low latency applications. Think multiplayer first person shooter games in the browser or any other online game with a realtime component.

Opening a websocket

When is created, it starts connecting immediately.

During the connection the browser (using headers) asks the server: “Do you support Websocket?” And if the server replies “yes”, then the talk continues in WebSocket protocol, which is not HTTP at all.

Here’s an example of browser headers for request made by .

  • – the origin of the client page, e.g. . WebSocket objects are cross-origin by nature. There are no special headers or other limitations. Old servers are unable to handle WebSocket anyway, so there are no compabitility issues. But header is important, as it allows the server to decide whether or not to talk WebSocket with this website.
  • – signals that the client would like to change the protocol.
  • – the requested protocol is “websocket”.
  • – a random browser-generated key for security.
  • – WebSocket protocol version, 13 is the current one.

WebSocket handshake can’t be emulated

We can’t use or to make this kind of HTTP-request, because JavaScript is not allowed to set these headers.

If the server agrees to switch to WebSocket, it should send code 101 response:

Here is , recoded using a special algorithm. The browser uses it to make sure that the response corresponds to the request.

Afterwards, the data is transfered using WebSocket protocol, we’ll see its structure (“frames”) soon. And that’s not HTTP at all.

There may be additional headers and that describe extensions and subprotocols.

For instance:

  • means that the browser supports data compression. An extension is something related to transferring the data, functionality that extends WebSocket protocol. The header is sent automatically by the browser, with the list of all extenions it supports.

  • means that we’d like to transfer not just any data, but the data in SOAP or WAMP (“The WebSocket Application Messaging Protocol”) protocols. WebSocket subprotocols are registered in the IANA catalogue. So, this header describes data formats that we’re going to use.

    This optional header is set using the second parameter of . That’s the array of subprotocols, e.g. if we’d like to use SOAP or WAMP:

The server should respond with a list of protocols and extensions that it agrees to use.

For example, the request:

Response:

Here the server responds that it supports the extension “deflate-frame”, and only SOAP of the requested subprotocols.

Простой сокет-сервер

В первую очередь надо в файле расскомментировать строку, позволяющую работать с сокетами и перезапустить сервер:

extension = php_sockets.dll

Вот как выглядит простейший сокет-сервер:

<?php
function SocketServer($limit = ) {
    $starttime = time();
    echo 'SERVER START' . PHP_EOL;

    echo 'Socket create...' . PHP_EOL;
    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

    if (false === $socket) {
        die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL);
    }

    echo 'Socket bind...' . PHP_EOL;
    $bind = socket_bind($socket, '127.0.0.1', 7777); // привязываем к ip и порту
    if (false === $bind) {
        die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL);
    }

    echo 'Set options...' . PHP_EOL;
    // разрешаем использовать один порт для нескольких соединений
    $option = socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
    if (false === $option) {
        die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL);
    }

    echo 'Listening socket...' . PHP_EOL;
    $listen = socket_listen($socket); // слушаем сокет
    if (false === $listen) {
        die('Error: ' . socket_strerror(socket_last_error()) . PHP_EOL);
    }

    while (true) { // бесконечный цикл ожидания подключений
        echo 'Waiting for connections...' . PHP_EOL;
        $connect = socket_accept($socket); // зависаем, пока не получим ответа
        if ($connect !== false) {
            echo 'Client connected...' . PHP_EOL;
            echo 'Send message to client...' . PHP_EOL;
            socket_write($connect, 'Hello, Client!');
        } else {
            echo 'Error: ' . socket_strerror(socket_last_error()) . PHP_EOL;
            usleep(1000);
        }

        // останавливаем сервер после $limit секунд
        if ($limit && (time() - $starttime > $limit)) {
            echo 'Closing connection...' . PHP_EOL;
            socket_close($socket);
            echo 'SERVER STOP' . PHP_EOL;
            return;
        }
    }
}

error_reporting(E_ALL); // выводим все ошибки и предупреждения
set_time_limit();      // бесконечное время работы скрипта
ob_implicit_flush();    // включаем вывод без буферизации

// Запускаем сервер в работу, завершение работы через 60 секунд
SocketServer(60);

Запустим его в работу:

> php.exe -f simple.php
SERVER START
Socket create...
Socket bind...
Set option...
Listening socket...
Waiting for connections...

Попробуем пообщаться с сервером с помощью :

> telnet

Получив приглашение , даем команду:

> open 127.0.0.1 7777

И видим сообщение от сервера:

Наш сервер в другом окне тоже встрепенулся:

Установка Workerman

Чтобы скачать Workerman, сначала устанавливаем composer:

# apt update # apt install composer

Теперь скачиваем Workerman в папку /usr/local/workerman:

# mkdir /usr/local/workerman # cd /usr/local/workerman # composer require workerman/workerman

И создаём php-файл, в котором будем писать код сервера чата:

touch ChatWorker.php

Далее открываем файл ChatWorker.php для редактирования. Это можно сделать разными способами. Самый хардкорный и олдскульный вариант — редактировать прямо в терминале, воспользовавшись консольными редакторами nano, mcedit, vim и др.

Если работаете в Linux, то из рабочего окружения KDE можно подключиться через файловый менеджер Dolphin по протоколу SFTP и открыть файл в любом редакторе или даже в IDE (например, в KDevelop).

Если работаете в Windows, то можете скачать Notepad++ с плагином NppFTP, либо что-то более продвинутое, вроде Sublime / Atom / Visual Studio Code, и так же подключиться по протоколу SFTP.

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

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

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

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

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

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

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

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

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

Browser-based example¶

Here’s an example of how to run a WebSocket server and connect from a browser.

Run this script in a console:

#!/usr/bin/env python

# WS server that sends messages at random intervals

import asyncio
import datetime
import random
import websockets

async def time(websocket, path):
    while True
        now = datetime.datetime.utcnow().isoformat() + "Z"
        await websocket.send(now)
        await asyncio.sleep(random.random() * 3)

start_server = websockets.serve(time, "127.0.0.1", 5678)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

Then open this HTML file in a browser.

Реализация сервера на PHP

Исходники простого WebSocket echo-сервера выложил сюда. Код хорошо документирован, но я всё же опишу некоторые тонкости реализации. Чтобы «поднять» WebSocket сервер нужно создать обычный TCP-сервер. В PHP TCP-сервер реализуется через «stream_socket» или через PHP расширение «sockets». Различия между ними в том, что «stream_socket» работает на встроенных функциях PHP для работы с потоками, «sockets» же работает через модуль PHP и повторяет функции для работы с сокетами в языке «C». Я выбрал «sockets».

Процесс реализован через «while» с задержкой 0.2 секунды. Процесс не форкается и сообщения выбрасывает в консоль, поэтому запускать необходимо только через консоль. Для того, чтобы обслуживать несколько клиентов одновременно, сокет делаю неблокирующим и через «socket_select» каждые 0.2 секунды прослушиваю сокет. При рукопожатии проверяю только наличие заголовков.

Фреймы парсю через «pack/unpack». Сервер не понимает фрагментированных фреймов. Сервер выдаёт только незамаскированные сообщения, т.к. некоторые браузеры не понимают замаскированных сообщений. Сервер реагирует только на текстовые фреймы и фрейм закрытия соединения, бинарные фреймы не понимает.

Ну собственно всё, удачи в исследовании этого не простого протокола.

About HTML5 WebSocket

The HTML5 WebSockets specification defines an API that enables web pages to use the WebSockets protocol for two-way communication with a remote host. It introduces the WebSocket interface and defines a full-duplex communication channel that operates through a single socket over the Web. HTML5 WebSockets provide an enormous reduction in unnecessary network traffic and latency compared to the unscalable polling and long-polling solutions that were used to simulate a full-duplex connection by maintaining two connections.

HTML5 WebSockets account for network hazards such as proxies and firewalls, making streaming possible over any connection, and with the ability to support upstream and downstream communications over a single connection, HTML5 WebSockets-based applications place less burden on servers, allowing existing machines to support more concurrent connections. The following figure shows a basic WebSocket-based architecture in which browsers use a WebSocket connection for full-duplex, direct communication with remote hosts.

One of the more unique features WebSockets provide is its ability to traverse firewalls and proxies, a problem area for many applications. Comet-style applications typically employ long-polling as a rudimentary line of defense against firewalls and proxies. The technique is effective, but is not well suited for applications that have sub-500 millisecond latency or high throughput requirements. Plugin-based technologies such as Adobe Flash, also provide some level of socket support, but have long been burdened with the very proxy and firewall traversal problems that WebSockets now resolve.

A WebSocket detects the presence of a proxy server and automatically sets up a tunnel to pass through the proxy. The tunnel is established by issuing an HTTP CONNECT statement to the proxy server, which requests for the proxy server to open a TCP/IP connection to a specific host and port. Once the tunnel is set up, communication can flow unimpeded through the proxy. Since HTTP/S works in a similar fashion, secure WebSockets over SSL can leverage the same HTTP CONNECT technique. Note that WebSockets are just beginning to be supported by modern browsers (Chrome now supports WebSockets natively). However, backward-compatible implementations that enable today’s browsers to take advantage of this emerging technology are available.

WebSockets—like other pieces of the HTML5 effort such as Local Storage and Geolocation—was originally part of the HTML5 specification, but was moved to a separate standards document to keep the specification focused. WebSockets has been submitted to the Internet Engineering Task Force (IETF) by its creators, the Web Hypertext Application Technology Working Group (WHATWG). Authors, evangelists, and companies involved in the standardization still refer to the original set of features, including WebSockets, as «HTML5.»

Сервисы, ограниченные запросом (request-scoped)

Многие среды предоставляют сервисы, или фильтры, которые управляют Web-запросами, поступающими в сервлет. Например, фильтр может выполнять:

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

Другой пример ― расширение Guice Servlet службы Google Guice (библиотека внедрения зависимостей). Как и Spring, Guice может связывать сервисы в пределах запроса. Для каждого нового запроса экземпляр создается не более одного раза (подробности см. в разделе ).

К типичным применениям такого подхода относится кэширование в запросе пользовательского объекта, извлекаемого из хранилища (например, базы данных), с помощью идентификатора пользователя, взятого из кластерного HTTP-сеанса. В Google Guice можно написать код, аналогичный коду листинга 7.

Листинг 7. Связывание с областью видимости в пределах запроса
@Provides 
@RequestScoped 
Member member(AuthManager authManager, 
              MemberRepository memberRepository) { 
    return memberRepository.findById(authManager.getCurrentUserId());
}

Когда элемент внедряется в класс, Guice пытаться извлечь его из запроса. Если тот не найден, он обращается к хранилищу и помещает результат в запрос.

Сервисы с областью видимости в пределах запроса можно использовать с любым решением Reverse Ajax, кроме WebSockets. Любое другое решение основывается на HTTP-запросах, коротких или долгоживущих, так что каждый запрос проходит через системы диспетчеризации сервлетов, и фильтры выполняются. В последующих частях этого цикла вы увидите, что при выполнении приостановленного (долгоживущего) HTTP-запроса есть также возможность еще раз пропустить запрос через цепочку фильтров.

В WebSockets данные поступают непосредственно по обратному вызову , как в TCP-сокете. В этом случае никакого HTTP-запроса нет, поэтому нет контекста запроса, из которого можно брать и сохранять объекты с областью видимости в пределах запроса. Таким образом, использовать сервис, требующий от обратного вызова request-scoped объект, не удастся.

Пример guice-и-websocket в показывает, как обойти это ограничение и использовать request-scoped объекты для обратного вызова . Если запустить пример и нажать каждую кнопку на Web-странице для проверки вызова Ajax (request-scoped), WebSocket и WebSocket с имитацией request-scoped объекта, получится результат, показанный на рисунке 3.

Рисунок 3. Результат обработчика WebSocket с использованием request-scoped сервисов

С такими проблемами можно столкнуться, используя:

  • Spring;
  • Hibernate;
  • любую другую среду, которая требует request-scoped или per-request модели, например, ;
  • любую систему, использующую средства для привязки переменных к потоку запроса внутри фильтра и последующего обращения к нему.

В Guice есть элегантное решение, которое показано в листинге 8.

Листинг 8. Имитация request-scoped модели из обратного вызова
// Ссылка на запрос удерживается при вызове 
// doWebSocketMethod
HttpServletRequest request =  
Map<Key<?>, Object> bindings = new HashMap<Key<?>, Object>(); 
// У меня есть сервис, которому требуется запрос для получения доступа к сеансу, 
// и я подаю такой запрос, но вы можете предоставить любое другое 
// связывание, которое может вам понадобиться
bindings.put(Key.get(HttpServletRequest.class), request); 
ServletScopes.scopeRequest(new Callable<Object>() {
 	@Override 
	public Object call() throws Exception { 
  		// обращение к хранилищу или любому сервису с использованием объектов, 
  		// ограниченных одним запросом. 
		outbound.sendMessage(); 
		return null; 
	} 
}, bindings).call();

Настройка VDS-сервера

Сразу после приобретения VDS и установки операционной системы (выбрал свежую версию Ubuntu 18.04 на тарифе «Master») подключаемся к нему. На сервер можно зайти через консоль из панели управления VDS, но это не самый удобный вариант. Предпочтительнее подключаться по SSH.

Если разные способы подключения по SSH из Windows, например:  1. Воспользоваться программой Putty; 2. Воспользоваться терминалом Cygwin; 3. Воспользоваться терминалом Ubuntu из WSL (я выбрал этот способ).

В Linux намного проще, клиент для подключения по SSH, как правило, установлен во всех дистрибутивах по умолчанию, поэтому просто открываем терминал.

Независимо от выбранного способа, команда для подключения будет одна:

ssh -l root {VDS_IP_ADDRESS}

где {VDS_IP_ADDRESS} – это IP-адрес вашего сервера, который можно найти в панели управления VDS (блок «Список используемых IP-адресов»).

Окно терминала

一个简单例子

要打开一个 WebSocket 连接,我们需要在 url 中使用特殊的协议 创建 :

同样也有一个加密的 协议。类似于 WebSocket 中的 HTTPS。

始终使用

协议不仅是被加密的,而且更可靠。


因为 数据不是加密的,对于任何中间人来说其数据都是可见的。并且,旧的代理服务器不了解 WebSocket,它们可能会因为看到“奇怪的” header 而中止连接。

另一方面, 是基于 TLS 的 WebSocket,类似于 HTTPS 是基于 TLS 的 HTTP),传输安全层在发送方对数据进行了加密,在接收方进行解密。因此,数据包是通过代理加密传输的。它们看不到传输的里面的内容,且会让这些数据通过。

一旦 socket 被建立,我们就应该监听 socket 上的事件。一共有 4 个事件:

  • —— 连接已建立,
  • —— 接收到数据,
  • —— WebSocket 错误,
  • —— 连接已关闭。

……如果我们想发送一些东西,那么可以使用 。

这是一个示例:

出于演示目的,在上面的示例中,运行着一个用 Node.js 写的小型服务器 server.js。它响应为 “Hello from server, John”,然后等待 5 秒,关闭连接。

所以你看到的事件顺序为: → → 。

这就是 WebSocket,我们已经可以使用 WebSocket 通信了。很简单,不是吗?

现在让我们更深入地学习它。

Общая информация о веб-сокетах

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

Рассмотрим классическую схему уведомления о сообщениях на сайте. Когда пользователь авторизуется на сайте, браузер каждый 30 секунд (может и чаще) шлёт ajax-запрос на сайт, по определённому урлу. Запрос типа — «Пришли ли мне новые сообщения». Сервер в большинстве случаев будет отвечать «Сообщений новых нет», и только изредка долгожданное «У вас 1 новое сообщение». Когда пользователей не много такая схема устраивает, но когда их много сервер получает до 1000 и более безсмысленных запросов. Такая схема использовалась, потому что http построен по принципу сделал запрос, получил ответ и «давай до свидание». В http нет возможности отправить сообщение от сервера браузеру, если браузер не спросит. При схеме с веб-сокетами браузеру достаточно создать соединение и ждать, сервер сам ответит браузеру, когда нужно. Преимущество на лицо — значительно снижается трафик и нагрузка на сервер, и уведомление приходит моментально. Широта использования веб-сокетов велика: чаты, уведомления, «доставучие» online-консультанты и прочее.

Простой клиент веб-сокетов

С точки зрения веб-страницы функциональность веб-сокетов легко понять и использовать. Первый шаг — это создать объект WebSocket и передать ему URL. Код для этого подобен следующему:

Строка URL начинается с текста ws://, который идентифицирует подключение типа веб-сокет. Этот URL указывает файл веб-приложения на сервере (в данном случае это сценарий socketServer.php).

Стандарт веб-сокетов также поддерживает URL, которые начинаются с текста wss://, что указывает на требование использовать безопасное, зашифрованное подключение (точно так же, как и при запросе веб-страницы указывается URL, начинающийся с https:// вместо http://).

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

Само обстоятельство создания объекта WebSocket понуждает страницу пытаться подключиться к серверу. Дальше надо использовать одно из четырех событий объекта WebSocket: onOpen (при установлении подключения), onError (когда возникает ошибка), onClose (при закрытии подключения) и onMessage (когда страница получает сообщение от сервера):

Например, в случае успешного подключения неплохо бы отправить соответствующее подтверждающее сообщение. Такое сообщение доставляется с помощью метода send() объекта WebSocket, которому в качестве параметра передается обычный текст. Далее приведена функция, которая обрабатывает событие onopen и отправляет сообщение:

Предположительно, веб-сервер получит это сообщение и даст на него ответ.

События onError и onClose можно использовать для отправки извещений посетителю веб-страницы. Но безоговорочно самым важным является событие onMessage, которое срабатывает при получении новых данных от сервера. Опять же, код JavaScript для обработки этого события не представляет никаких сложностей — мы просто извлекаем текст сообщения из свойства data:

Если веб-страница решит, что вся ее работа выполнена, она может закрыть подключение, используя метод disconnect():

Из этого обзора веб-сокетов можно видеть, что использование сервера веб-сокетов стороннего разработчика не представляет никаких трудностей — нам нужно лишь знать, какие сообщения отправлять, а какие — ожидать.

Чтобы заставить подключение веб-сокетов работать, выполняется большой объем работы за кулисами. Прежде всего, веб-страница устанавливает связь по обычному стандарту HTTP. Потом это подключение нужно повысить до подключения веб-сокетов, позволяющего свободную двустороннюю связь. На этом этапе возможны проблемы, если между компьютером клиента и веб-сервером находится прокси-сервер (как, например, в типичной корпоративной сети). Прокси-сервер может отказаться сотрудничать и разорвет подключение. Эту проблему можно решить, обнаруживая неудачное подключение (посредством события onError объекта WebSocket) и применяя один из заполнителей (polyfills) для сокетов, описанных на веб-сайте GitHub. Эти заполнители применяют метод опроса, чтобы эмулировать подключение веб-сокетов.

Chat example

Let’s review a chat example using browser WebSocket API and Node.js WebSocket module https://github.com/websockets/ws. We’ll pay the main attention to the client side, but the server is also simple.

HTML: we need a to send messages and a for incoming messages:

From JavaScript we want three things:

  1. Open the connection.
  2. On form submission – for the message.
  3. On incoming message – append it to .

Here’s the code:

Server-side code is a little bit beyond our scope. Here we’ll use Node.js, but you don’t have to. Other platforms also have their means to work with WebSocket.

The server-side algorithm will be:

  1. Create – a set of sockets.
  2. For each accepted websocket, add it to the set and setup event listener to get its messages.
  3. When a message received: iterate over clients and send it to everyone.
  4. When a connection is closed: .

Here’s the working example:

You can also download it (upper-right button in the iframe) and run locally. Just don’t forget to install Node.js and before running.


С этим читают