On December 20th of last year I went on vacation, for the whole 2 weeks. What to do on vacation? That's right - code. Code, which there is no time to do during working hours. The last few years I have had very little code. Hands were miserable. What code is written on vacation? I do not know about you, but I write bicycles. What for? There can be many reasons, but the main one is interesting to me. I love C ++ and Lua. I also love bash and awk. Do not throw stones, this is personal, it happened. I don’t really like JavaScript (although for the last 2 years, if something has been coding something on JS), this is also personal.
The result of boredom on vacation was LAppS - Lua Application Server . This holiday coding lasted for 6 months (of course, after December I didn’t spend a lot of time on the code, this is evident from commits in github). But in the last 2 weeks we managed to grab a lot of time to get something working.
As it is already clear from the name, - LAppS Lua application server. Lua is quite a popular language, a bunch of Nginx + Lua with OpenResty libraries is actively used throughout the world, one Cloudflare is worth. At the time of development, the above-mentioned Lua module for Nginx and Tarantool, luvit.io already existed. But none of them supported WebSockets. LAppS does not support HTTP. But now LAppS exceeds the performance of uWebSockets (at the cost of consumption of large computing resources).
The main idea was to minimize the development cycle of microservices. Lua has a lower entry threshold than JavaScript. I'm more than sure, even middle school students can quite easily start programming in Lua. But here all the available web application development tools for Lua, have not such a minimum threshold of entry.
Therefore, the main emphasis in the development was placed on the minimalist API and the ease of creating applications, as well as the ease of configurability of the application server and services.
Lua applications (services) in LAppS do not block I / O. This is probably the biggest difference from a web server with Lua scripting. LAppS uses 2 configuration files to configure the behavior of the WebSockets server and to deploy applications.
By default, if there are no configuration files, then LAppS will try to launch demo applications running in the distribution.
{ "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 } } }
In the above example, 2 demo services are configured: echo and echo_lapps. The service name is the default path and search for Lua modules of applications. In fact, applications in LAppS are modules following a specific interface.
In addition to the internal parameter, all other service settings are required.
Installation and installation instructions can be found on the project’s wiki page.
You can use the prepared deb package for installation in ubuntu-xenial
Applications are actually Lua modules that should have several predefined methods with predefined behavior:
Note: the onMessage method is required to return a boolean value. If the value is returned false, the connection for which the method was called is broken (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;
Configuration for this service:
"myapp" : { "internal" : false, "request_target" : "/myapp", "instances" : 1, "protocol": "raw" }
The LAppS protocol specification is based on the Google JSON-RPC specification with the following key points:
The specification is available on github.
I will not give the full code of the application, I will give only the implementation of the onMessage method. Details of the demo application can be found in the source code on 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 loads several modules before starting the service: nljson , ws , bcast . details of the specification of the modules can be found on the project wiki.
Briefly:
Working with a module is not much different from working with native Lua tables. Moreover, Lua tables are converted to nljson userdata using simple assignment. However, Lua does not distinguish between objects (key-value) and arrays, so for example, for empty Lua-tables, converting them to nljson presents some obstacle. for example
local object=nljson.decode({})
Will create a JSON-Array named object. Therefore, it is better to use this initialization:
local object=nljson.decode('{}')
This definition will uniquely create a JSON-Object.
Further, this object can be used as a native table:
object["test"]=""; print(object.test) object["map"]={ ["key1"] = "value", ["key2"] = 33 } print(object)
The speed of working with nljson objects differs little from native Lua tables.
It's all simpler than steamed turnip, - the benefit of WebSockets API for browsers is thought out and simple.
Mandatory library cbor.js. Webix is ​​used to display bar-chart.
Do not kick the password in the code text. This is just a demo.
<!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>
The client application is an echo client for the LAppS protocol. The server part of the application broadcasts once a second its time, the schedule is updated on this OON.
Note: If several clients are launched in the browser, then the number of broadcasts will increase, because Broadcast sent from onMessage, once a second.
To fix this, you need to implement standalone applications that communicate with the rest of the LAppS stack. This part is now in development.
Source: https://habr.com/ru/post/354882/
All Articles