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?
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.
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).
The following options are available:
{callback, CallbackType}
Specify the type of the callback module. CallbackType
can
be either of the following:
basic
- Same as {basic, []}
. This is the
default.
{basic, InitialState}
- Indicate your callback module
is a basic callback module. InitialState
is the
callback's initial state for handling this client.
{advanced, InitialState}
- Same as above but for an
advanced callback module.
{origin, Origin}
Specify the Origin
URL from which messages will be
accepted. This is useful for protecting against cross-site attack. The
option defaults to any
, meaning calls will be accepted
from any origin.
{keepalive, KeepAliveBoolean}
If true, Yaws will automatically send a ping message
every keepAliveTimeout
milliseconds. By default keepalive
pings are disabled.
{keepalive_timeout, keepAliveTimeout}
Specify the interval in milliseconds to send keepalive pings, by
default 30000. Ignored if KeepAliveBoolean
is false.
{keepalive_grace_period, KeepAliveGracePeriod}
Specify the amount of time, in milliseconds, to wait after sending a
keepalive ping. If no message is received
within KeepAliveGracePeriod
milliseconds, a timeout will
occur. Depending on the DropBoolean
value, a close frame
is sent with the status code 1006 (if DropBoolean
is
true) or the callback module is notified
(see Module:handle_info/2
below).
By
default, KeepAliveGracePeriod
is set to 2000. Ignored
if KeepAliveBoolean
is false.
{drop_on_timeout, DropBoolean}
If true, a close frame is sent with the status code 1006 when a
timeout occurs after a keepalive ping has been sent
(see KeepAliveGracePeriod
). Disabled by default. Ignored
if KeepAliveBoolean
is false.
{close_timeout, CloseTimeout}
After sending a close frame to a client, Yaws will wait for the client
acknowledgement for CloseTimeout
milliseconds. Then it
will close the underlying TCP connection. By
default CloseTimeout
is set to 5000.
{close_if_unmasked, CloseUnmaskedBoolean}
If true, Yaws will reject any unmasked incoming frame by sending a
close frame with the status code 1002. Disabled by default.
Note: According to RFC 6455, a client must mask all frames that it
sends to the server
(See RFC
6455 - Section 5.1).
{max_frame_size, MaxFrameSize}
Specify the maximum allowed size, in bytes, for received frames. By
default 16MB. It is also the maximum size for unfragmented
messages.
This limit is checked for all types of callback module.
{max_message_size, MaxMsgSize}
Specify the maximum allowed message size in bytes, by default
16MB.
This limit is checked only for basic callback modules.
{auto_fragment_message, AutoFragBoolean}
If true, outgoing messages will be automatically fragmented if their
payload exceeds OutFragSize
bytes. This flag is set to
false by default.
{auto_fragment_threshold, OutFragSize}
Specify the maximum payload size of each fragment
if AutoFragBoolean
is true. OutFragSize
is
set to 1MB by default. Ignored is AutoFragBoolean
is
false.
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.
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) -> ResultMessage :: {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}
, whereStatus
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 ininclude/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 callingModule: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) -> ResultArgs :: [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 theout/1
function andInitialState
is the term associated to theCallbackType
described above.If an integer timeout value is provided, it will overload the next keepalive timeout (see
keepalive_timeout
option above). The atominfinity
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}
, whereReason
is any term.
Module:handle_open(WSState, State) -> ResultWSState :: #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 usingyaws_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 stateNewState
.If the function returns
{error, Reason}
, the handling process closes the connection and terminates callingModule:terminate({error, Reason}, State)
(if defined, see below).
Module:handle_message(Message, State) -> ResultMessage :: 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/1If 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) -> ResultInfo :: 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/1If 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 atomtimeout
, 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) -> okReason :: 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 andState
is the internal state of the callback module.
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) -> ResultFrame :: #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/1This function is called when a frame is received. The
#ws_frame_info{}
record, defined ininclude/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, whereStatus
is the numerical status code corresponding to the error (see RFC 6455 - Section 7.4.1) andReason
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.
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() }).