Коллеги, приветствую. Хочу вынести на публичное обсуждение свои мысли и некоторые моменты реализации своего проекта. Websockets — тема пожалуй уже избитая, но меня простимулировала на этот шаг работа “WebRTC Cookbook” под авторством Andrii Sergiienko, в которой технология Websockets используется в качестве сигнального сервиса для управления потоковыми данными.

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

Вышеупомянутая книга сопровождается примерами. Одним их них является реализация на Java сервиса Websockets, где мультиплексирование данных происходит по номеру комнаты чата. Номер (или имя) комнаты назначается прикладным программистом. Не хотелось бы сразу указывать на это, как на определённый недостаток. Всё таки это учебный проект. Скажу лишь, что для своего проекта я ставил следующие требования:

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

Вопросы по шифрованию данных и надёжности работы такого сервиса пожалуй можно пока не поднимать. Всё таки это немного из другой предметной области.

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

image

На рисунке цифрами обозначены следующие участники взаимодействия:

  1. Различные источники событий в сети.
  2. HTTP-сервер, где эти события от источников принимаются, накапливаются, обрабатываются.
  3. Сервис Websockets, применяемый собственно для online-уведомлений потребителей информации. Помимо собственно этого сервиса здесь должна быть и интерграция со “своим” HTTP-сервером, транслирующим структурированную информацию от различных проектов.
  4. Это собственно клиенты, являющимися приёмниками обработанной информации от различных источников 1.

Чтобы как-то упорядочить данные для обмена сообщениями с сервисом предлагается выбрать JSON-формат. Такой выбор обуславливается простотой работы с JSON-строками, широким набором разлиных библиотек и их переносимостью для разных платформ.

{
"WSCI_TYPE" : "WSCI_REG",
"WSCI_ID" : "bqOPfKmKCPV … zJS2LqtFang"
}

WSCI_TYPE — тип полученного сообщения. В данном случае WSCI_REG означает, что это регистрационное сообщение, в котором через параметр WSCI_ID передаётся идентификатор или токен соединения. Данным ключём приложение “делится” с другими пользователями сервиса. Ключ может записываться в базу данных либо в какие-то списки с общим доступом и других пользователей различных сервисов. Отмечу, что похожий принцип работы был заимствован у Google. У них есть сервис GCM (Google Clouds Messaging).

Следующим типом сообщения является WSCI_DATA. В нём передаются собственно данные пользователя. Выглядит это так:

{
"WSCI_TYPE" : "WSCI_DATA",
"WSCI_DATA" : “Некоторые данные для пользователя”
}

Между пользовательским сервером (2) и сервисом WebSockets (3) передаётся ещё один тип сообщения, формат которого следующий:

{
    "WSCI_ARRAY" : [список токенов (идентификаторов соединений)],
    "WSCI_DATA"  : "Некоторые данные"
}

Например:

{
    “WSCI_ARRAY”:[“d97I.....r2Oo”, “o7yz....tIu7”],
    “WSCI_DATA”:”This is your data”
}

Массив WSCI_ARRAY содержит список идентификаторов соединений (токенов). На PHP формирование такого сообщения выглядит следующим образом:

$testJSON = array(
    'WSCI_ARRAY'=>array(
        "d97I....r2Oo",
        "o7yz....MtIu7"
    ),
    WSCI_DATA'=>'This is a data for service.'
);

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

define('WSCI_SERVICE', "http://95.47.161.69/wsci.php");
class WsciCurl {
    function __construct(){}
    function __destruct(){}

    //
    function SendDataToWSCI($wsci_array, $wsci_data){
        $data = base64_encode(
            json_encode(
                array(
                    "WSCI_ARRAY"=>$wsci_array,
                    "WSCI_DATA"=>$wsci_data
                )
            )
        );
        $fields = array('data'=>$data);
        //
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, WSCI_SERVICE);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
        $response = curl_exec($ch);
        curl_close($ch);
        $json_answer = json_decode($response);
        return $json_answer->{'result'}==='success';
    }
};
Листинг 1. Передача данных от сервера пользователя в сервис Websockets.

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

require_once('wsci_curl.php');
$wsci_array = array(
    "d97ICUblJKsOBH2YmWJqf0WU8Z9IaMemutFNLH8adFuFkXKmAe0ze3zlptSr2Oo",
    "o7yzZjIp1tcoaREUBG8h4XObLTbTykMk2zSFSJ2TFEA5qV0a4Vt1g0Hno3MtIu7"
);
$wsci_data = 'This is a data for service.';
$wsci = new WsciCurl();
$wsci->SendDataToWSCI($wsci_array, $wsci_data);

