Minch behaviour (Minch v0.2.0)
View SourceA WebSocket client build on top of Mint.WebSocket
.
Features
- Reconnects with backoff
- Use
Minch.backoff/2
to calculate an exponential backoff duration.
- Use
- Handling control frames
- Closes the connection after receiving a
:close
frame. - Replies to server pings automatically.
- Closes the connection after receiving a
Installation
The package can be installed by adding minch
to your list of dependencies in mix.exs
:
def deps do
[
{:minch, "~> 0.2.0"}
]
end
Usage
Basic example
defmodule EchoClient do
use Minch
@impl true
def connect(state) do
state.url
end
@impl true
def handle_frame(frame, state) do
IO.inspect(frame)
{:ok, state}
end
end
{:ok, pid} = Minch.start_link(EchoClient, %{url: "wss://ws.postman-echo.com/raw"})
Minch.send_frame(pid, :ping)
Supervised client
defmodule EchoClient do
use Minch
require Logger
def start_link(init_arg) do
Minch.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
{:ok, %{connected?: false}}
end
@impl true
def connect(_state) do
url = "wss://ws.postman-echo.com/raw"
headers = [{"authorization", "bearer: example"}]
# don't do this in production
options = [transport_opts: [{:verify, :verify_none}]]
{url, headers, options}
end
@impl true
def handle_connect(_response, state) do
Logger.info("connected")
Process.send_after(self(), :produce, 5000)
{:reply, {:text, "welcome"}, %{state | connected?: true}}
end
@impl true
def handle_disconnect(reason, attempt, state) do
Logger.warning("disconnected: #{inspect(reason)}")
{:reconnect, Minch.backoff(attempt), %{state | connected?: false}}
end
@impl true
def handle_info(:produce, state) do
Process.send_after(self(), :produce, 5000)
{:reply, {:text, DateTime.utc_now() |> DateTime.to_iso8601()}, state}
end
@impl true
def handle_frame(frame, state) do
Logger.info(inspect(frame))
{:ok, state}
end
end
Simple client
url = "wss://ws.postman-echo.com/raw"
headers = []
# don't do this in production
options = [transport_opts: [{:verify, :verify_none}]]
IO.puts("checking ping to #{url}...")
case Minch.connect(url, headers, options) do
{:ok, pid, ref} ->
Minch.send_frame(pid, {:text, to_string(System.monotonic_time())})
case Minch.receive_frame(ref, 5000) do
{:text, start} ->
ping =
System.convert_time_unit(
System.monotonic_time() - String.to_integer(start),
:native,
:millisecond
)
IO.puts("#{ping}ms")
:timeout ->
IO.puts("timeout")
end
Minch.close(pid)
{:error, error} ->
IO.puts("connection error: #{inspect(error)}")
end
Summary
Callbacks
Invoked to retrieve the connection details.
Invoked to handle a successful connection.
Invoked to handle a disconnect from the server or a failed connection attempt.
Invoked to handle internal errors.
Invoked to handle an incoming WebSocket frame.
Invoked to handle info messages.
Invoked when the client process is started.
Invoked when the client process is about to exit.
Types
@type client() :: GenServer.server()
@type frame() :: Mint.WebSocket.frame() | Mint.WebSocket.shorthand_frame()
@type response() :: %{status: Mint.Types.status(), headers: Mint.Types.headers()}
@type state() :: term()
Callbacks
@callback connect(state :: state()) :: url | {url, headers} | {url, headers, options} when url: String.t() | URI.t(), headers: Mint.Types.headers(), options: Keyword.t()
Invoked to retrieve the connection details.
See options for Mint.HTTP.connect/4
and Mint.WebSocket.upgrade/5
.
@callback handle_connect(response(), state()) :: callback_result()
Invoked to handle a successful connection.
@callback handle_disconnect(reason :: term(), attempt :: pos_integer(), state()) :: {:reconnect, backoff :: pos_integer(), state()} | {:stop, reason :: term(), state()}
Invoked to handle a disconnect from the server or a failed connection attempt.
Returning {:reconnect, backoff, state}
will schedule a reconnect after backoff
milliseconds.
@callback handle_error(error :: term(), state()) :: callback_result()
Invoked to handle internal errors.
@callback handle_frame(Mint.WebSocket.frame(), state()) :: callback_result()
Invoked to handle an incoming WebSocket frame.
@callback handle_info(message :: term(), state()) :: callback_result()
Invoked to handle info messages.
Invoked when the client process is started.
@callback terminate(reason, state :: state()) :: term() when reason: :normal | :shutdown | {:shutdown, term()} | term()
Invoked when the client process is about to exit.
Functions
@spec backoff(non_neg_integer(), min: pos_integer(), max: pos_integer(), factor: pos_integer() ) :: non_neg_integer()
Calculates an exponential backoff duration.
It accepts the following options:
:min
- The minimum backoff duration in milliseconds. Defaults to200
.:max
- The maximum backoff duration in milliseconds. Defaults to30_000
.:factor
- The exponent to apply to the attempt number. Defaults to2
.
Examples
iex(8)> for attempt <- 1..20, do: Minch.backoff(attempt)
[200, 800, 1800, 3200, 5000, 7200, 9800, 12800, 16200, 20000, 24200, 28800,
30000, 30000, 30000, 30000, 30000, 30000, 30000, 30000]
for attempt <- 1..20, do: Minch.backoff(attempt, min: 100, max: 10000, factor: 1.5)
[100, 283, 520, 800, 1118, 1470, 1852, 2263, 2700, 3162, 3648, 4157, 4687, 5238,
5809, 6400, 7009, 7637, 8282, 8944]
@spec close(pid()) :: :ok
Closes the connection opened by connect/3
.
@spec connect(String.t() | URI.t(), Mint.Types.headers(), Keyword.t()) :: {:ok, pid(), Mint.Types.request_ref()} | {:error, Mint.WebSocket.error()}
Connects to a WebSocket server.
Once connected, the function will return the connection's PID and the request reference.
Use the send_frame/2
function to send WebSocket frames to the server.
Incoming WebSocket frames will be sent to the caller's mailbox as a tuple {:frame, request_ref, frame}
.
You can use the receive_frame/2
function to receive these frames.
Use Process.monitor/1
to get notified when the connection is closed by the server.
Example
iex> {:ok, pid, ref} = Minch.connect("wss://ws.postman-echo.com/raw", [], transport_opts: [{:verify, :verify_none}])
{:ok, _pid, _ref}
iex> Minch.send_frame(pid, {:text, "hello"})
:ok
iex> Minch.receive_frame(ref, 5000)
{:text, "hello"}
iex> Minch.close(pid)
:ok
@spec receive_frame(Mint.Types.request_ref(), timeout()) :: Mint.WebSocket.frame() | :timeout
Receives an incoming WebSocket frame.
See connect/3
.
@spec send_frame(client(), Mint.WebSocket.frame() | Mint.WebSocket.shorthand_frame()) :: :ok | {:error, term()}
Sends a WebSocket frame.
@spec start_link(module(), term(), GenServer.options()) :: GenServer.on_start()
Starts a Minch
client process linked to the current process.