MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和制动器(比如通过Twitter让房屋联网)的通信协议。
###1.1 延伸阅读
推荐一些背景补充:
协议详细内容,我肯定说得不如协议内容中文版,建议大家先扫一下,对一些名词有印象,后续再查看。
其中,比较重要的部分,也是代码里需要设置的可变头部部分,
推荐几个比较好的学习地方:
订阅和发布以及代理服务器的理解示意图:
工作流:
服务器先启动,然后客户端订阅相关的Topic。Client A 和C发布主题为:Question
的What's the temperature?
。Client B因为订阅了Question
这个Topic,所以可以收到信息,Client B收到信息做判断后发布答案Topic: Temperture
出去,订阅了相关Topic的Client A 和Client C能接收到37°。
- 实现MQTT协议需要:客户端和服务器端
- MQTT协议中有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。
代理服务器,是用于服务客户端的,目前很多公司都有相关的服务: 列表
里面我只选用过Mosquitto,也就不做分析.
sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa
sudo apt-get update
sudo apt-get install mosquitto
安装后位于:/etc/mosquitto
,里面可以看到默认配置文件mosquitto.conf
sudo service mosquitto status
sudo service mosquitto start
sudo service mosquitto stop (常用,在测试SSL的时候)
mosquitto -v (加载默认配置)
mosquitto -v -c xx/xx/mosquitto.conf -p 8883 (加载指定配置)
避免麻烦,建议在使用SSL新建一个配置文件,这样不用一直改来改去。-c加载配置文件,-p端口。
TCP端口8883和1883已在IANA注册,分别用于MQTT的TLS和非TLS通信。
上一步只是安装了Mosquitto服务器,不包括客户端,安装这个是用于调试,可以在命令行测试证书、ip等,很方便。
sudo apt-get install mosquitto-clients
mosquitto_sub -t temperature
mosquitto_pub -t temperature -m 37°
服务器我们借助mosquitto软件来试下,那么客户端我们当然不会自己去写一个协议.显然已经有很多先驱写了,我们只需要导入就好了.这里采用比较出名的Eclipse Paho库,它包含的各种语言,或者库列表.
pip3 install paho-mqtt
#导入包
import paho.mqtt.client as mqtt
#创建client对象
client = mqtt.Client(id)
#连接
client.connect(host,post)
#订阅
client.subscribe(topic)
client.on_message = func #接收到信息后的处理函数
#发布
client.publish(topic, payload)
import paho.mqtt.client as mqtt
import sys
#改成自己的ip,命令ifconfig可以查看
host = "xx.xxx.xxx.xxx"
topic_sub = "Question"
topic_pub = "temperature"
def on_connect(client, userdata, flags, rc):
print("Connected with result code " + str(rc))
client.subscribe(topic_sub)
def on_message(client, userdata, msg):
print(msg.payload)
client.publish(topic_pub, "37°")
def main(argv = None):
#声明客户端
client = mqtt.Client()
#连接
client.connect(host, 1883, 60)
#两个回调函数,用于执行连接成功和接收到信息要做的事
client.on_connect = on_connect
client.on_message = on_message
client.loop_forever()
if __name__ == "__main__":
sys.exit(main())
运行python客户端,然后在终端发布一条消息
mosquitto_pub -t Question -m 123
#发送端
fp = open("7.jpg", "rb")
payload = fp.read()
client.publish(topic_pub, payload)
#接收端
def on_message(client, userdata, msg):
new_filename = "new_img.jpg"
fp = open(new_filename, 'wb')
fp.write(msg.payload)
fp.close()
这个可以推广到传输其他文件.
从源码编译,这个稍微比较麻烦,安装过程如下,注意文件整理:
git clone https://github.com/eclipse/paho.mqtt.c.git
cd paho.mqtt.c
make
sudo make install
在使用的过程中也要注意编译的方式,这里提供一个Makefile做参考:
test:test.cpp cmqtt.cpp cmqtt.h
g++ -o test test.cpp cmqtt.cpp -lpaho-mqtt3c \
-I ../../paho.mqtt.c/src \
-L ../../paho.mqtt.c/build \
-pthread -Imqtt \
-std=c++11
C代码方面我提供关键部分供初学者容易上手,我自己进行过一次c++的封装,比较复杂.有时间的话另开一帖.
//mqttclient.c
#include
#include
#include
#include
#include "MQTTClient.h"
#include
#include
#define NUM_THREADS 2
#define ADDRESS "tcp://xx.xxx.xx.xxx:1883"
#define CLIENTID "ExampleClient_pub"
#define SUB_CLIENTID "ExampleClient_sub" //更改此处客户端ID
#define TOPICPUB "Question" //更改发送的话题
#define TOPICSUB "temperature"
#define QOS 1
#define TIMEOUT 10000L
#define DISCONNECT "out"
int CONNECT = 1;
volatile MQTTClient_deliveryToken deliverytoken;
long PAYLOADLEN;
char* PAYLOAD;
void delivered(void *context, MQTTClient_deliveryToken dt)
{
printf("Message with token value %d delivery confirmed\n", dt);
deliverytoken = dt;
}
int msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message)
{
int i;
char* payloadptr;
printf("Message arrived\n");
printf(" topic: %s\n", topicName);
printf(" message: \n");
payloadptr = message->payload;
if (strcmp(payloadptr, DISCONNECT) == 0) {
printf("\n out!!");
CONNECT = 0;
}
for (i = 0; i < message->payloadlen; i++) {
putchar(*payloadptr++);
}
printf("\n");
MQTTClient_freeMessage(&message);
MQTTClient_free(topicName);
return 1;
}
void connlost(void *context, char *cause)
{
printf("\nConnection lost\n");
printf(" cause: %s\n", cause);
}
void *pubClient(void *threadid) {
long tid;
tid = (long)threadid;
int count = 0;
printf("Hello World! It's me, thread #%ld!\n", tid);
//声明一个MQTTClient
MQTTClient client;
//初始化MQTT Client选项
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
//#define MQTTClient_message_initializer { {'M', 'Q', 'T', 'M'}, 0, 0, NULL, 0, 0, 0, 0 }
MQTTClient_message pubmsg = MQTTClient_message_initializer;
//声明消息token
MQTTClient_deliveryToken token;
int rc;
//使用参数创建一个client,并将其赋值给之前声明的client
MQTTClient_create(&client, ADDRESS, CLIENTID,
MQTTCLIENT_PERSISTENCE_NONE, NULL);
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;
//使用MQTTClient_connect将client连接到服务器,使用指定的连接选项。成功则返回MQTTCLIENT_SUCCESS
if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to connect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
PAYLOAD = "What's the temperature";
// printf("%s\n", PAYLOAD);
pubmsg.payload = PAYLOAD;
pubmsg.payloadlen = (int)strlen(PAYLOAD);
pubmsg.qos = QOS;
pubmsg.retained = 0;
//循环发布
while (CONNECT) {
MQTTClient_publishMessage(client, TOPICPUB, &pubmsg, &token);
printf("Waiting for up to %d seconds for publication of %s\n"
"on topic %s for client with ClientID: %s\n",
(int)(TIMEOUT/1000), PAYLOAD, TOPICPUB, CLIENTID);
rc = MQTTClient_waitForCompletion(client, token, TIMEOUT);
printf("Message with delivery token %d delivered\n", token);
// thread sleep
usleep(2000000L);
}
MQTTClient_disconnect(client, 10000);
MQTTClient_destroy(&client);
}
void *subClient(void *threadid) {
long tid;
tid = (long)threadid;
printf("Hello World! It's me, thread #%ld!\n", tid);
MQTTClient client;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
int rc;
int ch;
MQTTClient_create(&client, ADDRESS, SUB_CLIENTID,
MQTTCLIENT_PERSISTENCE_NONE, NULL);
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;
//设置回调函数
MQTTClient_setCallbacks(client, NULL, connlost, msgarrvd, delivered);
if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to connect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
printf("Subscribing to topic %s\nfor client %s using QoS%d\n\n"
"Press Q to quit\n\n", TOPICSUB, SUB_CLIENTID, QOS);
MQTTClient_subscribe(client, TOPICSUB, QOS);
do
{
ch = getchar();
} while (ch != 'Q' && ch != 'q');
//quit
MQTTClient_unsubscribe(client, TOPICSUB);
MQTTClient_disconnect(client, 10000);
MQTTClient_destroy(&client);
pthread_exit(NULL);
}
int main(int argc, char* argv[])
{
pthread_t threads[NUM_THREADS];
pthread_create(&threads[0], NULL, subClient, (void *)0);
pthread_create(&threads[1], NULL, pubClient, (void *)1);
pthread_exit(NULL);
}
Android客户端同样需要一些配置来导入库,官网教程不是很好看,HIVEMQ的不错:
build.gradle
//和android\dependencies同级
repositories {
maven {
url "https://repo.eclipse.org/content/repositories/paho-snapshots/"
}
}
dependencies {
......
implementation('org.eclipse.paho:org.eclipse.paho.android.service:1.0.2') {
exclude module: 'support-v4'
}
......
}
AndrroidManifest.xml
// 放在manifest下一级
// 放在下一级
###5.2 Code
public class MainActivity extends AppCompatActivity {
private static final String TAG = "LQH";
Button bt_connect;
Button bt_sub;
Button bt_pub;
TextView textView;
String log;
MqttAndroidClient client;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bt_connect = findViewById(R.id.bt_connect);
bt_pub = findViewById(R.id.bt_pub);
bt_sub = findViewById(R.id.bt_sub);
textView = findViewById(R.id.textView);
log = "Log:\n\n";
textView.setText(log);
bt_connect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String clientId = MqttClient.generateClientId();
//创建客户端
client = new MqttAndroidClient(MainActivity.this, "tcp://xx.xxx.xx.xxx:1883",
clientId);
//连接
try {
IMqttToken token = client.connect();
token.setActionCallback(new IMqttActionListener() {
//两个响应函数
@Override
public void onSuccess(IMqttToken asyncActionToken) {
// We are connected
Log.d(TAG, "onSuccess");
Toast.makeText(MainActivity.this, "connect successed", Toast.LENGTH_SHORT).show();
log += "Connect successed!\n\n";
textView.setText(log);
}
@Override
public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
// Something went wrong e.g. connection timeout or firewall problems
Log.d(TAG, "onFailure");
Toast.makeText(MainActivity.this, "not connect", Toast.LENGTH_SHORT).show();
log += "Connect failed!\n\n";
textView.setText(log);
}
});
} catch (MqttException e) {
e.printStackTrace();
}
//设置几个回调函数
client.setCallback(new MqttCallback() {
//连接断开
@Override
public void connectionLost(Throwable cause) {
Toast.makeText(MainActivity.this, "connectionLost", Toast.LENGTH_SHORT).show();
}
//接收信息
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
log = log + "Recevied msg: " + new String(message.getPayload()) + "\n\n";
textView.setText(log);
}
//发布信息成功
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
Toast.makeText(MainActivity.this, "published", Toast.LENGTH_SHORT).show();
log = log + "Published\n\n";
textView.setText(log);
}
});
}
});
bt_sub.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final String topic = "temperature";
int qos = 1;
try {
//订阅
IMqttToken subToken = client.subscribe(topic, qos);
subToken.setActionCallback(new IMqttActionListener() {
@Override
public void onSuccess(IMqttToken asyncActionToken) {
// The message was published
Toast.makeText(MainActivity.this, "subscribe successed", Toast.LENGTH_SHORT).show();
log = log + "Subscribe topic: " + topic + " successed!\n\n";
textView.setText(log);
}
@Override
public void onFailure(IMqttToken asyncActionToken,
Throwable exception) {
// The subscription could not be performed, maybe the user was not
// authorized to subscribe on the specified topic e.g. using wildcards
Toast.makeText(MainActivity.this, "subscribe failure", Toast.LENGTH_SHORT).show();
}
});
} catch (MqttException e) {
e.printStackTrace();
}
}
});
bt_pub.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String topic = "Question";
String payload = "What's the temperature?";
try {
MqttMessage message = new MqttMessage(payload.getBytes());
//发布
client.publish(topic, message);
log = log + "Publish:\n" + " topic:" + topic + "\n payload:" + payload + "\n\n";
textView.setText(log);
} catch (MqttException e) {
e.printStackTrace();
}
}
});
}
}
好吧,就是这么简陋的,例如你没连接就发布,程序会崩溃.hhh
这边有两个帖子,一个系列,写得实在好:
我也稍作解释:
经常采用单向认证,双向虽然更安全,但是每个客户还要求生成证书会很麻烦。后面代码也基于此。
修改配置文件
既然我们想要采用ssl认证,那么我们自然需要改配置,稍微一思考,我们需要改的内容也不多,指定ca.crt, server.crt, server.key三个文件的路径,指定单向认证或者双向认证。
mqtt_tls.conf
# 这是我的路径,要改
# CA证书,pem格式,
cafile /xxx/ca/ca.crt
# 服务器证书
certfile /xxx/server/server.crt
# 服务器密钥
keyfile /xxx/server/server.key
# false->单向认证, true->双向认证
require_certificate false
# 如果require_certificate为true,则可以将use_identity_as_username设置为true以将客户端证书中的CN值用作用户名。 如果true,则不会将password_file选项用于此侦听器。
use_identity_as_username false
mosquitto -v -c xx/xx/mqtt_tls.conf -p 8883
mosquitto_sub -t temperture -h xx.xxx.xx.xxx -p 8883 --cafile /xxx/xxx/ca.crt
mosquitto_pub -t temperture -m 37° -h xx.xxx.xx.xxx -p 8883 --cafile /xx/xxx/ca.crt
这样就实现了mosquitto的配置测试,
python代码的改动比较的简单:
client.tls_set("/xx/xxx/ca.crt", tls_version=ssl.PROTOCOL_TLSv1_2)
# client.connect(host, 1883, 60)
client.connect(host, 8883, 60)
简单加载证书,后面的版本指定本来可以没有,但我这边后来软件变动出现问题,所以加上这个。
###7.3 C
C的代码改动也不复杂,就是有个坑
// 1
#define ADDRESS "ssl://xx.xxx.xx.xxx:8883"
// 2
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
MQTTClient_SSLOptions ssl = MQTTClient_SSLOptions_initializer;
ssl.trustStore = "/xx/xxx/ca.pem";
conn_opts.ssl = &ssl;
// 3,巨坑
//编译加载的库不一样,要 +s !!! -lpaho-mqtt3cs
test:test.cpp cmqtt.cpp cmqtt.h
g++ -o test test.cpp cmqtt.cpp -lpaho-mqtt3cs \
-I ../../paho.mqtt.c/src \
-L ../../paho.mqtt.c/build \
-pthread -Imqtt \
-std=c++11
这块和前面两个有点不一样,因为之前的ca.crt在这里是不能用的,Android能加载的证书需要是bks格式的,所以这里需要先生成bks,然后把ca.crt添加进去。再加载。
java -version
根据上面指令查看jdk版本,然后下载合适的bcprov,–>bcprov-ext-jdk15on-160.jar
,然后放到(jdk_home)/jre/lib/ext
这里可以用下面指令找到放置的文件夹:
locate jaccess
jaccess只是本来存在(jdk_home)/jre/lib/ext
文件夹下的另一个文件,如果安装了jdk和android-studio,那么可以找到三条位置,选择/xxx/android-studio/jre/jre/lib/ext/
,修改这里比较简单,/usr/下的往往还涉及权限。
keytool -importcert -keystore test_ca.bks -file /xxx/ca.crt -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "/xxx/android-studio/jre/jre/lib/ext/bcprov-ext-jdk15on-160.jar"
输入六位密码,yes,就可以看到生成的test_ca.bks
将test_ca.bks
放到android项目下的/res/raw
,没有就新建
// 1
client = new MqttAndroidClient(MainActivity.this, "ssl://xx/xxx/xx/xxx:8883",
clientId);
// 2.配置MqttConnectOptions
MqttConnectOptions options = new MqttConnectOptions();
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
KeyStore keyStore = KeyStore.getInstance("BKS");
// 刚才生成的文件加载,“123456”对应密码
keyStore.load(this.getResources().openRawResource(R.raw.test_ca),"123456".toCharArray());
trustManagerFactory.init(keyStore);
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
SocketFactory factory = sslContext.getSocketFactory();
options.setSocketFactory(factory);
然后Alt+Enter
解决各种红色波浪。基本都是异常处理。