mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 20:30:23 +00:00
Update the Websocket handlers chapter
This commit is contained in:
parent
06e1e2be68
commit
a231216b07
1 changed files with 171 additions and 102 deletions
|
@ -1,20 +1,22 @@
|
||||||
[[ws_handlers]]
|
[[ws_handlers]]
|
||||||
== Handling Websocket connections
|
== Websocket handlers
|
||||||
|
|
||||||
A special handler is required for handling Websocket connections.
|
Websocket handlers provide an interface for upgrading HTTP/1.1
|
||||||
Websocket handlers allow you to initialize the connection,
|
connections to Websocket and sending or receiving frames on
|
||||||
handle incoming frames from the socket, handle incoming Erlang
|
the Websocket connection.
|
||||||
messages and then clean up on termination.
|
|
||||||
|
|
||||||
Websocket handlers essentially act as a bridge between the client
|
As Websocket connections are established through the HTTP/1.1
|
||||||
and the Erlang system. They will typically do little more than
|
upgrade mechanism, Websocket handlers need to be able to first
|
||||||
socket communication and decoding/encoding of frames.
|
receive the HTTP request for the upgrade, before switching to
|
||||||
|
Websocket and taking over the connection. They can then receive
|
||||||
|
or send Websocket frames, handle incoming Erlang messages or
|
||||||
|
close the connection.
|
||||||
|
|
||||||
=== Initialization
|
=== Upgrade
|
||||||
|
|
||||||
First, the `init/2` callback is called. This callback is common
|
The `init/2` callback is called when the request is received.
|
||||||
to all handlers. To establish a Websocket connection, this function
|
To establish a Websocket connection, you must switch to the
|
||||||
must return a `ws` tuple.
|
`cowboy_websocket` module:
|
||||||
|
|
||||||
[source,erlang]
|
[source,erlang]
|
||||||
----
|
----
|
||||||
|
@ -22,15 +24,36 @@ init(Req, State) ->
|
||||||
{cowboy_websocket, Req, State}.
|
{cowboy_websocket, Req, State}.
|
||||||
----
|
----
|
||||||
|
|
||||||
Upon receiving this tuple, Cowboy will switch to the code
|
Cowboy will perform the Websocket handshake immediately. Note
|
||||||
that handles Websocket connections and perform the handshake
|
that the handshake will fail if the client did not request an
|
||||||
immediately.
|
upgrade to Websocket.
|
||||||
|
|
||||||
If the sec-websocket-protocol header was sent with the request
|
The Req object becomes unavailable after this function returns.
|
||||||
for establishing a Websocket connection, then the Websocket
|
Any information required for proper execution of the Websocket
|
||||||
handler *must* select one of these subprotocol and send it
|
handler must be saved in the state.
|
||||||
back to the client, otherwise the client might decide to close
|
|
||||||
the connection, assuming no correct subprotocol was found.
|
=== Subprotocol
|
||||||
|
|
||||||
|
The client may provide a list of Websocket subprotocols it
|
||||||
|
supports in the sec-websocket-protocol header. The server *must*
|
||||||
|
select one of them and send it back to the client or the
|
||||||
|
handshake will fail.
|
||||||
|
|
||||||
|
For example, a client could understand both STOMP and MQTT over
|
||||||
|
Websocket, and provide the header:
|
||||||
|
|
||||||
|
----
|
||||||
|
sec-websocket-protocol: v12.stomp, mqtt
|
||||||
|
----
|
||||||
|
|
||||||
|
If the server only understands MQTT it can return:
|
||||||
|
|
||||||
|
----
|
||||||
|
sec-websocket-protocol: mqtt
|
||||||
|
----
|
||||||
|
|
||||||
|
This selection must be done in `init/2`. An example usage could
|
||||||
|
be:
|
||||||
|
|
||||||
[source,erlang]
|
[source,erlang]
|
||||||
----
|
----
|
||||||
|
@ -39,10 +62,10 @@ init(Req, State) ->
|
||||||
undefined ->
|
undefined ->
|
||||||
{ok, Req, State};
|
{ok, Req, State};
|
||||||
Subprotocols ->
|
Subprotocols ->
|
||||||
case lists:keymember(<<"mychat2">>, 1, Subprotocols) of
|
case lists:keymember(<<"mqtt">>, 1, Subprotocols) of
|
||||||
true ->
|
true ->
|
||||||
Req2 = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>,
|
Req2 = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>,
|
||||||
<<"mychat2">>, Req),
|
<<"mqtt">>, Req),
|
||||||
{ok, Req2, State};
|
{ok, Req2, State};
|
||||||
false ->
|
false ->
|
||||||
{stop, Req, State}
|
{stop, Req, State}
|
||||||
|
@ -50,42 +73,50 @@ init(Req, State) ->
|
||||||
end.
|
end.
|
||||||
----
|
----
|
||||||
|
|
||||||
It is not recommended to wait too long inside the `init/2`
|
=== Post-upgrade initialization
|
||||||
function. Any extra initialization may be done after returning by
|
|
||||||
sending yourself a message before doing anything. Any message sent
|
|
||||||
to `self()` from `init/2` is guaranteed to arrive before
|
|
||||||
any frames from the client.
|
|
||||||
|
|
||||||
It is also very easy to ensure that this message arrives before
|
Cowboy has separate processes for handling the connection
|
||||||
any message from other processes by sending it before registering
|
and requests. Because Websocket takes over the connection,
|
||||||
or enabling timers.
|
the Websocket protocol handling occurs in a different
|
||||||
|
process than the request handling.
|
||||||
|
|
||||||
// @todo This doesn't even work.
|
This is reflected in the different callbacks Websocket
|
||||||
|
handlers have. The `init/2` callback is called from the
|
||||||
|
temporary request process and the `websocket_` callbacks
|
||||||
|
from the connection process.
|
||||||
|
|
||||||
|
This means that some initialization cannot be done from
|
||||||
|
`init/2`. Anything that would require the current pid,
|
||||||
|
or be tied to the current pid, will not work as intended.
|
||||||
|
The optional `websocket_init/1` can be used instead:
|
||||||
|
|
||||||
[source,erlang]
|
[source,erlang]
|
||||||
----
|
----
|
||||||
init(Req, State) ->
|
websocket_init(State) ->
|
||||||
self() ! post_init,
|
erlang:start_timer(1000, self(), <<"Hello!">>),
|
||||||
%% Register process here...
|
|
||||||
{cowboy_websocket, Req, State}.
|
|
||||||
|
|
||||||
websocket_info(post_init, State) ->
|
|
||||||
%% Perform post_init initialization here...
|
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Handling frames from the client
|
All Websocket callbacks share the same return values. This
|
||||||
|
means that we can send frames to the client right after
|
||||||
|
the upgrade:
|
||||||
|
|
||||||
|
[source,erlang]
|
||||||
|
----
|
||||||
|
websocket_init(State) ->
|
||||||
|
{reply, {text, <<"Hello!">>}, State}.
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Receiving frames
|
||||||
|
|
||||||
Cowboy will call `websocket_handle/2` whenever a text, binary,
|
Cowboy will call `websocket_handle/2` whenever a text, binary,
|
||||||
ping or pong frame arrives from the client. Note that in the
|
ping or pong frame arrives from the client.
|
||||||
case of ping and pong frames, no action is expected as Cowboy
|
|
||||||
automatically replies to ping frames.
|
|
||||||
|
|
||||||
The handler can decide to send frames to the socket, stop
|
The handler can handle or ignore the frames. It can also
|
||||||
or just continue without sending anything.
|
send frames back to the client or stop the connection.
|
||||||
|
|
||||||
The following snippet echoes back any text frame received and
|
The following snippet echoes back any text frame received and
|
||||||
ignores all others.
|
ignores all others:
|
||||||
|
|
||||||
[source,erlang]
|
[source,erlang]
|
||||||
----
|
----
|
||||||
|
@ -95,16 +126,20 @@ websocket_handle(_Frame, State) ->
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Handling Erlang messages
|
Note that ping and pong frames require no action from the
|
||||||
|
handler as Cowboy will automatically reply to ping frames.
|
||||||
|
They are provided for informative purposes only.
|
||||||
|
|
||||||
|
=== Receiving Erlang messages
|
||||||
|
|
||||||
Cowboy will call `websocket_info/2` whenever an Erlang message
|
Cowboy will call `websocket_info/2` whenever an Erlang message
|
||||||
arrives.
|
arrives.
|
||||||
|
|
||||||
The handler can decide to send frames to the socket, stop
|
The handler can handle or ignore the messages. It can also
|
||||||
or just continue without sending anything.
|
send frames to the client or stop the connection.
|
||||||
|
|
||||||
The following snippet forwards any `log` message to the socket
|
The following snippet forwards log messages to the client
|
||||||
and ignores all others.
|
and ignores all others:
|
||||||
|
|
||||||
[source,erlang]
|
[source,erlang]
|
||||||
----
|
----
|
||||||
|
@ -114,60 +149,68 @@ websocket_info(_Info, State) ->
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Sending frames to the socket
|
=== Sending frames
|
||||||
|
|
||||||
Cowboy allows sending either a single frame or a list of
|
// @todo So yeah, reply makes no sense. Maybe change it to send. Sigh.
|
||||||
frames to the socket, in which case the frames are sent
|
|
||||||
sequentially. Any frame can be sent: text, binary, ping,
|
|
||||||
pong or close frames.
|
|
||||||
|
|
||||||
The following example sends three frames using a single `reply`
|
All `websocket_` callbacks share return values. They may
|
||||||
tuple.
|
send zero, one or many frames to the client.
|
||||||
|
|
||||||
|
To send nothing, just return an ok tuple:
|
||||||
|
|
||||||
[source,erlang]
|
[source,erlang]
|
||||||
----
|
----
|
||||||
websocket_info(hello_world, State) ->
|
websocket_info(_Info, State) ->
|
||||||
|
{ok, State}.
|
||||||
|
----
|
||||||
|
|
||||||
|
To send one frame, return a reply tuple with the frame to send:
|
||||||
|
|
||||||
|
[source,erlang]
|
||||||
|
----
|
||||||
|
websocket_info(_Info, State) ->
|
||||||
|
{reply, {text, <<"Hello!">>}, State}.
|
||||||
|
----
|
||||||
|
|
||||||
|
You can send frames of any type: text, binary, ping, pong
|
||||||
|
or close frames.
|
||||||
|
|
||||||
|
To send many frames at once, return a reply tuple with the
|
||||||
|
list of frames to send:
|
||||||
|
|
||||||
|
[source,erlang]
|
||||||
|
----
|
||||||
|
websocket_info(_Info, State) ->
|
||||||
{reply, [
|
{reply, [
|
||||||
{text, "Hello"},
|
{text, "Hello"},
|
||||||
{text, <<"world!">>},
|
{text, <<"world!">>},
|
||||||
{binary, <<0:8000>>}
|
{binary, <<0:8000>>}
|
||||||
], State};
|
], State}.
|
||||||
%% More websocket_info/2 clauses here...
|
|
||||||
----
|
----
|
||||||
|
|
||||||
Note that the payload for text and binary frames is of type
|
They are sent in the given order.
|
||||||
`iodata()`, meaning it can be either a `binary()` or an
|
|
||||||
`iolist()`.
|
|
||||||
|
|
||||||
Sending a `close` frame will immediately initiate the closing
|
=== Keeping the connection alive
|
||||||
of the Websocket connection. Be aware that any additional
|
|
||||||
frames sent by the client or any Erlang messages waiting to
|
|
||||||
be received will not be processed. Also note that when replying
|
|
||||||
a list of frames that includes close, any frame found after the
|
|
||||||
close frame will not be sent.
|
|
||||||
|
|
||||||
=== Ping and timeout
|
Cowboy will automatically respond to ping frames sent by
|
||||||
|
the client. They are still forwarded to the handler for
|
||||||
|
informative purposes, but no further action is required.
|
||||||
|
|
||||||
The biggest performance improvement you can do when dealing
|
Cowboy does not send ping frames itself. The handler can
|
||||||
with a huge number of Websocket connections is to reduce the
|
do it if required. A better solution in most cases is to
|
||||||
number of timers that are started on the server. A common use
|
let the client handle pings. Doing it from the handler
|
||||||
of timers when dealing with connections is for sending a ping
|
would imply having an additional timer per connection and
|
||||||
every once in a while. This should be done exclusively on the
|
this can be a considerable cost for servers that need to
|
||||||
client side. Indeed, a server handling one million Websocket
|
handle large numbers of connections.
|
||||||
connections will perform a lot better when it doesn't have to
|
|
||||||
handle one million extra timers too!
|
|
||||||
|
|
||||||
Cowboy will automatically respond to ping frames sent by the
|
Cowboy can be configured to close idle connections
|
||||||
client. It will still forward the frame to the handler for
|
automatically. It is highly recommended to configure
|
||||||
informative purpose, but no further action is required.
|
a timeout here, to avoid having processes linger longer
|
||||||
|
than needed.
|
||||||
|
|
||||||
Cowboy can be configured to automatically close the Websocket
|
The `init/2` callback can set the timeout to be used
|
||||||
connection when no data arrives on the socket. It is highly
|
for the connection. For example, this would make Cowboy
|
||||||
recommended to configure a timeout for it, as otherwise you
|
close connections idle for more than 60 seconds:
|
||||||
may end up with zombie "half-connected" sockets that may
|
|
||||||
leave the process alive forever.
|
|
||||||
|
|
||||||
A good timeout value is 60 seconds.
|
|
||||||
|
|
||||||
[source,erlang]
|
[source,erlang]
|
||||||
----
|
----
|
||||||
|
@ -178,21 +221,47 @@ init(Req, State) ->
|
||||||
This value cannot be changed once it is set. It defaults to
|
This value cannot be changed once it is set. It defaults to
|
||||||
`infinity`.
|
`infinity`.
|
||||||
|
|
||||||
=== Hibernate
|
// @todo Perhaps the default should be changed.
|
||||||
|
|
||||||
Most tuples returned from handler callbacks can include an
|
=== Saving memory
|
||||||
extra value `hibernate`. After doing any necessary operations
|
|
||||||
following the return of the callback, Cowboy will hibernate
|
|
||||||
the process.
|
|
||||||
|
|
||||||
It is highly recommended to hibernate processes that do not
|
The Websocket connection process can be set to hibernate
|
||||||
handle much traffic. It is a good idea to hibernate all
|
after the callback returns.
|
||||||
connections by default and investigate only when you start
|
|
||||||
noticing increased CPU usage.
|
|
||||||
|
|
||||||
=== Supporting older browsers
|
Simply add an `hibernate` field to the ok or reply tuples:
|
||||||
|
|
||||||
Unfortunately Websocket is a relatively recent technology,
|
[source,erlang]
|
||||||
which means that not all browsers support it. A library like
|
----
|
||||||
https://github.com/ninenines/bullet[Bullet] can be used to
|
websocket_init(State) ->
|
||||||
emulate Websocket connections on older browsers.
|
{ok, State, hibernate}.
|
||||||
|
|
||||||
|
websocket_handle(_Frame, State) ->
|
||||||
|
{ok, State, hibernate}.
|
||||||
|
|
||||||
|
websocket_info(_Info, State) ->
|
||||||
|
{reply, {text, <<"Hello!">>}, State, hibernate}.
|
||||||
|
----
|
||||||
|
|
||||||
|
It is highly recommended to write your handlers with
|
||||||
|
hibernate enabled, as this allows to greatly reduce the
|
||||||
|
memory usage. Do note however that an increase in the
|
||||||
|
CPU usage or latency can be observed instead, in particular
|
||||||
|
for the more busy connections.
|
||||||
|
|
||||||
|
=== Closing the connection
|
||||||
|
|
||||||
|
The connection can be closed at any time, either by telling
|
||||||
|
Cowboy to stop it or by sending a close frame.
|
||||||
|
|
||||||
|
To tell Cowboy to close the connection, use a stop tuple:
|
||||||
|
|
||||||
|
[source,erlang]
|
||||||
|
----
|
||||||
|
websocket_info(_Info, State) ->
|
||||||
|
{stop, State}.
|
||||||
|
----
|
||||||
|
|
||||||
|
Sending a `close` frame will immediately initiate the closing
|
||||||
|
of the Websocket connection. Note that when sending a list of
|
||||||
|
frames that include a close frame, any frame found after the
|
||||||
|
close frame will not be sent.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue