WebSockets with OpenResty

Lua WebSocket Implementation Installation

This blog post is updated for OpenResty 1.4.2.9.

I have been following OpenResty development closely for a while now, but I did never got an inspiration to really try it out, until now. Yichun Zhang (@agentzh) of OpenResty-fame announced that he just released a preliminary WebSockets support for Lua Nginx module (lua-nginx-module). I have been waiting for this to happen.

I managed to install, and test this on my Mac. Here is how I did it:

$ brew install pcre
$ wget http://openresty.org/download/ngx_openresty-1.4.2.9.tar.gz
$ tar zxf ngx_openresty-1.4.2.9.tar.gz
$ cd ngx_openresty-1.4.2.9
$ ./configure \
--with-luajit \
--with-cc-opt="-I/usr/local/Cellar/pcre/8.33/include" \
--with-ld-opt="-L/usr/local/Cellar/pcre/8.33/lib"
$ make
$ make install

Now we should have OpenResty installed with Lua module that supports WebSockets at /usr/local/openresty.


Next we need to write the WebSockets server code (right now just a stupid echoing server). Again, edit nginx.conf, and add a new location after “location / { … }”:

location /s {
lua_socket_log_errors off;
lua_check_client_abort on;
content_by_lua '
local server = require "resty.websocket.server"
local wb, err = server:new{
timeout = 5000,
max_payload_len = 65535
}
if not wb then
ngx.log(ngx.ERR, "failed to new websocket: ", err)
return ngx.exit(444)
end
while true do
local data, typ, err = wb:recv_frame()
if wb.fatal then
ngx.log(ngx.ERR, "failed to receive frame: ", err)
return ngx.exit(444)
end
if not data then
local bytes, err = wb:send_ping()
if not bytes then
ngx.log(ngx.ERR, "failed to send ping: ", err)
return ngx.exit(444)
end
elseif typ == "close" then break
elseif typ == "ping" then
local bytes, err = wb:send_pong()
if not bytes then
ngx.log(ngx.ERR, "failed to send pong: ", err)
return ngx.exit(444)
end
elseif typ == "pong" then
ngx.log(ngx.INFO, "client ponged")
elseif typ == "text" then
local bytes, err = wb:send_text(data)
if not bytes then
ngx.log(ngx.ERR, "failed to send text: ", err)
return ngx.exit(444)
end
end
end
wb:send_close()
';
}

Looks great. Now add websockets.html to /usr/local/openresty/nginx/html directory:

<html>
<head>
<script>
var ws = null;
function connect() {
if (ws !== null) return log('already connected');
ws = new WebSocket('ws://127.0.0.1/s/');
ws.onopen = function () {
log('connected');
};
ws.onerror = function (error) {
log(error);
};
ws.onmessage = function (e) {
log('recv: ' + e.data);
};
ws.onclose = function () {
log('disconnected');
ws = null;
};
return false;
}
function disconnect() {
if (ws === null) return log('already disconnected');
ws.close();
return false;
}
function send() {
if (ws === null) return log('please connect first');
var text = document.getElementById('text').value;
document.getElementById('text').value = "";
log('send: ' + text);
ws.send(text);
return false;
}
function log(text) {
var li = document.createElement('li');
li.appendChild(document.createTextNode(text));
document.getElementById('log').appendChild(li);
return false;
}
</script>
</head>
<body>
<form onsubmit="return send();">
<button type="button" onclick="return connect();">
Connect
</button>
<button type="button" onclick="return disconnect();">
Disconnect
</button>
<input id="text" type="text">
<button type="submit">Send</button>
</form>
<ol id="log"></ol>
</body>
</html>

And now start the nginx with:

sudo /usr/local/openresty/nginx/sbin/nginx

Then open a browser that has WebSocket support enabled, and open following url:

http://127.0.0.1/websockets.html
Lua Web Sockets in Action

To guard against half-open TCP connections, it is a good idea to enable TCP keepalive in your Nginx listen configuration directive:

# so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
# for example:
listen 80 so_keepalive=2s:2s:8;

(nginx documentation about so_keepalive)

You could also do this on system level, if you wish:

$ sysctl net.inet.tcp.always_keepalive
net.inet.tcp.always_keepalive: 0
$ sudo sysctl -w net.inet.tcp.always_keepalive=1
net.inet.tcp.always_keepalive: 0 -> 1

(for Linux, see: Using TCP keepalive under Linux)