MQTT over WS Client

Overview

This tutorial demonstrates how Mongoose Library can be used to implement an MQTT client that connects to a broker over WebSocket.

Our actions will be the following:

  • Initiate a Websocket connection to the public HiveMQ MQTT broker ws://broker.hivemq.com:8000/mqtt
  • When connected, send an MQTT login message inside a WebSocket message. From now on, WebSocket messages will carry MQTT messages
  • Subscribe to the topic mg/test
  • Publish a hello message to the mg/test topic
  • Receive that message back from the subscribed topic and close the connection

The details of the WebSocket client can be gathered from its tutorial. For the MQTT client details, there is its tutorial too, which also implements an automatic reconnection functionality.

API

From the MQTT API, we'll use:

void mg_mqtt_login(struct mg_connection *c, const char *url,
                   struct mg_mqtt_opts *opts);
int mg_mqtt_parse(const uint8_t *buf, size_t len, uint8_t version, struct mg_mqtt_message *m);

The function mg_mqtt_login() tries to login to the MQTT broker whose URL is specified in parameter url, which will also contain any login credentials needed. We can indicate session and last will options in a structure pointed to by the opts parameter. Note that this function relies on a connection already established.

The function mg_mqtt_parse() tries to parse the buffer buf and fill the mg_mqtt_message structure m if the buffer contains a proper (and complete) MQTT message. It will signal if there is an error or we need to call it again when more data will arrive to the buffer. Note that parameter version is not present in releases 7.7 and earlier.

We'll also use mg_mqtt_pub() and mg_mqtt_sub() to publish and subscribe, as we described in the MQTT client tutorial.

For detailed information, see the documentation.

From the WebSocket API, we'll use:

size_t mg_ws_wrap(struct mg_connection *c, size_t len, int op)

The function mg_ws_wrap() converts data in the output buffer to the WebSocket format. Parameter len contains the payload length, while parameter op specifies the type of WebSocket message.

We'll also use mg_ws_connect() to connect to the server, as we described in the WebSocket client tutorial.

For detailed information, see the documentation.

Main function

The main() function is as usual, we initialize an event manager, create the WebSocket connection by calling mg_ws_connect() (more on this later), and start the event loop.

The event loop executes until a termination signal is received. After that, the mg_mgr_free() function is called to perform cleanup.

When we create a WebSocket client connection, we actually create a connection that sends an HTTP request with a Connection: Upgrade header and some others. Many brokers, like the one we use in this example, work in a port-based or URL-based fashion and will serve MQTT based on the port we connect to or the URL we request. Other brokers may not, for those we need to tell them that the protocol encapsulated inside WebSocket will be MQTT, hence we add an additional header: Sec-Websocket-Protocol: mqtt for those cases.

Event handler

Connection failures will trigger an MG_EV_ERROR event, an error reason is passed as a char * pointer in parameter ev_data; check the error handling tutorial for more information.

Here, on any error - for example, DNS lookup failure, we log that error:

When the TCP connection to the server is established, we initialise TLS if the URL is wss://:

Note that, for that to work, the application must be built with TLS enabled. By default, the HiveMQ URL is not TLS, so building with TLS is not required; more information on how to build can be found below.

When the WebSocket negotiation is complete and the connection has been upgraded, an MG_EV_WS_OPEN event is sent; we need to establish the MQTT connection so we login to the broker.

The connection triggers MG_EV_WS_MSG events for every WebSocket message received. Here we will parse those messages for valid MQTT messages:

First we expect an MQTT_CMD_CONNACK message in response to our login attempt, then we need to ask for the MG_EV_MQTT_OPEN event to be triggered, as this is what is expected when the MQTT server accepts us as a client in a non-WS connection.

If authorization is successful, we subscribe to the desired topic, and immediately publish to it. Note that we use the function mg_ws_wrap() to wrap each MQTT message with a proper WebSocket header for a binary message

Then we expect an MQTT_CMD_PUBLISH message, corresponding to the broker sending back the message we just sent, since we subscribed to the very same topic. We print the message and signal Mongoose to terminate the connection:

On error or connection closure, we signal there is time to exit; check the error handling tutorial for more information.

Build and run

  • Follow the Build Tools tutorial to setup your development environment.
  • Start a terminal in the project directory; if you've not already done so, clone the Mongoose Library repo
    git clone https://github.com/cesanta/mongoose
    
  • Build the example, this will also start the MQTT over WebSocket client:
    cd mongoose/examples/mqtt-over-ws-client
    make clean all
    
  • Observe the log
    273058bd 2 main.c:30:fn                 Connected to WS. Logging in to MQTT...
    273059ba 2 main.c:41:fn                 GOT 4 bytes WS msg
    273059ba 2 main.c:50:fn                 CONNECTED to ws://broker.hivemq.com:8000/mqtt
    273059ba 2 main.c:53:fn                 SUBSCRIBED to mg/test
    273059ba 2 main.c:56:fn                 PUBLISHED hello -> mg/test
    27305abb 2 main.c:41:fn                 GOT 5 bytes WS msg
    27305ac4 2 main.c:41:fn                 GOT 18 bytes WS msg
    27305ac4 2 main.c:67:fn                 RECEIVED hello <- mg/test
    

Build with TLS support

Check the "How to build" section of the TLS tutorial for specific information on building options for your OS

For more information on developing TLS clients, check the TLS tutorial