GMOインターネット新里です。よくIoT向けにはMQTT/CoAPと話が出ますが、本エントリーでは、色々と実装してきた中で感じたことを綴ってみます。
IoT向けとは?
"IoT = Internet of Things"という言葉もわりと浸透してきましたね。僕が最初にIoTなるワードを見た時は、"これ=IoT"は一体何の顔文字なのか?と思ったりしましたが。WoT(Web of Things)、AIoT(AI of Things)といった、「とりあえずThings付けてみるか?」みたいな感じの言葉も出てきています。
IoT =「モノのインターネット」というのは、モノがインターネットに繋がって何かサービスをすると考えられますね。インターネットに繋がるには、一体どうすれば?という事があります。
モノが使う通信
僕が触ったり使ってきたモノにはIPを使うモノ以外もたくさんあります。ザックリと通信で大別するとこんな感じでしょうね。
■ 非IPモノBluetooth、BLE、Zigbee、920MHz/420MHz帯域モノ(Lazurite、RoLA、Sigfox、Wi-SUN、ELTERS...etc)
■ IPモノWi-Fi、セルラー(3G、LTEモノ)、有線(Ethernet)
すべてのIoT的なモノがIPを使えれば良いですが、必ずしもそうではありませんね。そこで、非IP物はインターネットに接続するGatewayを介して制御することになります。
たとえば、僕はBluetoothの体重計を使っています。この体重計がIoT端末としても考えることが出来て、スマホから体重のデータを通信して保存したり傾向を分析したり、、、といった感じですね。
このように、必ずしも直接インターネットに接続しなくても良いIoT的な端末と、Wi-Fi等を使用したIPで直接通信をする端末があります。後者については、たとえばAmazonスマートプラグは直接Wi-Fiに接続しています。そして、リモートでスマートプラグのON/OFFをすることが出来ます。
このインターネットにIPで接続するようなIoT的な端末において、よく話に上がるプロトコルにMQTTとCoAPがあります。なぜこの2つが?がというと、後述するTCP/UDPで対応している、気づいたら流行っていた、大手クラウドベンダーがプラットフォームとして対応した(GCP、Azure,AWSなどはMQTTに対応している)といった理由がありそうです。ただ、僕的にはHTTP(S)の方が、IoT的な環境でよく利用される物ではあると考えています。HTTPでAPIを叩くような感じで、IoT的な端末で実装した方が、「使い慣れた環境」として便利なのかなーと。他にも将来的にはQUIC、HTTP/3がIoTの世界にも入ってくるのは容易に想像できるでしょう。
MQTT
MQTTはIBMで仕様が作られたPub/Subメッセージプロトコルです。仕様はこちらで公開されていますね。仕組みは簡単、メッセージブローカーにSubscribeして、Publishでメッセージが配送されるといったシンプルな物ですね。
この例だとSubscriberにPublisherが配送された、{"temperature": "10"}が配送されます。ここでBroker部分がクラウド化されて、AWS IoT Core, Google Cloud IoT Core, Azure Hub...etc と主なクラウドサービスでは使うことができますね。同時にこれらのサービスだと、端末の管理も行ってくれて便利だったりもします。
以下はMQTTサーバに接続してPub/SubするParticle向けの簡単なコードです。
#include "MQTT.h"
// 受信用のコールバック
void callback(char* topic, byte* payload, unsigned int length);
// MQTTにコールバックを設定
MQTT client("server_dns_or_ip", 1883, callback);
// コールバック
void callback(char* topic, byte* payload, unsigned int length) {
char p[length + 1];
memcpy(p, payload, length);
p[length] = NULL;
if (!strcmp(p, "RED"))
RGB.color(255, 0, 0);
else if (!strcmp(p, "GREEN"))
RGB.color(0, 255, 0);
else if (!strcmp(p, "BLUE"))
RGB.color(0, 0, 255);
else
RGB.color(255, 255, 255);
delay(1000);
}
void setup() {
RGB.control(true);
// MQTTサーバに接続
client.connect("sparkclient");
// publish/subscribeの登録
if (client.isConnected()) {
client.publish("outTopic/message","hello world");
client.subscribe("inTopic/message");
}
}
void loop() {
if (client.isConnected())
client.loop();
}
MQTTは基本はTCPを使っていて、セキュリティを考えると下回りはTLSを使う方が良いでしょう。UDPを使うMQTT-SN(Sensor Network)という物もありますが、まだこれからといった感じがしています。
CoAP
CoAPはRFC 7252で上がっている、UDPを使ったM2M向けのプロトコルです。マイコンのような低消費電力、ROM/RAMの容量をターゲットにしています。CoAPの特徴的なのはUDPを使ってHTTPを実装しているような仕組みになっている点です。
MQTTのようにPub/Subではなく、Web(HTTP)を意識して動作します。センサー端末同士をCoAPで通信する他、CoAP Proxyを通すことでHTTPとの通信も可能にしている所がおもしろいですね。動作的にはCoAPサーバのエンドポイントに対してPUT/POST/GET/DELETE メソッドでアクセスしてデータの登録・取得をします。足回りにDTLSを使ってセキュリティに配慮したオプションも用意されています。
以下はESP32向けのCoAPの簡単な実装です。"/light" をエンドポイントとしたCoAPサーバとして動作します。
#include <WiFi.h>
#include <WiFiUdp.h>
#include <coap-simple.h>
const char* ssid = "your-ssid";
const char* password = "your-password";
// コールバック用関数
void callback_response(CoapPacket &packet, IPAddress ip, int port);
void callback_light(CoapPacket &packet, IPAddress ip, int port);
// UDP and CoAP class
WiFiUDP udp;
Coap coap(udp);
// LED STATE
bool LEDSTATE;
// CoAPサーバとして動作するコードバック
void callback_light(CoapPacket &packet, IPAddress ip, int port) {
Serial.println("[Light] ON/OFF");
// send response
char p[packet.payloadlen + 1];
memcpy(p, packet.payload, packet.payloadlen);
p[packet.payloadlen] = NULL;
String message(p);
if (message.equals("0"))
LEDSTATE = false;
else if(message.equals("1"))
LEDSTATE = true;
if (LEDSTATE) {
digitalWrite(9, HIGH) ;
coap.sendResponse(ip, port, packet.messageid, "1");
} else {
digitalWrite(9, LOW) ;
coap.sendResponse(ip, port, packet.messageid, "0");
}
}
// クライアント用コールバック
void callback_response(CoapPacket &packet, IPAddress ip, int port) {
Serial.println("[Coap Response got]");
char p[packet.payloadlen + 1];
memcpy(p, packet.payload, packet.payloadlen);
p[packet.payloadlen] = NULL;
Serial.println(p);
}
void setup() {
Serial.begin(9600);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// LEDの状態
pinMode(9, OUTPUT);
digitalWrite(9, HIGH);
LEDSTATE = true;
// サーバーのエンドポイント "/light" とコールバック定義
coap.server(callback_light, "light");
// クライアント動作する時のコールバックを定義
coap.response(callback_response);
// CoAPサーバとして起動
coap.start();
}
void loop() {
delay(1000);
coap.loop();
}
なーんとなくMQTTと似たような感じで実装できそうですが、TCP/UDPという大きな違い、プロトコルの中身も全然異なっていますね。
セキュリティ
上で触れたコードは個人的にCoAP/MQTTの実装をして、GitHubで公開をしています。MQTT、MQTT-TLS、Simple CoAP LibraryMQTTはParticleといデバイス向けで、CoAPについてはArduinoを始めとした一般的なマイコン環境での動作を想定した物です。両方ともIoT的な用途では色々に実装やIoT的な論文とかでも使われていますね。MQTTの方は数十万端末上でコンパイル&動作していたり、色々な会社さんでも実際に使われていたりしています。CoAPの方も探してみると、PlatformIOの方に展開されていたり、それなりに使って貰えているようです。TLSに対応したのは、組込み端末にTLSを実装してみたかったというのもあるものの、この手のマイコンにTLS組込みの大変さはあったりもします。もちろんOpenSSLのような超巨大な物は使えない。組込み向けのサイズが小さいmbedTLSを使うことになりますね。
■ 容量との果てしない戦い■ アルゴリズム何を使う?■ MPUのスペック大丈夫か?■ 存在しない関数群の補完■ verify、時刻同期どうする?
ざっと面倒な点はこれらになるでしょう。そもそもマイコンのアプリ領域で使えるサイズは限られていて、Particle Photonの場合は96Kbyteだけです。mbedTLSをギリギリに切り詰めても50-60Kbyteになってしまうため、アプリ領域で使えるサイズを圧迫して、アプリが実装する領域を削ってしまうことになっちゃいます。
TLSのアルゴリズムはセキュリティや接続先のサーバの接続性で色々なアルゴリズムを使いたいものの、また容量との戦いを繰り返すことに。そして、MPUのスペックは120MHz ARM Cortex-M3で大丈夫か? と心配だったものの、わりとこれはすんなり動作しました。ただ、メモリ容量も圧迫するので、常にサイズは意識する必要があるのがキツイですね。そして組込み向けなので、存在しない関数も当然出てきます。それらを補完して、次は時刻同期をどうするか?というのは1つ問題として出てきました。もちろんRTCの電池なんか付いている物はないです。ただ証明書のverify/expireで日時を確認するのは基本ということもあって、ネットワークに接続後時刻同期を行ってからverifyを行うようにしています。
実際にmbedTLSを組込むのにかかった時間は2日程度でした。というくらいmbedTLSを組込むのは容易な反面、マイコン・IoT端末にの環境に依存して「実際にTLSで通信できるもの」にするには工夫が必要になってきますね。
結局何が良いのか?
率直には、「好きなモノを使えばOK」とは思っています。MQTT・CoAP共にライブラリや実装が公開されて使えるものが多数あるため、実際に自分が使う環境で組込む分には問題ないでしょう。どちらもプロトコルはシンプルなので、自作しても良いでしょうし。将来的にQUIC、HTTP/3が主流になってくれば、IoT的なプロトコルでも同じような流れになるような予想はしています。
現在のところ、多くのクラウドサービスがMQTTに対応しているのもあって、クラウドとIoT的な連携を考慮すると、MQTTのほうが使いやすいでしょう。逆にモノ同士の通信を行う時や、UDPの軽量さを使いたい場合はCoAPを使うという選択に自然となってくる、ケースバイケースで用途が決まってくる流れですね。
もちろん、MQTT/CoAPにこだわる必要もなく、HTTPSで通信しても良いでしょう。最近のマイコンや端末は非常にスペックが強力になってきて、TLSに限らず大抵のプロトコルはすんなり使えるようになってきました。つまり、IoT向けだからMQTT/CoAPということもなく、どのプラットフォームで端末の環境に依存して、その上で動くアプリ・通信手段を考慮して、利用するプロトコルを選択した方が良いでしょう。