- PVSM.RU - https://www.pvsm.ru -
20-го декабря прошлого года я ушёл в отпуск, на целых 2 недели. Чем заняться в отпуске? Правильно, — кодом. Кодом, которым некогда заниматься в рабочее время. Последние несколько лет мне кодить приходилось очень мало. Руки стосковались. Какой код пишут в отпуске? Не знаю как вы, а я пишу велосипеды. Зачем? Причин может быть много, но основная, — мне интересно. Я люблю C++ и Lua. Я ещё и bash и awk люблю. Не закидывайте камнями, это личное, так получилось. JavaScript я не очень люблю (хотя последние 2 года если что-то и кодил то на JS), и это тоже личное.
Результатом скуки в отпуске стал LAppS — Lua Application Server [1]. Этот отпускной кодинг растянулся на 6 месяцев (конечно после декабря я коду уделял не очень много времени, это видно по коммитам в github). Но в последние 2 недели удалось урвать достаточно много времени для того, чтобы получилось что-то рабочее.
Как уже ясно из названия, — LAppS сервер приложений Lua. Lua достаточно популярный язык, связка Nginx+Lua с библиотеками OpenResty активно используется по всму миру, один Cloudflare чего стоит. На момент начала разработки, уже существовали и вышеупомянутый Lua модуль для Nginx и Tarantool, luvit.io. Но ни один из них не поддерживал WebSockets. LAppS не поддерживает HTTP. Зато уже сейчас LAppS превосходит в производительности uWebSockets (ценой потребления больших вычислительных ресурсов).
Основная идея была в том, чтобы максимально сократить цикл разработки микросервисов. Lua имеет порог вхождения ниже чем JavaScript. Я больше чем уверен, даже школьники средних классов, вполне спокойно могут начать программировать на Lua. Но вот все доступные средства разработки web приложений для Lua, имеют уже не такой минимальный порог вхождения.
Поэтому основной упор в разработке делался на минималистичность API и простоту создания приложений, а также лёгкость конфигурируемости сервера приложений и сервисов.
Lua-приложения (сервисы) в LAppS, не блокируют ввод-вывод. Это наверное самое большое отличие от web сервера со скриптингом на Lua. LAppS использует 2 конфигурационных файла, для настройки поведения сервера WebSockets и для деплоймента приложений.
По умолчанию, если отсутствуют конфигурационные файлы, то LAppS попытается запустить демо приложения идущие в дистрибутиве.
{
"listeners" : 1,
"connection_weight": 0.7,
"ip" : "0.0.0.0",
"port" : 5083,
"workers": {
"workers" : 3,
"max_connections" : 1000
},
"tls" : true,
"tls_certificates" : {
"ca" : "/opt/lapps/etc/ssl/cert.pem",
"cert" : "/opt/lapps/conf/ssl/cert.pem",
"key" : "/opt/lapps/conf/ssl/key.pem"
},
"auto_fragment" : true,
"max_inbound_message_size" : 300000
}
{
"directories" : {
"applications" : "apps",
"app_conf_dir" : "etc",
"tmp": "tmp",
"workdir": "workdir"
},
"services" : {
"echo" : {
"internal" : false,
"request_target" : "/echo",
"protocol" : "raw",
"instances" : 3
},
"echo_lapps" : {
"internal" : false,
"request_target" : "/echo_lapps",
"protocol" : "LAppS",
"instances" : 3
}
}
}
В выше приведённом примере сконфигурировано 2 демо-сервиса: echo и echo_lapps. Имя сервиса, это по умолчанию и путь поиска Lua-модулей приложений. По сути приложения в LAppS, это модули следующие определённому интерфейсу.
Кроме параметра internal, все остальные параметры настройки сервиса обязательны.
Инструкции по сборке и установке можно прочитать на wiki странице проекта [2]
Можно воспользоваться подготовленным deb [3] пакетом для установки в ubuntu-xenial
Приложения на самом деле являются Lua-модулями, которые должны иметь несколько предопределённых методов с предопределённым поведением:
Примечание: метод onMessage обязан возвращать булево значение. При возврате значения ложно, соединение для которого был вызван метод разрывается (close code 1000).
myapp = {}
myapp.__index = myapp;
myapp["onStart"]=function()
-- do something on start
end
myapp["onDisconnect"]=function(handler)
-- handler - is a unique client identifier
-- react on client disconnect
end
myapp["onShutdown"]=function()
-- do something on shutdown
end
myapp["onMessage"]=function(handler,opcode, message)
-- it is an echo, - we return back the same message
local result, errmsg=ws:send(handler,opcode,message);
if(not result)
then
print("myapp::OnMessage(): "..(errmsg or "none"));
end
return result;
end
return myapp;
Конфигурация для данного сервиса:
"myapp" : {
"internal" : false,
"request_target" : "/myapp",
"instances" : 1,
"protocol": "raw"
}
Спецификация протокола LAppS базируется на гугловской спецификации JSON-RPC со следующими ключевыми отпличиями:
Со спецификацией можно ознакомиться на github [4]
Не буду приводить полный код приложения, приведу лишь имплементацию метода onMessage. Детально с демо приложением можно ознакомиться в исходниках на github.
echo_lapps["onMessage"]=function(handler,msg_type, message)
-- функция для реакции на тип сообщения
local switch={
[1] = function() -- Клиентские нотификации без параметров не принимаются сервером
-- (это не ограничение это деталь реализации приложения)
-- сообщение об ошибке
local err_msg=nljson.decode([[{
"status" : 0,
"error" : {
"code" : -32600,
"message": "This server does not accept Client Notifications without params"
},
"cid" : 0
}]]);
-- отправка сообщения об ошибке
ws:send(handler,err_msg);
-- закрываем WebSocket с кодом 1003 - "не понимаю"
ws:close(handler,1003);
end,
[2] = function() -- CN с параметрами. обрабатываем.
local method=methods._cn_w_params_method[message.method] or echo_lapps.method_not_found;
method(handler,message.params);
end,
[3] = function() -- не поддерживаем запросы без параметров
local method=echo_lapps.method_not_found;
method(handler);
end,
[4] = function() -- поддерживаем запросы с параметрами
local method=methods._request_w_params_method[message.method] or echo_lapps.method_not_found;
method(handler,message.params);
end
}
-- выполняем селектор
switch[msg_type]();
return true;
end
LAppS подгружает несколько модулей, перед стартом сервиса: nljson, ws, bcast. детально со спецификацией модулей можно ознакомиться на wiki проекта.
Кратко:
Работа с модулем, мало чем отличается от работы с нативными таблицами Lua. Более того, таблицы Lua конвертируются в nljson userdata с помощью простого присваивания. Однако Lua не делает различия между объектами (ключ-значение) и массивами, поэтому например для пустых Lua-таблиц их конвертирование nljson представляет некое препятствие. Например
local object=nljson.decode({})
Приведёт к созданию JSON-Array с именем object. Поэтому лучше пользоваться такой инициализацией:
local object=nljson.decode('{}')
Это определение однозначно создаст JSON-Object.
Далее этим объектом можно пользоваться как нативной луа таблицей:
object["test"]="значение";
print(object.test)
object["map"]={
["key1"] = "value",
["key2"] = 33
}
print(object)
Скорость работы с nljson объектами мало отличается от нативных таблиц Lua.
Тут всё проще пареной репы, — благо WebSockets API для браузеров продуман и прост.
Обязательная библиотека cbor.js. Webix используется для отображения bar-chart.
Не пинайте за пароль в тексте кода. Это-же просто демо.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<link rel="stylesheet" href="http://cdn.webix.com/edge/webix.css" type="text/css">
<script src="http://cdn.webix.com/edge/webix.js" type="text/javascript"></script>
<script src="cbor.js" type="text/javascript"></script>
<div id="chart" style="width:100%;height:300px;margin:3px"></div>
<div id="stime" style="width:100%;height:300px;margin:3px"></div>
<script>
// globals
window["secs_since_start"]=0;
window["roundtrips"]=0;
window["subscribed"]=false;
window["lapps"]={
authkey : 0
};
// initial data set for the chart
var dataset = [
{ id:1, rps:0, second:0 }
]
// the chart
webix.ui({
id:"barChart",
container:"chart",
view:"chart",
type:"bar",
value:"#rps#",
label:"#rps#",
radius:0,
gradient:"rising",
barWidth:40,
tooltip:{
template:"#rps#"
},
xAxis:{
title:"Ticking RPS",
template:"#second#",
lines: false
},
padding:{
left:10,
right:10,
top:50
},
data: dataset
});
// might be a dialog instead. never do this in production.
var login = {
lapps : 1,
method: "login",
params: [
{
user : "admin",
password : "admin"
}
]
};
// echo request
var echo= {
lapps : 1,
method: "echo",
params: [
{ authkey : 0 },
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]
]
};
// create a websocket
var websocket = new WebSocket("wss://127.0.0.1:5083/echo_lapps");
websocket.binaryType = "arraybuffer";
// on response
websocket.onmessage = function(event)
{
window.roundtrips=window.roundtrips+1;
// CBOR to native JavaScript object
var message = CBOR.decode(event.data);
// Verifying the channel
if(message.cid === 0)
{
if(message.status === 1)
{
if(window.lapps.authkey === 0)
{
if(typeof message.result[0].authkey !== "undefined") // authkey is arrived
{
window.lapps.authkey=message.result[0].authkey;
echo.params[0].authkey = window.lapps.authkey;
websocket.send(CBOR.encode(echo));
}
else
{
console.log("No authkey: "+JSON.stringify(message));
}
}
else
{
websocket.send(CBOR.encode(echo));
// already authenticaed, may subscribe to OONs
if(!window.subscribed)
{
var subscribe={
lapps : 1,
method: "subscribe",
params: [
{ authkey: window.lapps.authkey }
],
cid: 5
};
websocket.send(CBOR.encode(subscribe));
window.subscribed=true;
}
}
}
else
{
console.log("ERROR: "+JSON.stringify(message));
}
}
else if(message.cid === 5) // server time OON
{
console.log("OON is received");
webix.message({
text : message.message[0],
type: "info",
expire: 999
});
window.secs_since_start++;
$$("barChart").add({rps: window.roundtrips, second: window.secs_since_start});
window.roundtrips=0;
if(window.secs_since_start > 30 )
{
$$("barChart").remove($$("barChart").getFirstId());
}
}
else // other OONs are just printed to console
{
console.log("OON: "+JSON.stringify(message));
}
};
// login on connection
websocket.onopen=function()
{
console.log('is open');
window.teststart=Date.now()/1000;
websocket.send(CBOR.encode(login));
}
// close connection if peer sent close frame
websocket.onclose=function()
{
console.log("is closed");
}
</script>
</body>
</html>
Клиентское приложение это echo-клиент для протокола LAppS. Серверная часть приложения броадкастит раз в секунду своё время, график обновляется по этому OON.
Примечание: Если в браузере запустить несколько клиентов, то и кол-во броадкастов увеличится, т.к. броадкасты отправляются из onMessage, раз в секунду.
Что-бы это исправить, необходима реализация самостоятельных приложений, коммуницирующих с остальным стэком LAppS. Эта часть сейчас в разработке.
Автор: thatsme
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/279482
Ссылки в тексте:
[1] LAppS — Lua Application Server: https://github.com/ITpC/LAppS
[2] wiki странице проекта: https://github.com/ITpC/LAppS/wiki
[3] deb: https://github.com/ITpC/LAppS/raw/master/packages/lapps-0.5.1-amd64.deb
[4] github: https://github.com/ITpC/LAppS/blob/master/LAppS_Protocol_Specification.md
[5] Источник: https://habr.com/post/354882/?utm_campaign=354882
Нажмите здесь для печати.