Articles

Erlang Websocket Server using Cowboy


NOTE: This is the translation of the original article I wrote in spanish for the wiki of Erlang Argentina.

Introduction

Out of curiosity, and because some of my own projects needed it, I decided to go ahead and try writing a websocket server in erlang. After evaluating some of the available options (so I dont have to implement the whole websocket RFC -and their different drafts/versions- myself), I chose the Cowboy framework. This article tries to show my conclusions after the experience.

Note that I dont have that much experience using Cowboy, so I'd really like to know whatever comment you may think of that might make this article (or the code) any better. By the way, you can find the code for this article at GitHub.

So, why Cowboy?

Well, it was the "last man standing" at the end of the tests. Up to now (05/2012), the available alternatives for writing a websocket server in erlang are:

  • Socket.io-Erlang: This was the most tempting option to me, because it would've been great to use socket.io on the client side. Socket.io is really popular (specially in NodeJS), and also because one of the authors (Frederic Trottier-Hebert) is also the author of LearnYouSomeErlang. Unfortunately, the authors decided to not add any more features beyond those provided by socket.io 0.6.x (socket.io is now in the 0.9.x version) due to not being ok with internal changes to the socket.io code. So this pretty much killed this one.
  • MochiWeb: This one is incredible popular, and allows you to quickly create HTTP servers and would've been a great opportunity to use it, but it does not support websockets natively.
  • Yaws: It's a complete HTTP server, designed as a robust and scalable solution to server dynamic and static content, and also supports websockets. It seemed to me this was kind of an overkill for my particular situation, but I would have definitely used it if all the other options failed.
  • Misultin: It's a library used to handle HTTP requests in an easy way. I didn't use this one because the author himself says (in the project README file), that he stopped further development because of the increased interest/attention and development on mochiweb and cowboy.
  • Cowboy: It has quite a different (and interesting) approach, because handling HTTP requests is just a part of the framework. Cowboy is actually more like a generic pool of connection accept()ors, that we can configure to handle normal TCP or SSL connections as transports, and handle protocols above them (like HTTP or any other we might like to). Out of the box, it supports HTTP, but it is essentially generic and extendable through behaviors (for example to support other protocols). A big drawback, though: documentation is scarce :( So you will need to browse through the source code to find the answers to your questions.

The code

So to start trying Cowboy, I decided to create a rebar application, which I called "erws" (Erlang WebSocket, so much for my creative talent).

The main module in this application is erws_handler.erl, but I'll show all the necessary files to have the complete application.

When the application starts, it makes cowboy listens at port 10100 and dispatch connections to erws_handler as HTTP requests (you will see how this is done in erws_app).

erws_handler implements two behaviors (cowboy_http_handler and cowboy_http_websocket_handler). The first one is used to respond to common HTTP GET requests and start the websocket handshake on it, by sending the necessary headers to make the "upgrade" of the HTTP connection (as the protocol states). For us this is really quite easy, we just need to return a specific tuple in erws_handler:init/1 (see the code). The dirty work is actually done by cowboy through the module cowboy_http_websocket.

Once this is done, and through the api that the cowboy_http_websocket_handler behavior imposes, we will handle the incoming data as messages, very much like a common gen_server.

erws_handler.erl

The main module, will handle and serve the requests.

-module(erws_handler).
-behaviour(cowboy_http_handler).
-behaviour(cowboy_http_websocket_handler).

% Behaviour cowboy_http_handler
-export([init/3, handle/2, terminate/2]).

% Behaviour cowboy_http_websocket_handler
-export([
    websocket_init/3, websocket_handle/3,
    websocket_info/3, websocket_terminate/3
]).

% Called to know how to dispatch a new connection.
init({tcp, http}, Req, _Opts) ->
    lager:debug("Request: ~p", [Req]),
    % "upgrade" every request to websocket,
    % we're not interested in serving any other content.
    {upgrade, protocol, cowboy_http_websocket}.

% Should never get here.
handle(Req, State) ->
    lager:debug("Unexpected request: ~p", [Req]),
    {ok, Req2} = cowboy_http_req:reply(404, [
        {'Content-Type', <<"text/html">>}
    ]),
    {ok, Req2, State}.

terminate(_Req, _State) ->
    ok.

% Called for every new websocket connection.
websocket_init(_Any, Req, []) ->
    lager:debug("New client"),
    Req2 = cowboy_http_req:compact(Req),
    {ok, Req2, undefined, hibernate}.

% Called when a text message arrives.
websocket_handle({text, Msg}, Req, State) ->
    lager:debug("Received: ~p", [Msg]),
    {reply,
        {text, << "Responding to ", Msg/binary >>},
        Req, State, hibernate
    };

% With this callback we can handle other kind of
% messages, like binary.
websocket_handle(_Any, Req, State) ->
    {ok, Req, State}.

% Other messages from the system are handled here.
websocket_info(_Info, Req, State) ->
    {ok, Req, State, hibernate}.

websocket_terminate(_Reason, _Req, _State) ->
    ok.

index.html

This is the HTML page we're going to use as a websocket client. I've tried this in Chrome, but it should work on any browser with HTML5 capabilities. You can find the Websocket API here.

<!DOCTYPE HTML>
<html>
<head>
<script type="text/javascript">
var ws = new Object;

function send()
{
    ws.send("hello world!");
    console.log('Message sent');
}

function open()
{
    if (!("WebSocket" in window)) {
        alert("This browser does not support WebSockets");
        return;
    }
    /* @todo: Change to your own server IP address */
    ws = new WebSocket("ws://1.1.1.1:10100");
    ws.onopen = function() {
        console.log('Connected');
    };
    ws.onmessage = function (evt)
    {
        var received_msg = evt.data;
        console.log("Received: " + received_msg);
        var txt = document.createTextNode("
            Simon says: " + received_msg
        );
        document.getElementById('msgs').appendChild(txt);
    };
    ws.onclose = function()
    {
        console.log('Connection closed');
    };
}
</script>
</head>
<body>
<div id="sse">
   <a href="javascript:open()">Open websocket connection</a><br/>
   <a href="javascript:send()">Send Message</a>
</div>
<div id="msgs">
</div>
</body>
</html>

erws_app.src

Our erws_app.src will be used by rebar to generate the application descriptor.

NOTE: Be sure to add the "crypto" application. I spent quite a few minutes trying to figure out why the websocket connection was failing, and it turned out that I missed to include the crypto app, used by cowboy at the handshake stage of the websocket connection.

{application, erws, [
    {description, ""},
    {vsn, "1"},
    {registered, []},
    {applications, [
        kernel,stdlib,crypto,cowboy,
        compiler,lager,syntax_tools
    ]},
    {mod, { erws_app, []}},
    {env, []}
]}.

rebar.config

This is the project configuration file. Our dependencies are cowboy and lager (optional).

NOTE: We use tag 0.6.1 of cowboy source code because from then on it has some BC break changes.

% The next option is required so we can use lager.
{erl_opts, [{parse_transform, lager_transform}]}.
{lib_dirs,["deps"]}.
% Our dependencies.
{deps, [
    {'cowboy', ".*", {
        git, "git://github.com/extend/cowboy.git", {tag, "0.6.1"}}
    },
    {'lager', ".*", {
        git, "git://github.com/basho/lager.git", "master"}
    }
]}.

erws_app.erl

Our application. This code configures cowboy to listen for and accept connections in the given port (with the given protocol), and then dispatch the requests to the given modules.

-module(erws_app).
-behaviour(application).
-export([start/2, stop/1]).

start(_StartType, _StartArgs) ->
    %% {Host, list({Path, Handler, Opts})}
    %% Dispatch the requests (whatever the host is) to
    %% erws_handler, without any additional options.
    Dispatch = [{'_', [
        {'_', erws_handler, []}
    ]}],
    %% Name, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts
    %% Listen in 10100/tcp for http connections.
    cowboy:start_listener(erws_websocket, 100,
        cowboy_tcp_transport, [{port, 10100}],
        cowboy_http_protocol, [{dispatch, Dispatch}]
    ),
    erws_sup:start_link().

stop(_State) ->
    ok.

erws_sup.erl

This is our top supervisor. In this case, it does absolutely nothing (pretty much like a human supervisor :D).

-module(erws_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    {ok, { {one_for_one, 5, 10}, []} }.

Conclusion

Although I'm not a big fan of dependencies (and/or using whatever frameworks are out there), Cowboy does a great job, and I'm sure that it will be even a better choice for more complex applications, where other kind of content could be served by HTTP(S) besides the websocket content.

Overall, Cowboy lets you focus on the real deal (the features you need) without having to worry about more mundane tasks, like coding a pool of workers to handle connections, or (like in this particular case), the implementation details for the websocket protocol. With just a few lines of code, we can really put some magic into those dull webapps :)