Protokół MQTT #3: implementacja klienta

mqtt przyklad

Przyszedł czas na implementację klienta MQTT, a dokładnie publishera. Jak wiadomo z poprzedniego wpisu wykorzystamy w tym celu bibliotekę Paho MQTT Client napisaną w języku C.

Wprowadzenie

Na potrzeby tego wpisu wykorzystamy jedną z przykładowych aplikacji klienta umożliwiającej wysyłanie wiadomości. Ten jak i kilka innych dostępnych przykładów znajdziesz w folderze paho.mqtt.c/src/samples. Kod, po drobnych modyfikacjach, wygląda następująco:

Jak widać, w przykładzie zostało wykorzystane API standardowe, synchroniczne, dlatego dołączony został plik nagłówkowy MQTTClient.h. Oprócz tego, należy jeszcze pamiętać o podlinkowaniu biblioteki paho-mqtt3c. Kod klienta można podzielić na trzy podstawowe części: create, connect i publish/subscribe.

Create

Na początku definiujemy uchwyt do klienta:

Następnie go tworzymy za pomocą funkcji MQTTClient_create():

Jako pierwszy argument przekazujemy adres do utworzonego wcześniej uchwytu.
Kolejnym argumentem jest string zawierający adres brokera, z którym klient będzie się łączył. Powinien on mieć następujący format: „protocol://host:port”. W tym wypadku będziemy się łączyli z brokerem postawionym na localhoscie, więc makro ma postać:

Trzecim argumentem jest string zawierający ID klienta, na podstawie którego broker będzie go identyfikował. Powinien on być unikalny dla każdego klienta. W tym przypadku też wykorzystane zostało makro:

W kolejnym argumencie mamy możliwość przekazania jednego z trzech bibliotecznych makr. W tym przykładzie wykorzystane zostało MQTTCLIENT_PERSISTENCE_NONE, co powoduje, że w przypadku gdy klient padnie to aktualny stan przesyłanych wiadomości zostanie utracony i wiadomości mogą nie zostać doręczone, nawet przy QoS ustawionym na wartość 1 lub 2. W przypadku wyboru MQTTCLIENT_PERSISTENCE_DEFAULT wszystkie wiadomości, które są aktualnie przesyłane są także przechowywane na dysku. Dzięki czemu, w sytuacji gdy klient padnie (aplikacja się zawiesi), możliwe będzie jego odtworzenie. Wiadomości, które były w trakcie przesyłania także zostaną odczytane i klient będzie mógł rozpocząć swoją pracę w kontekście, w którym nastąpiła awaria. Ostatni argument jest zależny od poprzedniego i w związku z tym, że w poprzednim wybraliśmy MQTTCLIENT_PERSISTENCE_NONE to w tym należy wpisać NULL. Szczegółowe informacje na temat dwóch ostatnich argumentów można znaleźć w dokumentacji biblioteki.

Connect

Aby nawiązać połączenie z brokerem, klient musi mu przesłać zdefiniowany w standardzie pakiet CONNECT. Warto dodać, że wiele przesyłanych w pakiecie informacji, jak np. login czy hasło jest opcjonalnych. Jak wyglądają poszczególne ramki zobaczyć można w dokumentacji protokołu. Nie musimy jednak jej dogłębnie analizować, ponieważ biblioteka udostępnia nam gotowe funkcje i struktury, umożliwiające wprowadzenie konkretnych ustawień. Do nawiązania połączenia wykorzystywana jest funkcja MQTTClient_connect():

Jak widać, przyjmuje ona dwa argumenty. Pierwszym jest uchwyt naszego klienta, a drugim jest adres do struktury, w której przechowywane są wszystkie ustawienia.

Struktura jest na początku inicjalizowana standardowymi ustawieniami:

Jest to dość istotne, ponieważ dla elementów struktury nie są zdefiniowane wartości domyślne, więc w przypadku zmiennych automatycznych przyjmują one wartości losowe, a w przypadku zmiennych statycznych wypełnione są zerami, co będzie powodowało niepoprawną pracę klienta.

Po zainicjalizowaniu zmiennej, ustawiane są interesujące nas pola:

Za pomocą pola keepAliveInterval ustawiamy okres czasu (w sekundach), co jaki klient ma wysyłać do brokera wiadomość pingującą. Broker na każdą z nich powinien odpowiedzieć wiadomością potwierdzającą. Taki mechanizm umożliwia obu stronom określenie czy strona przeciwna jest ciagle osiągalna.