В массиве $wsci_array размещаются токены соединений, в которые необходимо отправить данные.

В качестве примера обработчика событий на клиентской стороне (4) можно предложить следующий фрагмент кода на JavaScript:

var wsUrl = "ws://95.47.161.69:12080";
//
var ws = new WebSocket(wsUrl);
ws.onopen = function(evt){ onOpen(evt) };
ws.onclose = function(evt){  onClose(evt) };
ws.onmessage = function(evt){ onMessage(evt) };
ws.onerror = function(evt){ onError(evt) };
....
ws.close();
....

function onOpen(evt){
    alert("Connected!");
}

function onClose(evt){
    alert('Finishing code: ' + evt.code);
}

function onMessage(evt){
    var jsonObj = JSON.parse(evt.data);
    var type = jsonObj.WSCI_TYPE;
    switch( type ) {
        case "WSCI_REG"     : {
            var id = jsonObj.WSCI_ID;
            alert('Key = ' + id);
            //Здесь можно сохранить идентификатор, например, в БД
            break;
        }
        case "WSCI_DATA"    : {
            var data = jsonObj.WSCI_DATA;
            alert('Data: ' + data);
            //Здесь идёт дополнительная обработка и отображение полученных данных
            break;
        }
    }
}

function onError(evt){
    alert('Error: '+evt.data);
}
Листинг 2. Пример обработчика событий в браузере клиента.

Теперь рассмотрим узел (3) и разберём, что здесь происходит. Если обратить внимание на листинг 1, то видно, что модуль wsci.php является точкой входа в этот сервис и предназначен для приёма пользовательских данных (для уточнения — принимаются по протоколу HTTP) и их трансляции в сервис Websockets. Текст модуля следующий:

<?php
define('WSCI_SERVER', "127.0.0.1");
define('WSCI_PORT', 12080);
require_once('wsci_client.php');

ini_set('display_errors', 1);
error_reporting(E_ALL);

//Получаем данные POST-запроса
$data = $_POST["data"];
if ( empty($data) || strlen($data)==0 ) {
//There are no data for service
        $responce = array('result'=>'fail');
        echo json_encode($responce);
        exit(0);
}
$data = base64_decode($data);    //Это данные для передачи
//Соединяемся с вебсокетом
$wsci = new WsciClient();
$bc = false;
//Делаем попытки локального подключения к Websockets
for($i=0; $i<10; $i++, usleep(200000))
if ( ($bc = $wsci->connect(WSCI_SERVER, WSCI_PORT, "/"))==true ) break;
//Проверяем результат открытия
if ( !$bc ) {
//Connection to websocket service is fail
$responce = array('result'=>'fail');
echo json_encode($responce);
exit(0);
}

//Передаем данные в вебсокет
$wsci->sendText($data);
//Закрываем соединение
$wsci->sendClose();
$wsci->disconnect();
//
//Возвращаем успех
echo json_encode(array("result"=>"success"));
?>

Листинг 3. Модуль трансляции пользовательских данных в сервис вебсокетов

Рассмотрим, что и как должно происходить в сервисе Websockets. Во первых, необходимо определиться протоколе взаимодействия с клиентами. Могу ошибиться, но сейчас устоявшимся стандартом является RFC_6455. Во-вторых, в минимальном уровне функциональности сервиса. Представляется (и это реализовано), что сервис должен выполнять следующий набор функций:

  • Соединение с клиентами, поддержка списка соединений с контролем их количества с одно IP-адреса;
  • Поиск необходимого соединения по ключу (токену, идентификатору) и проталкиванию данных пользователя в это соединение;
  • Поиск и закрытие “зависших” соединений;
  • Приём данных от клиентов должен осуществляться в соответствии со спецификацией, показанной и реализованной в листинге 1;
  • Парсинг данных клиента заключается в получении списка токенов (массив WSCI_ARRAY) и собственно данных (узел WSCI_DATA);

Непосредственная реализация сервиса

Разработке и отладке предшествовала работа по поиску и анализу готовых решений и возможностей их использования для своих целей. Встречались совершенно экзотические варианты реализаций, например, на JavaScript. Хорошим случаем была PHP-реализация, в которой был запрограммирован контроль эха. На Java много готовых, хороших примеров. Существует вариант модуля Websockets, который интегрируется в Apache. Но так как мы не ищем простых путей, то был выбран вариант разработки на C++. Трудно, хлопотно, муторно отлаживать, но тем не менее был выбран этот вариант по следующим соображениям:

  • Скорость работы приложения. При одинаковых алгоритмах работа загрузочного модуля будет быстрей, чем, к примеру, приложения на Java порядка 40 раз (это не моя оценка). Быстродействие важно для обработки большого числа соединений;
  • Возможность использования параллельных программных потоков обработки данных.

