WebSockets with OpenResty

Lua WebSocket Implementation Installation
This blog post is updated for OpenResty 1.4.2.9.

本文适用于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)

编辑:糖果
作者:Aapo Talvensaari