Front-end JavaScript

EPISODE I

HighLoad

Шкарбатов Дмитрий

Руковожу командой web-разработки

в ПриватБанке, Pentester,

MD в области защиты информации

shkarbatov@gmail.com

https://www.linkedin.com/in/shkarbatov

Давайте вспомним Ajax
и подумаем
зачем нам WebSocket 

WebSocket (RFC 6455) — протокол полнодуплексной связи (может передавать и принимать одновременно) поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени.

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

WebSocket

Протокол WebSocket работает над HTTP.

Это означает, что при соединении браузер отправляет специальные заголовки, спрашивая: «поддерживает ли сервер WebSocket?».

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

Установление WebSocket соединений

Соединение WebSocket можно открывать как WS:// или как WSS://. Протокол WSS представляет собой WebSocket над HTTPS.

Кроме большей безопасности, у WSS есть важное преимущество перед обычным WS – большая вероятность соединения.

Дело в том, что HTTPS шифрует трафик от клиента к серверу, а HTTP – нет.

Если между клиентом и сервером есть прокси, то в случае с HTTP все WebSocket-заголовки и данные передаются через него. Прокси имеет к ним доступ, ведь они никак не шифруются, и может расценить происходящее как нарушение протокола HTTP, обрезать заголовки или оборвать передачу. А в случае с WSS весь трафик сразу кодируется и через прокси проходит уже в закодированном виде. Поэтому заголовки гарантированно пройдут.

WSS

В протокол встроена проверка связи при помощи управляющих фреймов типа PING и PONG.

Тот, кто хочет проверить соединение, отправляет фрейм PING с произвольным телом. Его получатель должен в разумное время ответить фреймом PONG с тем же телом.

Этот функционал встроен в браузерную реализацию, так что браузер ответит на PING сервера, но управлять им из JavaScript нельзя.

Иначе говоря, сервер всегда знает, жив ли посетитель или у него проблема с сетью.

PING/PONG

Can I use

Как с ним работать?

var ws = new WebSocket('ws://curex.ll:8880');
// У объекта socket есть четыре коллбэка:
// один при получении данных и три – при изменениях в состоянии соединения:

ws.onopen = function() {
    console.log("Соединение установлено.");
};

// On message receive
ws.onmessage = function (event) {
    console.log("Получены данные " + event.data);
};

// On error connection
ws.onerror = function (error) {
    console.log("Ошибка " + error.message);
};

// On close connection
ws.onclose = function (event) {
    if (event.wasClean) {
        console.log('Соединение закрыто чисто');

    } else {
        console.log('Обрыв соединения'); // например, "убит" процесс сервера
    }
    console.log('Код: ' + event.code + ' причина: ' + event.reason);
};

$(document).ready(function () {
    ws.send('121212');
});

Нагрузка

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

Тестирование

  • Нагрузка сервера с помощью Apache Jmeter (нужен дополнительный плагин) автоматизированная работа

 

  • Фронт, с привлечением разработчика ручная работа

Каково решение?

Серебрянной пули - нет!

Отдельный контекст для выполнения фоновых задач, который не блокирует UI. Обычно worker создаётся в виде отдельного скрипта, ресурсы worker-а живут в процессе создавшей его страницы.


В worker-е есть:

  • navigator
  • location
  • applicationCache
  • XHR, websocket
  • importScripts для синхронной загрузки скриптов

Worker

Нагрузка

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

То же самое, что и Worker, но может быть использован с нескольких страниц.

Shared Worker

DOM

В worker-е нельзя использовать DOM, вместо window глобальный объект называется self. Нельзя получить доступ к localStorage и рисовать на canvas.
 

Доступ к объектам

Из worker-ов нельзя вернуть объект. В javascript нет lock-ов и других возможностей потокобезопасности, поэтому из worker-ов нельзя передавать объекты по ссылке, всё отправленное в worker или из него будет скопировано.

Ограничения

CORS

Пока что worker-ы не поддерживают совместное использование ресурсов между разными источниками, создать worker можно только загрузив его со своего домена.

 

Размер стека

Для worker-ов выделяется меньший размер стека, иногда это имеет значение.

