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
- 988