Mint.WebSocket (MintWebSocket v0.1.0) View Source

(Unofficial) WebSocket support for the Mint functional HTTP client

Like Mint, Mint.WebSocket provides a functional, process-less interface for operating a WebSocket connection. Mint.WebSocket is an extension to Mint: the sending and receiving of messages is done with Mint functions. Prospective Mint.WebSocket users should first familiarize themselves with Mint.HTTP.

Mint.WebSocket is not fully spec-conformant on its own. Runtime behaviors such as responding to pings with pongs must be implemented by the user of Mint.WebSocket.

Usage

A connection formed with Mint.HTTP.connect/4 can be upgraded to a WebSocket connection with upgrade/4.

{:ok, conn} = Mint.HTTP.connect(:http, "localhost", 9_000)
{:ok, conn, ref} = Mint.WebSocket.upgrade(conn, "/", [])

upgrade/4 sends an upgrade request to the remote server. The WebSocket connection is then built by awaiting the HTTP response from the server.

http_reply_message = receive(do: (message -> message))
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, resp_headers}, {:done, ^ref}]} =
  Mint.HTTP.stream(conn, http_reply_message)

{:ok, conn, websocket} =
  Mint.WebSocket.new(conn, ref, status, resp_headers)

Now that the WebSocket connection has been formed, we use the websocket data structure to encode and decode frames, and the Mint.HTTP.stream_request_body/3 and Mint.HTTP.stream/2 functions from Mint to perform sending and receiving of encoded frames.

For example, we'll send a "hello world" text frame across our connection.

{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)

And let's say that the server is echoing our messages; let's receive our echoed "hello world" text frame.

echo_message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, echo_message)
{:ok, websocket, [{:text, "hello world"}]} = Mint.WebSocket.decode(websocket, data)

HTTP/2 Support

Mint.WebSocket supports WebSockets over HTTP/2 as defined in rfc8441. rfc8441 is an extension to the HTTP/2 specification. At the time of writing, very few HTTP/2 server libraries support or enable HTTP/2 WebSockets by default.

upgrade/4 works on both HTTP/1 and HTTP/2 connections. In order to select HTTP/2, the :http2 protocol should be explicitly selected in Mint.HTTP.connect/4.

{:ok, %Mint.HTTP2{} = conn} =
  Mint.HTTP.connect(:http, "websocket.example", 80, protocols: [:http2])
{:ok, conn, ref} = Mint.WebSocket.upgrade(conn, "/", [])

If the server does not support the extended CONNECT method needed to bootstrap WebSocket connections over HTTP/2, upgrade/4 will return an error tuple with the :extended_connect_disabled error reason.

{:error, conn, %Mint.WebSocketError{reason: :extended_connect_disabled}}

Why use HTTP/2 for WebSocket connections in the first place? HTTP/2 can multiplex many requests over the same connection, which can reduce the latency incurred by forming new connections for each request. A WebSocket connection only occupies one stream of a HTTP/2 connection, so even if an HTTP/2 connection has an open WebSocket communication, it can be used to transport more requests.

WebSocket Secure

Encryption of connections is handled by Mint functions. To start a WSS connection, select :https as the scheme in Mint.HTTP.connect/4:

{:ok, conn} = Mint.HTTP.connect(:https, "websocket.example", 443)

And use upgrade/4 to upgrade the connection to WebSocket. See the Mint documentation on SSL for more information.

Extensions

The WebSocket protocol allows for extensions. Extensions act as a middleware for encoding and decoding frames. For example "permessage-deflate" compresses and decompresses the body of data frames, which minifies the amount of bytes which must be sent over the network.

See Mint.WebSocket.Extension for more information about extensions and Mint.WebSocket.PerMessageDeflate for information about the "permessage-deflate" extension.

Link to this section Summary

Types

A WebSocket frame

Shorthand notations for control frames

t()

An immutable data structure representing a WebSocket connection

Functions

Decodes a binary into a list of frames

Encodes a frame into a binary

Creates a new WebSocket data structure given the server's reply to the upgrade request

Requests that a connection be upgraded to the WebSocket protocol

Link to this section Types

Specs

Specs

frame() ::
  {:text, String.t()}
  | {:binary, binary()}
  | {:ping, binary()}
  | {:pong, binary()}
  | {:close, code :: non_neg_integer(), reason :: binary()}

