diff options
Diffstat (limited to 'doc')
-rw-r--r-- | doc/src/guide/ws_handlers.asciidoc | 273 |
1 files changed, 171 insertions, 102 deletions
diff --git a/doc/src/guide/ws_handlers.asciidoc b/doc/src/guide/ws_handlers.asciidoc index 1411ab6..e1a7c25 100644 --- a/doc/src/guide/ws_handlers.asciidoc +++ b/doc/src/guide/ws_handlers.asciidoc @@ -1,20 +1,22 @@ [[ws_handlers]] -== Handling Websocket connections +== Websocket handlers -A special handler is required for handling Websocket connections. -Websocket handlers allow you to initialize the connection, -handle incoming frames from the socket, handle incoming Erlang -messages and then clean up on termination. +Websocket handlers provide an interface for upgrading HTTP/1.1 +connections to Websocket and sending or receiving frames on +the Websocket connection. -Websocket handlers essentially act as a bridge between the client -and the Erlang system. They will typically do little more than -socket communication and decoding/encoding of frames. +As Websocket connections are established through the HTTP/1.1 +upgrade mechanism, Websocket handlers need to be able to first +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 -to all handlers. To establish a Websocket connection, this function -must return a `ws` tuple. +The `init/2` callback is called when the request is received. +To establish a Websocket connection, you must switch to the +`cowboy_websocket` module: [source,erlang] ---- @@ -22,15 +24,36 @@ init(Req, State) -> {cowboy_websocket, Req, State}. ---- -Upon receiving this tuple, Cowboy will switch to the code -that handles Websocket connections and perform the handshake -immediately. +Cowboy will perform the Websocket handshake immediately. Note +that the handshake will fail if the client did not request an +upgrade to Websocket. -If the sec-websocket-protocol header was sent with the request -for establishing a Websocket connection, then the Websocket -handler *must* select one of these subprotocol and send it -back to the client, otherwise the client might decide to close -the connection, assuming no correct subprotocol was found. +The Req object becomes unavailable after this function returns. +Any information required for proper execution of the Websocket +handler must be saved in the state. + +=== 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] ---- @@ -39,10 +62,10 @@ init(Req, State) -> undefined -> {ok, Req, State}; Subprotocols -> - case lists:keymember(<<"mychat2">>, 1, Subprotocols) of + case lists:keymember(<<"mqtt">>, 1, Subprotocols) of true -> Req2 = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, - <<"mychat2">>, Req), + <<"mqtt">>, Req), {ok, Req2, State}; false -> {stop, Req, State} @@ -50,42 +73,50 @@ init(Req, State) -> end. ---- -It is not recommended to wait too long inside the `init/2` -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. +=== Post-upgrade initialization + +Cowboy has separate processes for handling the connection +and requests. Because Websocket takes over the connection, +the Websocket protocol handling occurs in a different +process than the request handling. -It is also very easy to ensure that this message arrives before -any message from other processes by sending it before registering -or enabling timers. +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. -// @todo This doesn't even work. +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] ---- -init(Req, State) -> - self() ! post_init, - %% Register process here... - {cowboy_websocket, Req, State}. - -websocket_info(post_init, State) -> - %% Perform post_init initialization here... +websocket_init(State) -> + erlang:start_timer(1000, self(), <<"Hello!">>), {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, -ping or pong frame arrives from the client. Note that in the -case of ping and pong frames, no action is expected as Cowboy -automatically replies to ping frames. +ping or pong frame arrives from the client. -The handler can decide to send frames to the socket, stop -or just continue without sending anything. +The handler can handle or ignore the frames. It can also +send frames back to the client or stop the connection. The following snippet echoes back any text frame received and -ignores all others. +ignores all others: [source,erlang] ---- @@ -95,16 +126,20 @@ websocket_handle(_Frame, 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 arrives. -The handler can decide to send frames to the socket, stop -or just continue without sending anything. +The handler can handle or ignore the messages. It can also +send frames to the client or stop the connection. -The following snippet forwards any `log` message to the socket -and ignores all others. +The following snippet forwards log messages to the client +and ignores all others: [source,erlang] ---- @@ -114,60 +149,68 @@ websocket_info(_Info, State) -> {ok, State}. ---- -=== Sending frames to the socket +=== Sending frames -Cowboy allows sending either a single frame or a list of -frames to the socket, in which case the frames are sent -sequentially. Any frame can be sent: text, binary, ping, -pong or close frames. +// @todo So yeah, reply makes no sense. Maybe change it to send. Sigh. -The following example sends three frames using a single `reply` -tuple. +All `websocket_` callbacks share return values. They may +send zero, one or many frames to the client. + +To send nothing, just return an ok tuple: [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, [ {text, "Hello"}, {text, <<"world!">>}, {binary, <<0:8000>>} - ], State}; -%% More websocket_info/2 clauses here... + ], State}. ---- -Note that the payload for text and binary frames is of type -`iodata()`, meaning it can be either a `binary()` or an -`iolist()`. +They are sent in the given order. -Sending a `close` frame will immediately initiate the closing -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 +=== Keeping the connection alive -The biggest performance improvement you can do when dealing -with a huge number of Websocket connections is to reduce the -number of timers that are started on the server. A common use -of timers when dealing with connections is for sending a ping -every once in a while. This should be done exclusively on the -client side. Indeed, a server handling one million Websocket -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 client. They are still forwarded to the handler for +informative purposes, but no further action is required. -Cowboy will automatically respond to ping frames sent by the -client. It will still forward the frame to the handler for -informative purpose, but no further action is required. +Cowboy does not send ping frames itself. The handler can +do it if required. A better solution in most cases is to +let the client handle pings. Doing it from the handler +would imply having an additional timer per connection and +this can be a considerable cost for servers that need to +handle large numbers of connections. -Cowboy can be configured to automatically close the Websocket -connection when no data arrives on the socket. It is highly -recommended to configure a timeout for it, as otherwise you -may end up with zombie "half-connected" sockets that may -leave the process alive forever. +Cowboy can be configured to close idle connections +automatically. It is highly recommended to configure +a timeout here, to avoid having processes linger longer +than needed. -A good timeout value is 60 seconds. +The `init/2` callback can set the timeout to be used +for the connection. For example, this would make Cowboy +close connections idle for more than 60 seconds: [source,erlang] ---- @@ -178,21 +221,47 @@ init(Req, State) -> This value cannot be changed once it is set. It defaults to `infinity`. -=== Hibernate +// @todo Perhaps the default should be changed. -Most tuples returned from handler callbacks can include an -extra value `hibernate`. After doing any necessary operations -following the return of the callback, Cowboy will hibernate -the process. +=== Saving memory -It is highly recommended to hibernate processes that do not -handle much traffic. It is a good idea to hibernate all -connections by default and investigate only when you start -noticing increased CPU usage. +The Websocket connection process can be set to hibernate +after the callback returns. -=== Supporting older browsers +Simply add an `hibernate` field to the ok or reply tuples: + +[source,erlang] +---- +websocket_init(State) -> + {ok, State, hibernate}. + +websocket_handle(_Frame, State) -> + {ok, State, hibernate}. -Unfortunately Websocket is a relatively recent technology, -which means that not all browsers support it. A library like -https://github.com/ninenines/bullet[Bullet] can be used to -emulate Websocket connections on older browsers. +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. |