Ограничения

  • когда он закроется сам, вызвав self.close()
  • когда закроются все странички, его использующие (при этом у worker-а не будет возможности закончить вычисления)
  • когда пользователь принудительно завершит его (например, в хроме из chrome://inspect)
  • когда упадёт или он, или процесс странички, где он живёт

Завершение работы

Can I use

Debug


chrome://inspect/#workers

Использование


web_worker = new SharedWorker('shared_worker.js');

web_worker.port.addEventListener('message', function(e) {

     // On message receive
     if (e.data.operation === 'on_message') {
        alert('SocketOnMessage');

    // On error connection
    } else if (e.data.operation === 'on_error') {
        alert('SocketOnError');

    // On close connection, trying to reconnect in 3 sec
    } else if (e.data.operation === 'on_close') {
        alert('SocketOnClose');
    }
}, false);

// Shared Worker On Error
web_worker.onerror = function(err){
    alert('WebWorkerError ' + err.message);
    web_worker.port.close();
};

// Init SharedWorker
web_worker.port.start();

// Start command Shared Worker
web_worker.port.postMessage({'cmd': 'start', 'url': 'ws://ws.privatbank.ua'});

shared_worker.js

var port;
var ws = null;
var peers = [];

self.addEventListener('connect', function(e) {

port = e.ports[0];
peers.push(port);

port.addEventListener('message', function(e) {

    // Start
    if (e.data.cmd === 'start') {

        if (ws === null) {
            // Socket init
            ws = new WebSocket(e.data.url);
        }

        // On message receive
        ws.onmessage = function (msg) {
            send({
                'operation': 'on_message',
                'data': JSON.parse(msg.data)
            });
        };
        // On error connection
        ws.onerror = function () {
            ws = null;
            send({'operation': 'on_error'});
        };

        // On close connection
        ws.onclose = function (event) {
            ws = null;
            send({'operation': 'on_close',
                'data': event.code});
        };

    // Send Message
    } else if (e.data.cmd === 'send') {
        if (ws !== null && ws.readyState === 1)
            ws.send(e.data.data);
    }

    // Отправляем данные клиенту
    function send (data) {
        peers.forEach(function (port) {
            port.postMessage(data);
        });
    }
}, false);

port.start();

}, false);

Исходный код примера выше


    https://github.com/Shkarbatov/WebSocketInSharedWorkerJS

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

function getData(ldap, branch, bank) {

    // Если вкладка не активная, не делаем на нее запросы
    if (document.hidden || document.msHidden || document.webkitHidden || document.mozHidden) {

        timerId = setTimeout(function () {
            getData(ldap, branch, bank)
        }, GetDataForCashier.settings.delay_request);

    } else {
        timerId = setTimeout(function () {
            web_worker.port.postMessage({
                'cmd': 'send',
                'data': JSON.stringify({data})
            });

            getData(ldap, branch, bank)

        }, GetDataForCashier.settings.delay_request);
    }
}

SharedWorker

Service workers

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

Service workers

Service workers запускаются только поверх HTTPS из соображений безопасности.

Основное его назначение - это кеширование запросов.

Can I use было на 12.2016

Can I use стало на 08.2018

Debug


chrome://inspect/#service-workers
chrome://serviceworker-internals

function getData(send_data, need_repeat) {
    var msg = new MessageChannel();
    msg.port1.onmessage = function(event){
        //Response received from SW
        console.log('Receive response' +
            ' from server: ' + event.data);
        $('[name=data]').html(event.data);
    };

    navigator
        .serviceWorker
        .controller
        .postMessage(send_data, [msg.port2]);

    if (need_repeat) {
        getData(send_data, need_repeat);
    }
}

// Инитим и запускаем опрос
function run () {
    // Send message to SW
    // Start command Shared Worker
    getData({'cmd': 'start'}, false);

    // Запускаем опрос
    getData('569908768654', true);
}

Использование

Использование


if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
        navigator.serviceWorker
            .register('service_worker.js?version=2', {scope: './'})
            .then(function (reg) {
                reg.onupdatefound = function () {
                    var installingWorker = reg.installing;

                    installingWorker.onstatechange = function () {

                        switch (installingWorker.state) {
                            case 'activated':
                                if (navigator.serviceWorker.controller) {
                                    console.log('New or updated content is available.');
                                    run();

                                } else {
                                    console.log('Content is now available offline!');
                                }
                                break;

                            case 'redundant':
                                console.error('The installing became redundant.');
                                break;
                        }
                    };
                };
            });
    });
}

service_worker.js

var port;
var ws = null;
var peers = [];

self.addEventListener('install', (event) => {
    console.log('Установлен');
});

self.addEventListener('activate', (event) => {
    // Activate. Become available to all pages
    event.waitUntil(self.clients.claim());
});

self.addEventListener('fetch', (event) => {
    // console.log('Происходит запрос на сервер');
});

self.addEventListener('message', function (evt) {
if (evt.data.cmd === 'start') {

    peers.push(evt.ports[0]);

    if (ws === null) {
        // Socket init
        ws = new WebSocket('ws://site.ll:8880');
    }

    ws.onopen = function () {
        return true;
    };
// On message receive
ws.onmessage = function (event) {
    console.log("Receive " + event.data);
    send_data(event.data);
};

// On error connection
ws.onerror = function (error) {
    // console.log("Error " + error.message);
};

// On close connection
ws.onclose = function (event) {
    console.log(event.code +' '+ event.reason);
};

// Отправляем данные клиенту
function send_data(data) {
    peers.forEach(function (port) {
        port.postMessage(data);
    });
}

} else {
    // Инитим объект и запускаем опрос
    if (ws !== null && ws.readyState === 1)
        ws.send(evt.data);
}
});

Исходный код примера выше


    https://github.com/Shkarbatov/WebSocketInServiceWorkerJS

Стоит обратить внимание

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

Удаление воркера произойдет автоматически, по истечении определенного времени.

Я слышал еще
что-то, типа

Push API

Push API

Push API

Push API

  • Далеко не все браузеры поддерживают;
  • Клиент может отказаться принимать сообщения;
  • Работает только с https;
  • Нужна регистрация в Firebase Cloud Messaging для GoogleChrome;
  • Запросы проходят через внешний сервис.

Push API

  • https://habr.com/post/321924/
  • https://github.com/web-push-libs/web-push-php
  • https://github.com/eveness/web-push-api

Выводы

- Нет серебрянной пули

- Нет 100% поддержки браузерами

- Очень мало внятной информации

 

+ В нашем случае снижение нагрузки на 50%

+ Технология проста в освоении

+ Гибкая в использовании

   Service Worker -> Shared Worker ->

         Worker -> WebSocket -> AJAX

Вопросы?

Империя наносит
ответный удар

Удачи!

WebSockets 2018 episode 1 front-end javascript

By James Jason

WebSockets 2018 episode 1 front-end javascript

  • 902