A WebSocket frame

  • {:binary, binary} - a frame containing binary data. Binary frames can be used to send arbitrary binary data such as a PDF.
  • {:text, text} - a frame containing string data. Text frames must be valid utf8. Elixir has wonderful support for utf8: String.valid?/1 can detect valid and invalid utf8.
  • {:ping, binary} - a control frame which the server should respond to with a pong. The binary data must be echoed in the pong response.
  • {:pong, binary} - a control frame which forms a reply to a ping frame. Pings and pongs may be used to check the a connection is alive or to estimate latency.
  • {:close, code, reason} - a control frame used to request that a connection be closed or to acknowledgee a close frame send by the server.

These may be passed to encode/2 or returned from decode/2.

Close frames

In order to close a WebSocket connection gracefully, either the client or server sends a close frame. Then the other endpoint responds with a close with code 1_000 and then closes the TCP connection. This can be accomplished in Mint.WebSocket like so:

{:ok, websocket, data} = Mint.WebSocket.encode(websocket, :close)
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)

close_response = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, close_response)
{:ok, websocket, [{:close, 1_000, ""}]} = Mint.WebSocket.decode(websocket, data)

Mint.HTTP.close(conn)

rfc6455 section 7.4.1 documents codes which may be used in the code element.

Specs

shorthand_frame() :: :ping | :pong | :close

Shorthand notations for control frames

  • :ping - shorthand for {:ping, ""}
  • :pong - shorthand for {:pong, ""}
  • :close - shorthand for {:close, 1_000, ""}

These may be passed to encode/2.

Specs

t()

An immutable data structure representing a WebSocket connection

Link to this section Functions

Specs

decode(t(), data :: binary()) ::
  {:ok, t(), [frame() | {:error, term()}]} | {:error, t(), any()}

Decodes a binary into a list of frames

The binary may received from the connection with Mint.HTTP.stream/2.

This function will invoke the Mint.WebSocket.Extension.decode/2 callback for any accepted extensions.

Examples

message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
{:ok, websocket, frames} = Mint.WebSocket.decode(websocket, data)
Link to this function

encode(websocket, frame)

View Source

Specs

encode(t(), shorthand_frame() | frame()) ::
  {:ok, t(), binary()} | {:error, t(), any()}

Encodes a frame into a binary

The resulting binary may be sent with Mint.HTTP.stream_request_body/3.

This function will invoke the Mint.WebSocket.Extension.encode/2 callback for any accepted extensions.

Examples

{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
Link to this function

new(conn, request_ref, status, response_headers)

View Source

Specs

Creates a new WebSocket data structure given the server's reply to the upgrade request

This function will setup any extensions accepted by the server using the Mint.WebSocket.Extension.init/2 callback.

Examples

http_reply = receive(do: (message -> message))
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, headers}, {:done, ^ref}]} =
  Mint.HTTP.stream(conn, http_reply)

{:ok, conn, websocket} =
  Mint.WebSocket.new(conn, ref, status, resp_headers)
Link to this function

upgrade(conn, path, headers, opts \\ [])

View Source

Specs

upgrade(
  conn :: Mint.HTTP.t(),
  path :: String.t(),
  headers :: Mint.Types.headers(),
  opts :: Keyword.t()
) ::
  {:ok, Mint.HTTP.t(), Mint.Types.request_ref()}
  | {:error, Mint.HTTP.t(), error()}

Requests that a connection be upgraded to the WebSocket protocol

This function wraps Mint.HTTP.request/5 to provide a single interface for bootstrapping an upgrade for HTTP/1 and HTTP/2 connections.

For HTTP/1 connections, this function performs a GET request with WebSocket-specific headers. For HTTP/2 connections, this function performs an extended CONNECT request which opens a stream to be used for the WebSocket connection.

Options

  • :extensions - a list of extensions to negotiate. See the extensions section below.

Extensions

Extensions should be declared by passing the :extensions option in the opts keyword list. Note that in the WebSocket protocol, extensions are negotiated: the client proposes a list of extensions and the server may accept any (or none) of them. See Mint.WebSocket.Extension for more information about extension negotiation.

Extensions may be passed as a list of Mint.WebSocket.Extension structs or with the following shorthand notations:

  • module - shorthand for {module, []}
  • {module, params} - shorthand for {module, params, []}
  • {module, params, opts} - a shorthand which is expanded to a Mint.WebSocket.Extension struct

Examples

{:ok, conn} = Mint.HTTP.connect(:http, "localhost", 9_000)
{:ok, conn, ref} =
  Mint.WebSocket.upgrade(conn, "/", [], extensions: [Mint.WebSocket.PerMessageDeflate])
# or provide params:
{:ok, conn, ref} =
  Mint.WebSocket.upgrade(
    conn,
    "/",
    [],
    extensions: [{Mint.WebSocket.PerMessageDeflate, [:client_max_window_bits]]}]
  )