-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
37 changed files
with
1,115 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
# Connecting to a MQTT Broker | ||
|
||
Tortoise is capable of connecting to any MQTT broker that implements | ||
the 3.1.1 version of the MQTT protocol (support for MQTT 5 is | ||
planned). It does so by taking a connection specification, and with it | ||
will do its best to connect to the broker and keeping the connection | ||
open. | ||
|
||
A minimal connection specification looks like this: | ||
|
||
``` elixir | ||
{ok, _pid} = | ||
Tortoise.Connection.start_link( | ||
client_id: HelloWorld, | ||
server: {Tortoise.Transport.Tcp, host: "localhost", port: 1883}, | ||
handler: {Tortoise.Handler.Logger, []} | ||
) | ||
``` | ||
|
||
This will establish a TPC connection to a broker running on | ||
*localhost* port *1883*. The connection takes a module that implements | ||
the `Tortoise.Handler`-behaviour; In this case the | ||
`Tortoise.Handler.Logger` callback module, which will print a log | ||
statement on events happening during the connection life cycle. | ||
|
||
Furthermore, we specify that the `client_id` of the connection is | ||
`HelloWorld`. The client id can later be used to interact with the | ||
connection, such as publishing messages and subscribing to topics. | ||
|
||
Notice that this example expect a server configured to allow anonymous | ||
connections. Not all MQTT brokers are configured the same, so | ||
depending on the server more configuration options might be needed for | ||
a successful connection. This document aims to give an overview. | ||
|
||
## Network Transport | ||
|
||
Tortoise has an abstraction for the network transport, and comes | ||
with two official implementations included in Tortoise itself: | ||
|
||
- `Tortoise.Transport.Tcp` used to connect to a broker via | ||
TCP. While this transport is the simplest to use it is also the | ||
least secure, and should only be used on trusted networks. It is | ||
based on `:gen_tcp` found in the Erlang/OTP distribution. | ||
|
||
- `Tortoise.Transport.SSL` used to create a secure connection via | ||
secure socket layer. This option takes a bit more work to setup, | ||
but it will prevent people from eavesdropping on the data being | ||
sent between the client and the broker. | ||
|
||
The transports are given with the `server` field in the connection | ||
specification as a tuple, containing a the transport type and an | ||
options list specific to the given transport. | ||
|
||
`Tortoise.Transport.Tcp` takes two options; the host name as a string, | ||
such as `"localhost"` or a four-tuple describing an IP-network | ||
address, such as `{127, 0, 0, 1}`. An example where the TCP transport | ||
is used can be seen in the introduction to this article. | ||
|
||
The `Tortoise.Transport.SSL` is a bit more versatile in its | ||
configuration options. It is based on the `:ssl` module from the | ||
Erlang distribution, so be sure to check the documentation for the | ||
`:ssl` module for detailed information on the possible configuration | ||
options. | ||
|
||
*[todo, describe SSL connection better when I have internet]* | ||
|
||
Information on creating a custom transport can be found in the | ||
`Tortoise.Transport` module, but for most cases the TCP and SSL module | ||
should suffice. | ||
|
||
## Connection Handler | ||
|
||
A handful of events are possible during a client life cycle. Tortoise | ||
aim to expose the interesting events as callback functions, defined in | ||
the `Tortoise.Handler`-behaviour, making it possible to implement | ||
custom behavior for the client. The exposed events are: | ||
|
||
- The client is initialized, or terminated allowing for | ||
initialization and tear down of subsystems | ||
- A connection to the server is establish | ||
- The connection to the server is dropped | ||
- The subscription status to a given topic filter is changed | ||
- A message is received on one of the subscribed topic filters | ||
|
||
Read more about defining custom behavior for a connection in the | ||
documentation for the `Tortoise.Handler`-module. | ||
|
||
## The `client_id` | ||
|
||
In MQTT the clients announce themselves to the broker with what is | ||
referred to as a *client id*. Two clients cannot share the same client | ||
id on a broker, and depending on the implementation (or configuration) | ||
the server will either kick the first client, or deny the new client | ||
if it specifies a client id already in use. | ||
|
||
The protocol specifies that a valid client id is between 1 and 23 | ||
UTF-8 encoded bytes in length, but some server configurations may | ||
allow for longer ids; thus tortoise will allow for client identifiers | ||
longer than 23 bytes but some MQTT brokers might reject the | ||
connection. | ||
|
||
Allowed values are a string, or an atom. If an atom is specified it | ||
will be converted to a string when the connection message is send on | ||
the wire, but it will be possible to refer to the connection using the | ||
atom, which can be more convenient. Notice that the client id can | ||
easily reach the 23 bytes when converted from an atom because atoms | ||
starting with an uppercase letter will be prefixed with *Elixir.*; | ||
therefore `MyClientId` will be 17 bytes instead of the 10 one could | ||
expect. | ||
|
||
``` elixir | ||
iex(1)> client_id = Atom.to_string(MyClientId) | ||
"Elixir.MyClientId" | ||
iex(2)> byte_size(client_id) | ||
17 | ||
``` | ||
|
||
The specified client identifier is used to identify a connection when | ||
publishing messages, subscribing to topics, or otherwise interacting | ||
with the named connection. | ||
|
||
``` elixir | ||
{ok, _pid} = | ||
Tortoise.Connection.start_link( | ||
client_id: MyClient, | ||
server: {Tortoise.Transport.Tcp, host: "localhost", port: 1883}, | ||
handler: {Tortoise.Handler.Logger, []} | ||
) | ||
|
||
Tortoise.publish(MyClient, "foo/bar", "hello") | ||
``` | ||
|
||
**Notice**: Though the MQTT 3.1.1 protocol allow for a zero-byte | ||
client id—in which case the server should assign a random `client_id` | ||
for the connection—a client id is enforced in Tortoise. This is | ||
done so the connection has an identifier that can be used when | ||
interacting with the connection. | ||
|
||
## User Name and Password | ||
|
||
Some brokers are configured to require some basic authentication, | ||
which will determine whether a user is allowed to subscribe or publish | ||
to a given topic, and some set limitations to what quality of service | ||
a particular user, or group of users, are allowed to subscribe or | ||
publish with. | ||
|
||
To specify a user name and password for a connection the aptly named | ||
*user_name* and *password* comes in handy. Both of them take UTF-8 | ||
encoded strings, or `nil` as their value, in which case an anonymous | ||
connection is attempted. Depending on the broker configuration it is | ||
allowed to specify a user name and omit the password, but the user | ||
name has to be specified if a password is specified. | ||
|
||
Both default to `nil` if left blank. | ||
|
||
## The keep alive interval | ||
|
||
When connected a MQTT client should ping the server on a set interval | ||
to let the broker know that it is still alive. The keep alive value is | ||
given as an integer, describing time in seconds between keep alive | ||
messages, and should be set depending on factors such as power | ||
consumption, network bandwidth, etc. Per default Tortoise will sent a | ||
keep alive message every 60 seconds, which is a reasonable value for | ||
most installations. The allowed maximum value is `65_535`, which is 18 | ||
hours, 12 minutes, and 15 seconds; most would consider this a bit too | ||
extreme, and some brokers might reject connections specifying a too | ||
long `keep_alive` interval. | ||
|
||
Some brokers allow disabling the keep alive interval by setting it to | ||
zero, so `Tortoise` allow for a `keep_alive` specified as `0`. Note | ||
that the broker can still choose to disconnect a given client on the | ||
grounds of inactivity. When `keep_alive` is disabled the broker | ||
implementation will decide its own measure of inactivity, so to avoid | ||
unspecified behavior it is advised to use a keep alive value. | ||
|
||
## Last will message | ||
|
||
It is possible to specify a message which should be dispatched by the | ||
broker if the client is disconnected from the broker abruptly. This | ||
message is known as the last will message, and allow for other | ||
connected clients to act on other clients leaving the broker. | ||
|
||
The last will message is specified as part of the connection, and for | ||
Tortoise it is possible to configure a last will message by passing in | ||
a `Tortoise.Package.Publish` struct to the *will* connection | ||
configuration field. | ||
|
||
``` elixir | ||
{:ok, pid} = | ||
Tortoise.Connection.start_link( | ||
client_id: William, | ||
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883}, | ||
handler: {Tortoise.Handler.Logger, []}, | ||
will: %Tortoise.Package.Publish{topic: "foo/bar", payload: "goodbye"} | ||
) | ||
``` | ||
|
||
If we have another client connected to the broker, subscribing to | ||
*foo/bar*, we should now receive a message containing the message | ||
*goodbye* on that topic, should the client called *William* disconnect | ||
abruptly from the broker. We can simulate this by terminating the *pid* | ||
using `Process.exit(pid, :ouch)`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
# Connection Supervision | ||
|
||
An important aspect of building an Elixir application is setting up a | ||
supervision structure that ensure the application will continue | ||
working if parts of the system should reach an erroneous state and | ||
need to get restarted into a known working state. To do this one need | ||
to group the processes the application consist of in a manner such | ||
that processes belonging together will start and terminate together. | ||
|
||
`Tortoise` offers multiple ways of supervising one or multiple | ||
connections; by using the provided dynamic `Tortoise.Supervisor` or | ||
starting a dynamic supervisor belonging to the application using the | ||
connections; or by starting the connections needed directly in an | ||
application supervisor. This document will describe the ways of | ||
supervision, and give an overview for when to use a given supervision | ||
strategy. | ||
|
||
|
||
## Linked Connection | ||
|
||
A connection can be started and linked to the current process by using | ||
the `Tortoise.Connection.start_link/1` function. | ||
|
||
``` elixir | ||
Tortoise.Connection.start_link( | ||
client_id: HeartOfGold, | ||
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883}, | ||
handler: {Tortoise.Handler.Logger, []} | ||
) | ||
``` | ||
|
||
As with any other linked process both process will terminate if either | ||
terminate, as described in the `Process.link/1` documentation. This | ||
mean that any stored state in the process that own the MQTT connection | ||
will disappear with the process if the connection process | ||
terminates. Therefore it is not recommended to link a connection | ||
process like this outside of experimenting in `IEx`, but instead run | ||
it inside of a supervisor process. When properly supervised connection | ||
terminations the crash will be contained, allowing the other processes | ||
to keep their state. | ||
|
||
|
||
## Supervising a connection | ||
|
||
The `Tortoise.Connection` module provides a `child_spec/1` which makes | ||
it easier to start a `Tortoise.Connection` as part of a supervisor by | ||
simply passing a `{Tortoise.Connection, connection_specification}` to | ||
the supervisor child list. | ||
|
||
``` elixir | ||
defmodule MyApp.Supervisor do | ||
use Supervisor | ||
|
||
def start_link(opts) do | ||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__) | ||
end | ||
|
||
@impl true | ||
def init(_opts) do | ||
children = [ | ||
{Tortoise.Connection, | ||
[ | ||
client_id: WombatTaskForce, | ||
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883}, | ||
handler: {Tortoise.Handler.Logger, []} | ||
]} | ||
] | ||
|
||
Supervisor.init(children, strategy: :one_for_one) | ||
end | ||
end | ||
``` | ||
|
||
The great thing about this approach is that the connection can live in | ||
the same supervision tree as the rest of the application that depend | ||
on that connection. The connection is started, restarted, and stopped | ||
with the application as a whole, ensuring the connection is closed | ||
with the processes that depend on it. | ||
|
||
Be sure to set a reasonable connection strategy for the | ||
supervisor. Refer to the `Supervisor` documentation for more | ||
information on usage and configuration. | ||
|
||
|
||
## The `Tortoise.Supervisor` | ||
|
||
When `Tortoise` is included as a dependency in the *mix.exs*-file of | ||
an application `Tortoise` will automatically get started along the | ||
application. During the application start up a dynamic supervisor will | ||
spawn and register itself under the name `Tortoise.Supervisor`. This | ||
can be used to start supervised connections that will get restarted if | ||
they are terminated with an abnormal reason. | ||
|
||
To start a connection on the `Tortoise.Supervisor` one can use the | ||
`Tortoise.Supervisor.start_child/2` function, which defaults to using | ||
the dynamic supervisor registered under the name | ||
`Tortoise.Supervisor`. | ||
|
||
``` elixir | ||
Tortoise.Supervisor.start_child( | ||
client_id: "heart-of-gold", | ||
handler: {Tortoise.Handler.Logger, []}, | ||
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883} | ||
) | ||
``` | ||
|
||
This is an easy and convenient way of getting started, as everything | ||
needed to supervise a connection is there when the `Tortoise` | ||
application has been initialized. One downside is that, while the | ||
children are supervised they are not grouped with the application that | ||
need the connections; they are grouped with the `Tortoise` | ||
application. To mitigate this a `Tortoise.Supervisor.child_spec/1` | ||
function is available, which can be used to start the | ||
`Tortoise.Supervisor` as part of another supervisor. | ||
|
||
``` elixir | ||
defmodule MyApp.Supervisor do | ||
use Supervisor | ||
|
||
def start_link(opts) do | ||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__) | ||
end | ||
|
||
@impl true | ||
def init(_opts) do | ||
children = [ | ||
{Tortoise.Supervisor, | ||
[ | ||
name: MyApp.Connection.Supervisor, | ||
strategy: :one_for_one | ||
]} | ||
] | ||
|
||
Supervisor.init(children, strategy: :one_for_one) | ||
end | ||
end | ||
``` | ||
|
||
Connections can now, dynamically, be attached to the supervised | ||
`Tortoise.Supervisor` by calling the | ||
`Tortoise.Supervisor.start_child/2` function and specifying the name | ||
that was given to the supervisor, in this case | ||
*MyApp.Connection.Supervisor*. | ||
|
||
``` elixir | ||
Tortoise.Supervisor.start_child( | ||
MyApp.Connection.Supervisor, | ||
client_id: SmartHose, | ||
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883}, | ||
handler: {Tortoise.Handler.Logger, []} | ||
) | ||
``` | ||
|
||
This is the best way of supervising a dynamic set of connections, but | ||
might be overkill if only one, static connection is needed for the | ||
application. | ||
|
||
|
||
## Summary | ||
|
||
`Tortoise` makes it possible to spawn connections and supervise them, | ||
and it is always best practice to supervise a connection to ensure it | ||
remains up. Different approaches can be taken depending on the | ||
situation: | ||
|
||
* If a fixed amount of connections are needed the recommended way is | ||
to attach them directly to a supervision tree, along with the | ||
processes that depend on said connections using the | ||
`Tortoise.Connection.child_spec/1` | ||
|
||
* If a dynamic set of connections are needed the recommended way is | ||
to spawn a named `Tortoise.Supervisor` as part of a supervisor, | ||
which hold the processes that depend on the connections, and spawn | ||
the connections on the dynamic supervisor. | ||
|
||
Supervising the connections along the processes that rely on the | ||
connection ensure that the application can be started and stopped as a | ||
whole, and makes it possible to recover from faulty state. |
Oops, something went wrong.