Projet MQTT
Le but de ce projet est de programmer un client et un serveur MQTT, conformément à un sous-ensemble (de taille raisonnable) de la spécification 3.1.
Présentation
MQTT (Message Queuing Telemetry Transport) est un protocole de publication de donnéees très utilisé notamment dans le domaine de l'IoT (Internet of Things) : typiquement 1 ou plusieurs capteurs (appelés publishers) publient régulièrement des mesures sur un serveur (appelé broker) qui se charge ensuite de faire suivre ces mesures à différents clients (appelés subscribers).
Vidéo d'introduction rapide à MQTT (François Riotte)
On trouve sur le web de nombreuses implémentations complètes et open source de ce protocole. Mosquitto est une de ces implémentations. Elle est déjà disponible sur les machines du CREMI.
Démo
Commençons par un exemple d'utilisation de Mosquitto.
Voici les trames échangées lors de la démo ci-dessus, à analyser avec Wireshark : mqtt.pcap.
Dans cette démo, le subscriber, le publisher et le broker s'exécutent tous
les trois sur la machine locale (127.0.0.1
), respectivement avec les ports
35634, 35636, et 1883.
- Les trames 4, 6, 8, 10 contiennent les paquets MQTT échangés entre le subscriber et le broker au moment du démarrage du subscriber.
- Les trames 15, 17, 19, 23 contiennent les paquets MQTT échangés entre le publisher et le broker au moment de la publication de la première témpérature "24".
- La trame 21 contient le paquet MQTT que le broker envoie au subscriber lors de la diffusion de la température "24".
Vous trouverez ici une description suffisamment précise sans être trop longue du format utilisé pour les paquets MQTT.
Objectif
L'objectif pédagogique de ce projet est double :
- vous amenez à lire et comprendre une spécification,
- vous amenez à produire une implémentation qui respecte un protocole.
Pour cela nous vous demandons d'implémenter en Python 3 un publisher, un subscriber et un broker offrant un sous-ensemble des fonctionnalités de MQTT tout en respectant strictement la spécification 3.1.1 du protocole. Le code que vous produirez devra être ""lisible, bien structuré, un minimum commenté et un minimum testé"". En passant un peu de temps à vous appliquer sur la qualité de votre code, vous en gagner beaucoup en temps de débugage.
Fonctionnalités
MQTT offre 3 niveaux de Qualité de Service (QoS en anglais) : de 0 la plus simple à 2 la plus sophistitquée. On ne considérera dans ce projet que la QoS 0.
Le protocole supporte de paquets de tailles relativement grandes. On supposera ici que tous les paquets ont une taille inférieure à 127 octets, et donc que la longueur des paquets sera codé sur 1 seul octet.
Par ailleurs, le protocole MQTT permet aussi de gérer l'authentification des
clients. Là encore on considérera uniquement une version sans authentification.
En revanche, dans sa version la plus aboutie, vos clients et serveurs doivent
supporter l'option retain
. Ainsi les seuls paquets que vous avez à considérer
sont ceux de type : CONNECT
, CONNACK
, PUBLISH
, SUBSCRIBE
, SUBACK
et
DISCONNECT
.
Voici les différents jalons à respecter :
1) Digérer la spécification du protocole pour en extraire les informations
pertinantes pour votre projet.
2) Se faire la main en implémentant une fonction create_mqtt_publish_msg
permettant de forger un paquet MQTT de type PUBLISH
. Votre fonction doit
renvoyer le paquet MQTT sous forme d'un byte array prêt à l'envoi avec la
fonction socket.sendall()
. (cf. activité VPL). Pour pouvoir être évaluée,
votre fonction doit être testée avec l'outil doctest (cf. Annexe).
3) Implémenter les fonctionnalités du publisher (sans l'option retain
).
4) Implémenter les fonctionnalités du subscriber.
5) Implémenter le broker en vous inspirant du serveur echo
(en version
select) vu en TP.
6) Modifier les implémentations du publisher et du broker pour gérer
l'option retain
.
Biblio
En complément, lisez attentivement les documents ci-dessous pour bien comprendre le fonctionnement du protocole MQTT :
- Wikipedia : https://en.wikipedia.org/wiki/MQTT
- Overview : http://www.steves-internet-guide.com/mqtt-protocol-messages-overview/
- Spécification 3.1 : http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html
- Liste de vidéos sur MQTT : https://www.youtube.com/watch?v=jTeJxQFD8Ak&list=PLRkdoPznE1EMXLW6XoYLGd4uUaB6wB0wd
Travail Demandé
Il s'agit d'un travail à réaliser en groupe de 2 étudiants.
Il est demandé de développer en langage Python 3 les programmes client (publisher+subscriber) et serveur (broker), conformément à la description du protocole MQTT fournies ci-dessus, en incluant les modifications demandées. On s'appuiera obligatoirement sur la bibliothèque socket et sur la couche de transport TCP pour réaliser ce projet.
Plus précisément, on demande de programmer les commandes ci-dessous en respectant la syntaxe suivante :
# server
$ ./mqtt-server.py -h
usage: mqtt-server [-h] [-p PORT] [-l LOG] [--debug]
Run the broker
optional arguments:
-h, --help show this help message and exit
-p PORT, --port PORT port used by the broker (default value: 1883)
-l LOG, --log LOG Filename to store log informations
--debug log debug informations
# client
$./mqtt-client -h
usage: mqtt-client [-h] [-H HOST] [-p PORT] [-i ID] [-l LOG] [--debug] [-r] -t TOPIC {pub,sub}
Run a publisher or a subsriber. Publisher read values (as strings) from the standard input and Subscriber
displays the values on the standard output.
positional arguments:
{pub,sub} indicate if the client is a publisher (pub) or a subscriber (sub)
optional arguments:
-h, --help show this help message and exit
-H HOST, --host HOST address of the broker (default value: localhost)
-p PORT, --port PORT port used by the broker (default value: 1883)
-i ID, --id ID client ID (default value: anonymous)
-l LOG, --log LOG Filename to store log informations
--debug log debug informations
-r, --retain Only for publisher: indicates that the values published must be retained by the
broker
-t TOPIC, --topic TOPIC
Voici une démo où votre publisher lit les valeurs saisies au clavier. Remarquez
dans la deuxième partie que le comportement du broker a changé lorsqu'on utilise
l'option -r
Voici une 2ième démo où votre publisher lit des valeurs à partir d'un fichier. Notez que le programme se termine correctement et sans erreur une fois toutes les valeurs lues.
La commande cliente permet de lancer un publisher
(option pub
) ou un
subscriber (option sub
).
Pour vous aider à démarrer ce projet, nous vous fournissons la coquille du projet, qui se compose de trois fichiers :
- le client mqtt-client.py ;
- le serveur mqtt-server.py ;
- le module mqtt.py, qui regroupe l'implémentation des fonctions du client et du serveur.
Cette coquille réalise juste l'analyse des options et des arguments passées à
ces commandes à l'aide du module argparse
. Vous ne devez pas modifier les
fichiers mqtt-client.py
et mqtt-server.py
.
N'essayez pas de réaliser tout le projet d'un coup, mais concentrez-vous sur les fonctionnalités principales, sans gérer les options ou les erreurs.
👉 Rendez votre travail sur Moodle. Votre code sera évalué automatiquement avec VPL : il est donc impératif de respecter scrupuleusement les consignes ! Votre code sera également relu par votre enseignant : il est donc impératif de soigner la qualité de son code (nommage des variables et fonctions cohérent, code commenté, éviter les fonctions trop longs ou trop complexes, éviter la duplication de code et les constantes magigues,...)
Annexes
Quelques astuces en Python pour vous aider.
Décoder une trame binaire
Voici un extrait de code illustrant comment décoder une requête utilisée dans un autre protocole (TFTP).
frame = b'\x00\x02test.txt\x00octet\x00' # sample of write request as byte array
frame1 = frame[0:2] # frame1 = b'\x00\x02'
frame2 = frame[2:] # frame2 = b'test.txt\x00octet\x00'
opcode = int.from_bytes(frame1, byteorder='big') # opcode = 2
args = frame2.split(b'\x00') # args = [b'test.txt', b'octet', b'']
filename = args[0].decode('ascii') # filename = 'test.txt'
(20).to_bytes(2, byteorder='big') # encode the number 20 on 2 bytes
mode = args[1].decode('ascii') # mode = 'octet'
Documentation Python
Un peu d'aide pour Python 3 :
- Types standards, comme str : https://docs.python.org/3/library/stdtypes.html
- Fonction open : https://docs.python.org/3/library/functions.html#open
- Module socket : https://docs.python.org/3/library/socket.html
- Module argparse : https://docs.python.org/3/library/argparse.html
Tester une fonction avec doctest
La manière la plus rapide et la plus simple pour tester une fonction Python est d'utiliser l'outil doctest : https://docs.python.org/3/library/doctest.html.
Considérons un fichier my_example.py, qui contient une fonction que l'on souhaite tester. Cette fonction convertit en hexadécimal une chaîne de caractères UTF-8. Ainsi, il est possible d'écrire un petit test dans la documentation de cette fonction comme ceci.
def my_function(msg):
""" Convert string into bytes
>>> my_function("coucou").hex()
'636f75636f75'
"""
return bytes(msg, "utf-8")
Nota Bene : A partir de la version 3.8 de Python, il est possible d'ajouter
un argument option à la fonction .hex(" ")
pour rendre la sortie en
hexadécimal plus lisible.
Pour lancer ce test, il suffit alors de faire :
$ python3 -m doctest -v my_example.py
Trying:
my_function("coucou").hex()
Expecting:
'636f75636f75'
ok
...
1 passed and 0 failed.
Test passed.