Некоторые вопросы и трудности в реализации

При проектировании приложений всегда приходится учитывать реалии бытия, закладываться на ограничения окружения, производительности оборудования, возможностей операционной системы, пропускной способности каналов связи и т.д. Так вот, хотелось бы услышать Ваше мнение, уважаемые коллеги, по поводу максимального число соединений с учётом хостинга на VDS. На текущий момент нагрузка на сервис минимальна, под массив соединений выделено 100 позиций. А будет ли успешно работать сервис при, допустим, 10000 соединений? Есть у кого-нибудь такой опыт?

Теперь по размеру ключа (токена) соединения. Насколько мне помнится, у Google длина токена в сервисе GCM 256 байт. Вопрос, насколько это оправдано? Не является ли такой длинный ключ избыточным? Дело в том, что при большом числе соединений, поиск конкретного соединения по ключу, может занять продолжительное время. Опять же, сортировка этих объектов по ключу. На всякий случай отмечу, что в моём случае размер ключа равен 64 байтов.

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

Перспективы

Можно сделать “облако”. Конечно, в экономическом аспекте конкурировать с “монстрами” IT-отрасли не реально. Просто рассматривается возможность реализации в принципе.

Примеры реализации

Вот пример и описание простого использования сервиса
А это прототип видеочата.
Здесь есть и вариант реализации для Андроид.

Комментарии (10)


  1. Moxa
    09.12.2017 17:40

    А будет ли успешно работать сервис при, допустим, 10000 соединений? Есть у кого-нибудь такой опыт?

    я поднимал на домашнем компе 1.6кк коннектов, работало, но cpu был занят обработкой сети, на 10к проблем быть не должно, но зависит от имплементации сервера


    1. WitNt Автор
      09.12.2017 21:14

      Я не понял, коллега, 1.6кк — это сколько? 16000 соединений? Или больше полутора миллионов? Это не праздный вопрос. Мне просто интересно, как по нагрузке будет вести себя сервис? Будет ли он справляться? Откровенно говоря, я не делал таких жёстких испытаний.


      1. Psychosynthesis
        09.12.2017 22:23

        Обычно под kk подразумевают миллионы.


        1. Moxa
          11.12.2017 15:51

          да, мне было скучно, я поднял 40 виртуальных машин для клиентских приложений и одну виртуалку для сервера, 40 клиентов по 20к коннектов = 800к клиентских коннектов, и на сервере — 800к серверных, оперативы сожрало примерно 12 гигов, но все уперлось в старенький амд.
          На таком количестве коннектов проц только занимался обработкой эвентов на соединениях и все было довольно неспешно, на 500к клиентов все работало прекрасно и проц не напрягался


  1. Hokum
    09.12.2017 19:09

    А почему между серверами не сделать взаимодействие через обычный сокет, это разве не будет эффективнее и проще — меньше данных будет передаваться или я ошибаюсь?


    А длина токена именно 256 байт у гугл, не бит (я не знаю, просто удивила такая длина)?


    1. Moxa
      09.12.2017 20:35

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


      1. Hokum
        09.12.2017 21:01

        не знал, спасибо.


    1. WitNt Автор
      09.12.2017 21:09

      256 это я конечно хватил. А вот длина 120 символов — это пожалуйста. Вот, например, что мне сегодня пришло от сервиса GCM: APA91bFCblOevBb33Y8yaLEpHvqx46lYXVrF1XlisUKcY7nevT09RrsZj-cBM9UbHUtsixEuTi-r1bkgDJdDK-HielBJ_5MxuaN786D6KyRmiDa85ARQZVw


  1. UncleAndy
    10.12.2017 17:33

    Сейчас делаю аналогичный сервер на golang. Гоняю его тестирование на 10000 одновременных коннектах (соединение/отправка данных/получение данных/дисконнект). Расход памяти в районе 2 Мб. По процу — до 200% в пике.

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


  1. 3draven
    10.12.2017 21:54

    Берется jms в качестве бека или любой брокер сообщений, вебсокеты или что угодно в качестве средства коннекта и все. Роутинг настраивается средствами брокера. Зачем велик то изобретать?

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

    Хотя по началу я тоже велик и тоже на вебсокетах пилил… но быстро опомнился, на третий день)