Writing WebSockets server with few lines of Lua on nginx/OpenResty
Photo: Marc-Olivier Jodoin

Writing WebSockets server with few lines of Lua on nginx/OpenResty

In case if you've been hunting for a super-fast way of doing WebSockets on your OpenResty or nginx with Lua module, there is a way to do it directly as a location handler. In most cases, with WebSockets, it is all just about getting data from users and sending something back, but, besides that, we should have it to easily inject PUB/SUBSCRIBE services, like NATS driven ones or custom made ones.

location /ws{
    client_max_body_size 32k;

    lua_socket_log_errors off;
    lua_check_client_abort on;

    content_by_lua_block {
        local server = require('websockets.server')

        -- server got data
        server.on.message = function(data, send)
            print(json.encode(data))
            send('{"type": "debug", "data": "---"}')
        end

        server.run()
 
    }
}

As you can see from the code above, there are just a few lines that can do the whole job for you. You add 'on.message' handler that receives data and send() function that you can use to send your data back to the user.

And the code for the /websockets/server.lua is the following:

---
--- Websockets Server for NGINX/OpenResty
--- Created by skitsanos.
---
local server = require "resty.websocket.server"

local response = {
    info = function()
        return json.encode({
            type = 'info',
            data = {
                version = '1.0.0',
                id = ngx.var.request_id
            }
        })
    end,

    message = function(data)
        return json.encode({
            type = 'message',
            data ?= data
        })
    end
}

local m = {
    version = '1.0.0',

    debug = true,

    json = true,

    send = nil,

    on = {
        message = nil
    }
}

function m.log(...)
    if (m.debug) then
        ngx.log(...)
    end
end

function m.run()
    local wb, err = server:new {
        max_payload_len = 32768,
        timeout = 5000
    }

    if not wb then
        m.log(ngx.ERR, "failed to new websocket: ", err)
        return ngx.exit(444)
    end

    wb:send_text(response.info())

    m.send = function(data)
        wb:send_text(data)
    end

    while true do
        -- try to receive
        local data, typ, errReceive = wb:recv_frame()

        -- check socked timeout
        if not data then
            if not string.find(errReceive, "timeout", 1, true) then
                m.log(ngx.ERR, "failed to receive a frame: ", errReceive)
                return ngx.exit(444)
            end
        end

        local token = ngx.var.arg_token;
        if (token == nil) then
            token = ngx.var.request_id
        end

        if (typ ~= nil) then
            m.log(ngx.INFO, 'received a frame of type ', typ, ' and payload ', data)
        end

        local switch = {
            ['close'] = function()
                -- for typ "close", err contains the status code
                local code = errReceive

                -- send a close frame back:
                local bytes, errSendClose = wb:send_close(1000)
                if (not bytes) then
                    m.log(ngx.ERR, 'failed to send the close frame: ', errSendClose)
                end

                m.log(ngx.INFO, 'closing with status code ', code, ' and message ', data)
                return 1, errSendClose
            end,

            ['ping'] = function()
                local bytes, SendPong = wb:send_pong()
                if (not bytes) then
                    ngx.log(ngx.ERR, "failed to send frame: ", SendPong)
                    return 1, SendPong
                end
            end,

            ['text'] = function()
                if (not m.json) then
                    if (m.on.message ~= nil) then
                        m.on.message(data, m.send)
                    end

                    return 0, nil
                end

                -- decode payload
                local ok, payload = pcall(json.decode, data)
                if (ok ~= true) then
                    wb:send_close(1003, 'Incorrect payload format. Must be valid JSON')
                    return 1, ok
                end

                if (m.on.message ~= nil) then
                    m.on.message(payload, m.send)
                end

                return 0, nil
            end
        }

        if (typ == 'close') then
            break
        end

        if (typ ~= nil) then
            -- check for return code
            local _, errRet = switch[typ]();
            if (errRet) then
                break
            end
        end
    end

    wb:send_close()
end


return m

And now the client-side, another bit of code, this time with JavaScript:

const intervalReconnect = 3000;

let socket;

const reconnectWebSocket = () =>
{
    socket = new WebSocket(`ws://localhost/wsrelay`);

    socket.onopen = () =>
    {
        console.log('connected');
    };

    socket.onclose = () =>
    {
        setTimeout(() =>
        {
            reconnectWebSocket();
        }, intervalReconnect);
    };

    socket.onerror = e =>
    {
        if (e.currentTarget.readyState === 3)
        {
            console.log('-- connection closed');
        }
    };

    socket.onmessage = e =>
    {
        const {type, data} = JSON.parse(e.data);
        switch (type)
        {
            case 'info':
                console.log(data);
                socket.send(JSON.stringify({type: 'subscribe', subject: 'app.*'}));
                break;

            default:
                console.log(data);
                break;
        }
    };
};


reconnectWebSocket();

This is just a basic example of reconnecting a WebSocket client. In case if the connection dropped, it will reconnect in 3 seconds. You can extend your 'onmessage' handler in any possible way.

Hope it helps anyone.

Félix Oberson

DevOps web, je donne vie aux idées numériques.

2 年

Thanks for sharing ! I am discovering OpenResty and this websocket feature is appealing.

要查看或添加评论,请登录

Evgenios Skitsanos的更多文章

社区洞察

其他会员也浏览了