Skip to main content

Building an MQTT Client

·2 mins

The idea of writing my own MQTT client has always felt interesting and attainable. I know how the protocol works at a high level, and I’ve used various MQTT clients over the years, including AWS IoT SDK, MQTTX, and mosquitto.

After about a week of staring at the MQTT 3.1.1 spec, I built a client in C++ that can publish and subscribe to MQTT messages: https://github.com/jcosentino11/mqtt-client 🎉

mqtt sub --topic hello/topic --address broker.emqx.io:1883 --client-id mqttclient1
mqtt pub --topic hello/topic --address broker.emqx.io:1883 --client-id mqttclient2 '{"hello": "world"}'

It ended up being a great learning experience, espcially being a newcomer to C++ and lower-level networking (much of my career has been backend Java).

Here are some of my learnings (basic as they may be 🙂):

  • I started by binding an IPv6 socket to an IP address (since the broker I was testing with was binding to IPv6), but later found out about getaddrinfo, which took care of DNS resolution and figuring out IPv4 vs IPv6, which was very convenient. [commit]
struct addrinfo hints = {}, *addrs;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;

if (getaddrinfo(host.data(), port.data(), &hints, &addrs) != 0) {
    return;
}

for (struct addrinfo *addr = addrs; addr != NULL; addr = addr->ai_next) {
    mSock = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
    if (mSock == -1) {
        continue;
    }
    if (connect(mSock, addr->ai_addr, addr->ai_addrlen) < 0) {
        close(mSock);
        return;
    }
}
  • I learned how to build packets byte-by-byte from looking at MQTT spec. Here’s an example (adapted from Packet.cpp) of building a SUBSCRIBE packet:
std::vector<uint8_t> payload;
std::string topic = "example";

// FIXED HEADER
payload.push_back(0b10000010); // MQTT control packet type (8)
payload.push_back(2 + (3 + topic.size())); // remaining length
    
// VARIABLE HEADER
payload.push_back(0); // packet identifier MSB
payload.push_back(1234); // packet identifier LSB

// PAYLOAD
payload.push_back(0); // topic length MSB
payload.push_back(topic.size()); // topic length LSB
for (size_t i = 0; i < topic.size(); ++i) {
    payload.push_back(topic[i]);
}
payload.push_back(1); // QoS 1
  • For testing, wireshark was invaluable. Using the filter mqtt && tcp.port == 1883 on my loopback interface, I was able to compare packets sent from my client vs. a real one (such as MQTTX).

As I’m sure you already know, nothing beats hands-on learning, and this was no exception!