Skip to content

Commit 9abbffa

Browse files
expand docs (#14)
1 parent 03cbd74 commit 9abbffa

10 files changed

Lines changed: 431 additions & 154 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,16 @@ http_get_message = receive(do: (message -> message))
6868
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, resp_headers}, {:done, ^ref}]} =
6969
Mint.HTTP.stream(conn, http_get_message)
7070

71-
{:ok, conn, websocket} = Mint.WebSocket.new(conn, ref, status, req_headers, resp_headers)
71+
{:ok, conn, websocket} = Mint.WebSocket.new(conn, ref, status, resp_headers)
7272

7373
# send the hello world frame
7474
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
7575
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
7676

7777
# receive the hello world reply frame
7878
hello_world_echo_message = receive(do: (message -> message))
79-
{:ok, _conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, hello_world_echo_message)
80-
{:ok, _websocket, [{:text, "hello world"}]} = Mint.WebSocket.decode(websocket, data)
79+
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, hello_world_echo_message)
80+
{:ok, websocket, [{:text, "hello world"}]} = Mint.WebSocket.decode(websocket, data)
8181
```
8282

8383
## Development workflow

config/config.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import Config
22

3-
config :logger, level: :info
3+
# config :logger, level: :info

lib/mint/web_socket.ex

Lines changed: 269 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,197 @@
11
defmodule Mint.WebSocket do
22
@moduledoc """
3-
WebSocket
3+
(Unofficial) WebSocket support for the Mint functional HTTP client
4+
5+
Like Mint, `Mint.WebSocket` provides a functional, process-less interface
6+
for operating a WebSocket connection. `Mint.WebSocket` is an extension
7+
to Mint: the sending and receiving of messages is done with Mint functions.
8+
Prospective Mint.WebSocket users should first familiarize themselves with
9+
`Mint.HTTP`.
10+
11+
Mint.WebSocket is not fully spec-conformant on its own. Runtime behaviors
12+
such as responding to pings with pongs must be implemented by the user of
13+
Mint.WebSocket.
14+
15+
## Usage
16+
17+
A connection formed with `Mint.HTTP.connect/4` can be upgraded to a WebSocket
18+
connection with `upgrade/4`.
19+
20+
```elixir
21+
{:ok, conn} = Mint.HTTP.connect(:http, "localhost", 9_000)
22+
{:ok, conn, ref} = Mint.WebSocket.upgrade(conn, "/", [])
23+
```
24+
25+
`upgrade/4` sends an upgrade request to the remote server. The WebSocket
26+
connection is then built by awaiting the HTTP response from the server.
27+
28+
```elixir
29+
http_reply_message = receive(do: (message -> message))
30+
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, resp_headers}, {:done, ^ref}]} =
31+
Mint.HTTP.stream(conn, http_reply_message)
32+
33+
{:ok, conn, websocket} =
34+
Mint.WebSocket.new(conn, ref, status, resp_headers)
35+
```
36+
37+
Now that the WebSocket connection has been formed, we use the `websocket`
38+
data structure to encode and decode frames, and the
39+
`Mint.HTTP.stream_request_body/3` and `Mint.HTTP.stream/2` functions from
40+
Mint to perform sending and receiving of encoded frames.
41+
42+
For example, we'll send a "hello world" text frame across our connection.
43+
44+
```elixir
45+
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
46+
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
47+
```
48+
49+
And let's say that the server is echoing our messages; let's receive our
50+
echoed "hello world" text frame.
51+
52+
```elixir
53+
echo_message = receive(do: (message -> message))
54+
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, echo_message)
55+
{:ok, websocket, [{:text, "hello world"}]} = Mint.WebSocket.decode(websocket, data)
56+
```
57+
58+
## HTTP/2 Support
59+
60+
Mint.WebSocket supports WebSockets over HTTP/2 as defined in rfc8441.
61+
rfc8441 is an extension to the HTTP/2 specification. At the time of
62+
writing, very few HTTP/2 server libraries support or enable HTTP/2
63+
WebSockets by default.
64+
65+
`upgrade/4` works on both HTTP/1 and HTTP/2 connections. In order to select
66+
HTTP/2, the `:http2` protocol should be explicitly selected in
67+
`Mint.HTTP.connect/4`.
68+
69+
```elixir
70+
{:ok, %Mint.HTTP2{} = conn} =
71+
Mint.HTTP.connect(:http, "websocket.example", 80, protocols: [:http2])
72+
{:ok, conn, ref} = Mint.WebSocket.upgrade(conn, "/", [])
73+
```
74+
75+
If the server does not support the extended CONNECT method needed to bootstrap
76+
WebSocket connections over HTTP/2, `upgrade/4` will return an error tuple
77+
with the `:extended_connect_disabled` error reason.
78+
79+
```elixir
80+
{:error, conn, %Mint.WebSocketError{reason: :extended_connect_disabled}}
81+
```
82+
83+
Why use HTTP/2 for WebSocket connections in the first place? HTTP/2
84+
can multiplex many requests over the same connection, which can
85+
reduce the latency incurred by forming new connections for each request.
86+
A WebSocket connection only occupies one stream of a HTTP/2 connection, so
87+
even if an HTTP/2 connection has an open WebSocket communication, it can be
88+
used to transport more requests.
89+
90+
## WebSocket Secure
91+
92+
Encryption of connections is handled by Mint functions. To start a WSS
93+
connection, select `:https` as the scheme in `Mint.HTTP.connect/4`:
94+
95+
```elixir
96+
{:ok, conn} = Mint.HTTP.connect(:https, "websocket.example", 443)
97+
```
98+
99+
And use `upgrade/4` to upgrade the connection to WebSocket. See the
100+
Mint documentation on SSL for more information.
101+
102+
## Extensions
103+
104+
The WebSocket protocol allows for _extensions_. Extensions act as a
105+
middleware for encoding and decoding frames. For example "permessage-deflate"
106+
compresses and decompresses the body of data frames, which minifies the amount
107+
of bytes which must be sent over the network.
108+
109+
See `Mint.WebSocket.Extension` for more information about extensions and
110+
`Mint.WebSocket.PerMessageDeflate` for information about the
111+
"permessage-deflate" extension.
4112
"""
5113

6-
require __MODULE__.Frame, as: Frame
7-
alias __MODULE__.{Utils, Extension}
114+
alias __MODULE__.{Utils, Extension, Frame}
8115
alias Mint.WebSocketError
9116

10-
@type t :: %__MODULE__{}
117+
@typedoc """
118+
An immutable data structure representing a WebSocket connection
119+
"""
120+
@opaque t :: %__MODULE__{
121+
extensions: [Extension.t()],
122+
fragment: tuple(),
123+
private: map(),
124+
buffer: binary()
125+
}
11126
defstruct extensions: [],
12-
fragments: [],
127+
fragment: nil,
13128
private: %{},
14129
buffer: <<>>
15130

16131
@type error :: Mint.Types.error() | WebSocketError.t()
17132

133+
@typedoc """
134+
Shorthand notations for control frames
135+
136+
* `:ping` - shorthand for `{:ping, ""}`
137+
* `:pong` - shorthand for `{:pong, ""}`
138+
* `:close` - shorthand for `{:close, 1_000, ""}`
139+
140+
These may be passed to `encode/2`.
141+
142+
<!--
143+
Note that the shorthand notations may be passed to `encode/2`
144+
but frames returned from `decode/2` will never be in
145+
shorthand format.
146+
-->
147+
"""
148+
@type shorthand_frame :: :ping | :pong | :close
149+
150+
@typedoc """
151+
A WebSocket frame
152+
153+
* `{:binary, binary}` - a frame containing binary data. Binary frames
154+
can be used to send arbitrary binary data such as a PDF.
155+
* `{:text, text}` - a frame containing string data. Text frames must be
156+
valid utf8. Elixir has wonderful support for utf8: `String.valid?/1`
157+
can detect valid and invalid utf8.
158+
* `{:ping, binary}` - a control frame which the server should respond to
159+
with a pong. The binary data must be echoed in the pong response.
160+
* `{:pong, binary}` - a control frame which forms a reply to a ping frame.
161+
Pings and pongs may be used to check the a connection is alive or to
162+
estimate latency.
163+
* `{:close, code, reason}` - a control frame used to request that a connection
164+
be closed or to acknowledgee a close frame send by the server.
165+
166+
These may be passed to `encode/2` or returned from `decode/2`.
167+
168+
## Close frames
169+
170+
In order to close a WebSocket connection gracefully, either the client or
171+
server sends a close frame. Then the other endpoint responds with a
172+
close with code `1_000` and then closes the TCP connection. This can be
173+
accomplished in Mint.WebSocket like so:
174+
175+
```elixir
176+
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, :close)
177+
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
178+
179+
close_response = receive(do: (message -> message))
180+
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, close_response)
181+
{:ok, websocket, [{:close, 1_000, ""}]} = Mint.WebSocket.decode(websocket, data)
182+
183+
Mint.HTTP.close(conn)
184+
```
185+
186+
[rfc6455
187+
section 7.4.1](https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1)
188+
documents codes which may be used in the `code` element.
189+
"""
18190
@type frame ::
19-
{:text, binary()}
191+
{:text, String.t()}
20192
| {:binary, binary()}
21-
| :ping
22193
| {:ping, binary()}
23-
| :pong
24194
| {:pong, binary()}
25-
| :close
26195
| {:close, code :: non_neg_integer(), reason :: binary()}
27196

28197
@doc """
@@ -35,13 +204,49 @@ defmodule Mint.WebSocket do
35204
WebSocket-specific headers. For HTTP/2 connections, this function performs
36205
an extended CONNECT request which opens a stream to be used for the WebSocket
37206
connection.
207+
208+
## Options
209+
210+
* `:extensions` - a list of extensions to negotiate. See the extensions
211+
section below.
212+
213+
## Extensions
214+
215+
Extensions should be declared by passing the `:extensions` option in the
216+
`opts` keyword list. Note that in the WebSocket protocol, extensions are
217+
negotiated: the client proposes a list of extensions and the server may
218+
accept any (or none) of them. See `Mint.WebSocket.Extension` for more
219+
information about extension negotiation.
220+
221+
Extensions may be passed as a list of `Mint.WebSocket.Extension` structs
222+
or with the following shorthand notations:
223+
224+
* `module` - shorthand for `{module, []}`
225+
* `{module, params}` - shorthand for `{module, params, []}`
226+
* `{module, params, opts}` - a shorthand which is expanded to a
227+
`Mint.WebSocket.Extension` struct
228+
229+
## Examples
230+
231+
```elixir
232+
{:ok, conn} = Mint.HTTP.connect(:http, "localhost", 9_000)
233+
{:ok, conn, ref} =
234+
Mint.WebSocket.upgrade(conn, "/", [], extensions: [Mint.WebSocket.PerMessageDeflate])
235+
# or provide params:
236+
{:ok, conn, ref} =
237+
Mint.WebSocket.upgrade(
238+
conn,
239+
"/",
240+
[],
241+
extensions: [{Mint.WebSocket.PerMessageDeflate, [:client_max_window_bits]]}]
242+
)
243+
```
38244
"""
39245
@spec upgrade(
40246
conn :: Mint.HTTP.t(),
41247
path :: String.t(),
42248
headers :: Mint.Types.headers(),
43-
# maybe t:Keyword.t/0, will hold extensions in the future
44-
opts :: list()
249+
opts :: Keyword.t()
45250
) :: {:ok, Mint.HTTP.t(), Mint.Types.request_ref()} | {:error, Mint.HTTP.t(), error()}
46251
def upgrade(conn, path, headers, opts \\ [])
47252

@@ -83,6 +288,24 @@ defmodule Mint.WebSocket do
83288
{:error, conn, %WebSocketError{reason: :extended_connect_disabled}}
84289
end
85290

291+
@doc """
292+
Creates a new WebSocket data structure given the server's reply to the
293+
upgrade request
294+
295+
This function will setup any extensions accepted by the server using
296+
the `c:Mint.WebSocket.Extension.init/2` callback.
297+
298+
## Examples
299+
300+
```elixir
301+
http_reply = receive(do: (message -> message))
302+
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, headers}, {:done, ^ref}]} =
303+
Mint.HTTP.stream(conn, http_reply)
304+
305+
{:ok, conn, websocket} =
306+
Mint.WebSocket.new(conn, ref, status, resp_headers)
307+
```
308+
"""
86309
@spec new(Mint.HTTP.t(), reference(), pos_integer(), Mint.Types.headers()) ::
87310
{:ok, Mint.HTTP.t(), t(), [Mint.Types.response()]} | {:error, Mint.HTTP.t(), error()}
88311
def new(%Mint.HTTP1{} = conn, _request_ref, status, _response_headers)
@@ -114,10 +337,42 @@ defmodule Mint.WebSocket do
114337
{:error, conn, %WebSocketError{reason: :connection_not_upgraded}}
115338
end
116339

117-
@spec encode(t(), frame()) :: {:ok, t(), binary()} | {:error, t(), any()}
118-
defdelegate encode(websocket, data), to: Frame
340+
@doc """
341+
Encodes a frame into a binary
342+
343+
The resulting binary may be sent with `Mint.HTTP.stream_request_body/3`.
344+
345+
This function will invoke the `c:Mint.WebSocket.Extension.encode/2` callback
346+
for any accepted extensions.
347+
348+
## Examples
119349
120-
@spec decode(t(), data :: binary()) :: {:ok, t(), [frame()]} | {:error, t(), any()}
350+
```elixir
351+
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
352+
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
353+
```
354+
"""
355+
@spec encode(t(), shorthand_frame() | frame()) :: {:ok, t(), binary()} | {:error, t(), any()}
356+
defdelegate encode(websocket, frame), to: Frame
357+
358+
@doc """
359+
Decodes a binary into a list of frames
360+
361+
The binary may received from the connection with `Mint.HTTP.stream/2`.
362+
363+
This function will invoke the `c:Mint.WebSocket.Extension.decode/2` callback
364+
for any accepted extensions.
365+
366+
## Examples
367+
368+
```elixir
369+
message = receive(do: (message -> message))
370+
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
371+
{:ok, websocket, frames} = Mint.WebSocket.decode(websocket, data)
372+
```
373+
"""
374+
@spec decode(t(), data :: binary()) ::
375+
{:ok, t(), [frame() | {:error, term()}]} | {:error, t(), any()}
121376
defdelegate decode(websocket, data), to: Frame
122377

123378
# we re-open the request in the conn for HTTP1 connections because a :done

0 commit comments

Comments
 (0)