WebSockets in Yaws

WebSockets! The new kid in town! Joe loves it, maybe you should too?

WebSockets allow for *real* two-way communication between the browser and Yaws without the overhead and latency that come with polling/long-polling solutions. That should be enough for an introduction. Now... how to use it?

A simple example

First of all, here is a simple example. It shows how to upgrade connections from HTTP to WebSocket.


out(A) ->
    %% To use the extended version of the basic echo callback, add
    %% 'extversion=true' in the query string.
    CallbackMod = case queryvar(A, "extversion") of
                      {ok, "true"} -> basic_echo_callback_extended;
                      _            -> basic_echo_callback
                  end,

    %% To enable keepalive timer add 'keepalive=true' in the query string.
    KeepAlive = case queryvar(A, "keepalive") of
                    {ok, "true"} -> true;
                    _            -> false
                end,

    %% To define a keepalive timeout value, add 'timeout=Int' in the query
    %% string.
    Tout  = case queryvar(A, "timeout") of
                {ok, Val} ->
                    try
                        list_to_integer(Val)
                    catch
                        _:_ -> infinity
                    end;
                _ ->
                    infinity
            end,

    %% To drop connection when a timeout occured, add 'drop=true' in the query
    %% string.
    Drop = case queryvar(A, "drop") of
               {ok, "true"} -> true;
               _            -> false
           end,

    %% To reject unmasked frames , add 'close_unmasked=true' in the query
    %% string.
    CloseUnmasked = case queryvar(A, "close_unmasked") of
                        {ok, "true"} -> true;
                        _            -> false
                    end,

    %% NOTE: change the line below to
    %%   Opts = [{origin, any}],
    %% if you want to accept calls from any origin.
    Opts = [
            {origin,            "http://" ++ (A#arg.headers)#headers.host},
            {keepalive,         KeepAlive},
            {keepalive_timeout, Tout},
            {drop_on_timeout,   Drop},
            {close_if_unmasked, CloseUnmasked}
           ],
    {websocket, CallbackMod, Opts}.


The above code can be executed Here.

Establish a WebSocket connection

To establish a WebSocket connection, a client must send a valid HTTP Upgrade request. Then, from the server side, the Yaws script (or the appmod or whatever) should return:

{websocket, CallbackMod, Options}

where CallbackMod is an atom identifying the WebSocket callback module, and Options is a (possibly empty) list (see below for details).

From here, Yaws spawns an Erlang process to manage the WebSocket connection. Once the handshake response is returned by Yaws, the connection is established and the handling process is ready to send or receive data. If something goes wrong during this step, Yaws returns an HTTP error (400, 403 or 500 depending of the error type).

Supported Options

The following options are available:

WebSocket callback modules

All frames received on a WebSocket connection are passed to the callback modules specified during the connection establishment by calling Module:handle_message/1 or Module:handle_message/2, depending on whether it’s a basic or an advanced callback module.

Basic callback modules

When a basic callback module is used, the messages defragmentation is handled by Yaws. From the callback module point of view, all incoming messages are unfragmented. This implies that fragmented frames will be accumulated, thus basic callback modules does not support data streaming.

A basic callback module MUST define the stateless function Module:handle_message/1:

Module:handle_message(Message) -> Result
    Message :: {Type, Data} | {close, Status, Reason}
    Result  :: noreply | {reply, Reply} | {close, CloseReason}
      Type        :: text | binary
      Data        :: binary()
      Reply       :: {Type, Data} | #ws_frame{} |
                     [{Type, Data}] | [#ws_frame{}]
      CloseReason :: Status | {Status, Reason}
      Status      :: integer() %% RFC 6455 Status Code
      Reason      :: binary()

This function is called when a message is received. {text, Data} (or {binary, Data}) is the unfragmented text (or binary) message. When the client closes the connection, the callback module is notified with the message {close, Status, Reason}, where Status is the numerical status code sent by the client or the value 1000 (see RFC 6455 - Section 7.4.1) if the client sent no status code. For an abnormal client closure, the status code is 1006 (as specified by RFC 6455 - Section 7.1.5). Reason is a binary containing any text the client sent to indicate the reason for closing the socket; this binary may be empty.

If the function returns {reply, Reply}, Reply is sent to the client. It is possible to send one or more unfragmentated messages by returning {Type, Data} or [{Type, Data}]. It is also possible to send one or more frames using the #ws_frame{} record instead, defined in include/yaws_api.hrl (useful to fragment messages by hand).

If the function returns noreply, nothing happens.

If the function returns {close, CloseReason}, the handling process closes the connection sending a close control frame to the client. CloseReason is used to set the status code and the (optional) close reason of the close control frame. Then the handling process terminates calling Module:terminate(CloseReason, State) (if defined, see below).

Because just handling messages is not enough for real applications, a basic callback module can define optional functions, mainly to manage a callback state. It can define one, some or all of the following functions:

Module:init(Args) -> Result
    Args   :: [ReqArg, InitialState]
    Result :: {ok, State} | {ok, State, Timeout} | {error, Reason}
      ReqArg       :: #arg{}
      InitialState :: term()
      State        :: term()
      Timeout      :: integer() >= 0 | infinity
      Reason       :: term()

If defined, this function is called to initialize the internal state of the callback module.

ReqArg is the #arg{} record supplied to the out/1 function and InitialState is the term associated to the CallbackType described above.

If an integer timeout value is provided, it will overload the next keepalive timeout (see keepalive_timeout option above). The atom infinity can be used to wait indefinitely. If no value is specified, the default keepalive timeout is used.

If something goes wrong during initialization, the function should return {error, Reason}, where Reason is any term.

Module:handle_open(WSState, State) -> Result
    WSState :: #ws_state{}
    State   :: term()
    Result  :: {ok, NewState} {error, Reason}
      NewState :: term()
      Reason   :: term()

If defined, this function is called when the connection is upgraded from HTTP to WebSocket.

WSState is the state of the WebSocket connection. It can be used to send messages to the client using yaws_api:websocket_send(WSState, Message).

State is the internal state of the callback module.

If the function returns {ok, NewState}, the handling process will continue executing with the possibly updated internal state NewState.

If the function returns {error, Reason}, the handling process closes the connection and terminates calling Module:terminate({error, Reason}, State) (if defined, see below).

Module:handle_message(Message, State) -> Result
    Message :: see Module:handle_message/1
    State   :: term()
    Result  :: {noreply, NewState} | {noreply, NewState, Timeout} |
               {reply, Reply} | {reply, Reply, NewState} |
               {reply, Reply, NewState, Timeout} |
               {close, CloseReason, NewState} |
               {close, CloseReason, Reply, NewState}
      NewState    :: term()
      Timeout     :: integer() >= 0 | infinity
      Reply       :: see Module:handle_message/1
      CloseReason :: see Module:handle_message/1

If defined, this function is called in place of Module:handle_message/1. The main difference with the previous version is that this one handles the internal state of the callback module.

State is internal state of the callback module.

See Module:handle_message/1 for a description of the other arguments and possible return values.

Module:handle_info(Info, State) -> Result
    Info   :: timeout | term()
    State  :: term()
    Result :: {noreply, NewState} | {noreply, NewState, Timeout} |
              {reply, Reply} | {reply, Reply, NewState} |
              {reply, Reply, NewState, Timeout} |
              {close, CloseReason, NewState} |
              {close, CloseReason, Reply, NewState}
      NewState    :: term()
      Timeout     :: integer() >= 0 | infinity
      Reply       :: see Module:handle_message/1
      CloseReason :: see Module:handle_message/1

If defined, this function is called when a timeout occurs (see drop_on_timeout option above) or when the handling process receives any unknown message.

Info is either the atom timeout, if a timeout has occurred, or the received message.

See Module:handle_message/1 for a description of the other arguments and possible return values.

Module:terminate(Reason, State) -> ok
    Reason :: Status | {Status, Text} | {error, Error}
    State  :: term()
      Status :: integer() %% RFC 6455 status code
      Text   :: binary()
      Error  :: term()

If defined, this function is called when the handling process is about to terminate. it should be the opposite of Module:init/1 and do any necessary cleaning up.

Reason is a term denoting the stop reason and State is the internal state of the callback module.

Advanced callback modules

Advanced callback modules should be used when automatic messages defragmentation done by Yaws is not desirable or acceptable. One could be used for example to handle data streaming over WebSockets. So, such modules should be prepared to handle frames directly (fragmented or not).

Unlike basic callback modules, Advanced ones MUST manage an internal state. So it MUST define the stateful function Module:handle_message/2 :

Module:handle_message(Frame, State) -> Result
    Frame   :: #ws_frame_info{} | {fail_connection, Status, Reason}
    State   :: term()
    Result  :: {noreply, NewState} | {noreply, NewState, Timeout} |
               {reply, Reply} | {reply, Reply, NewState} |
               {reply, Reply, NewState, Timeout} |
               {close, CloseReason, NewState} |
               {close, CloseReason, Reply, NewState}
      Status      :: integer() %% RFC 6455 status code
      Reason      :: binary()
      NewState    :: term()
      Timeout     :: integer() >= 0 | infinity
      Reply       :: see Module:handle_message/1
      CloseReason :: see Module:handle_message/1

This function is called when a frame is received. The #ws_frame_info{} record, defined in include/yaws_api.hrl, provides all details about this frame. State it the internal state of the callback module.

If an error occurs during the frame parsing, the term {fail_connection, Status, Reason} is passed, where Status is the numerical status code corresponding to the error (see RFC 6455 - Section 7.4.1) and Reason the binary containing optional information about it.

This function returns the same values as specified for the basic callback module's Module:handle_message/2. See above for details.

Advanced callback modules can also define the same optional functions as basic callback modules (except Module:handle_messages/2 which is mandatory here, of course). See above for details.

Record definitions

Here is the definition of records used in callback modules, defined in include/yaws_api.hrl:

%% Corresponds to the frame sections as in
%% https://tools.ietf.org/html/rfc6455#section-5.2
%% plus 'data' and 'ws_state'. Used for incoming frames.
-record(ws_frame_info, {
          fin,
          rsv,
          opcode,
          masked,
          masking_key,
          length,
          payload,
          data,        % The unmasked payload. Makes payload redundant.
          ws_state     % The ws_state after unframing this frame.
                       % This is useful for the endpoint to know what type of
                       % fragment a potentially fragmented message is.
         }).

%% Used for outgoing frames. No checks are done on the validity of a frame. This
%% is the application's responsability to send valid frames.
-record(ws_frame, {
          fin = true,
          rsv = 0,
          opcode,
          payload = <<>>
         }).

%%----------------------------------------------------------------------
%% The state of a WebSocket connection.
%% This is held by the ws owner process and passed in calls to yaws_api.
%%----------------------------------------------------------------------
-type frag_type() :: text
                   | binary
                   | none.  % The WebSocket is not expecting continuation
                            % of any fragmented message.
-record(ws_state, {
          vsn :: integer(),                     % WebSocket version number
          sock,                                 % gen_tcp or gen_ssl socket
          frag_type :: frag_type()
         }).

Valid XHTML 1.0!