Pole cleansession może przyjmować wartości 0 lub 1 i służy do kontroli zachowania klienta i brokera w czasie łączenia i rozłączania. Gdy cleansession jest ustawione na wartość 1 to uruchamiana jest czysta sesja. Wszystkie informacje z poprzedniej sesji zostaną wyczyszczone oraz broker nie będzie przechowywał dla klienta informacji z aktualnej sesji. W przypadku ustawienia cleansession na 0 zostanie odtworzona poprzednia sesja, a jeśli taka nie istnieje to zostanie utworzona nowa.

Struktura ma jeszcze wiele innych pól. Możliwe jest np. przesłania loginu i hasła na potrzeby uwierzytelnienia. Należy jednak mieć na uwadze, że jeśli nie korzystamy z protokołu SSL to dane przesyłane są jako tekst jawny. Dokładny opis wszystkich pól struktury można znaleźć tutaj.

Po przesłaniu przez klienta pakietu CONNECT broker zawsze odpowiada pakietem odpowiedzi CONNACK, w którym zawarty jest odpowiedni kod. W związku z tym, funkcja MQTTClient_connect() także zwraca różne wartości: albo flagę sukcesu albo kod błędu.

Publish

Przechodzimy teraz do wysyłania wiadomości 🙂 W tym celu wykorzystywana jest funkcja MQTTClient_publishMessage() posiadająca cztery parametry:

Pierwszym przekazywanym argumenetem, podobnie jak w przypadku MQTTClient_connect() jest uchwyt klienta. Drugim jest string zawierający nazwę tematu. W tym przypadku wykorzystane zostało makro:

Kolejnym jest adres do struktury przechowującej treść wiadomości oraz różne jej właściwości, a ostatnim – adres do tzw. tokena dostarczenia, który jest wykorzystywany do przechowywania informacji o tym czy wiadomość została dostarczona z sukcesem.

Struktura, podobnie jak w przypadku conn_opts, jest na początku inicjalizowana standardowymi ustawieniami, po czym ustawiane są interesujące nas pola, np.:

Do pola payload przekazywany jest adres do stringa zawierającego treść wiadomości. W tym przypadku wykorzystane zostało makro:

Do pola payloadlen przekazywana jest długość wiadomości, do qos wartość quality of service, o którym dokładniej pisałem w pierwszym wpisie tej serii. Pole retained może przyjmować wartość 0 lub 1. Ustawienie jedynki będzie informowało broker, że ma przechowywać kopię tej wiadomości. Dokładny opis wszystkich pól struktury można znaleźć tutaj.

Gdy wysyłamy jakąś wiadomość z QoS1 lub QoS2 i chcemy mieć pewność, że wiadomość została dostarczona to w związku z tym, że klient działa w trybie synchronicznym konieczne jest jeszcze wywołanie MQTTClient_waitForCompletion(), której argumentami są uchwyt do klienta, token oraz timeout. Funkcja blokuje wykonywanie programu do momentu dostarczenia wiadomości lub upłynięcia timeoutu.

Na końcu programu następuje rozłączenie i usunięcie klienta (zwolnienie pamięci):

Drugim argumentem przekazywanym do funkcji MQTTClient_disconnect() jest wartość timeoutu, który jest wykorzystywany po to, aby umożliwić klientowi ewentualne dokończenie przesyłania wiadomości.

Czas na testy!

No i to wszystko. Czas na kompilację i testy. Na początku uruchamiamy program brokera i klienta subskrybującego., tak jak to robiliśmy w pierwszym wpisie. Jedyne o czym należy pamiętać to o wpisaniu prawidłowej nazwy tematu i adresu IP brokera. Przypominam, że jeżeli klient i broker są uruchomione na tej samej maszynie to nie trzeba nawet ustawiać flagi -h, wystarczy taki zapis:

Gdy wszystko jest gotowe, można uruchomić napisany program 🙂 Wiadomość powinna wyświetlić się w konsoli subscribera:

Podsumowanie

No i pierwszy program mamy za sobą 🙂 Napisanie innego klienta nie powinno stanowić większego problemu. Jak wspominałem wcześniej, w pobranym folderze, dostępnych jest jeszcze kilka innych przykładów, zarówno z API synchronicznym jak i asynchronicznym. Warto sobie je jeszcze dodatkowo przejrzeć 🙂 W razie potrzeby, całe dokumentacje biblioteki dla obu API, wraz z opisem wszystkich funkcji oraz zmiennych są dostępne tutaj oraz tutaj.

To na razie tyle, jeśli chodzi o protokół MQTT. W przyszłości planuję jeszcze wpisy, w których przedstawię jak uruchomić bibliotekę na mikrokontrolerach, np. STM32, ale to dopiero za jakiś czas.