MQTT–利用mosquitto库多线程实现sub端和pub端
1.关于mqtt的介绍以及mosquitto的基本使用相信大家都已经有所了解;有时候我们想写一个程序,既可以实现订阅(sub端),又可以做别的事情,比如读取并且处理一些传感器的数据,或者是既可以发布(pub端),又可以订阅(sub端),这时候使用多线程就十分方便和高效;
2.多线程:在操作系统原理的术语中,线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈,寄存器环境。一个进程可以有很多线程,每条线程并行执行不同的任务;
线程可以提高应用程序在多核环境下处理事件的性能,提高效率。在Unix系统中,完成相关任务的不同代码间需要交换数据,如果采用多进程的方式,进程的创建所花的时间片要大于线程,另外进程间的通信比较麻烦。但是如果使用多线程的方式,通过共享的全局变量,线程间的通信变得非常容易。
3.由主线程调用pthread_create()创建的线程称为子线程:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void
*arg);
功能: pthreand_create()用来创建一个线程,并执行第三个参数start_routine所指向的函数;
第一个参数thread是一个pthread_t类型的指针,他用来返回该线程的线程ID。每个线程都能够通过pthread_self()来获取
自己的线程ID;
第二个参数是线程的属性,其类型是pthread_attr_t类型,其定义如下:
typedef struct
{
int detachstate; //线程的分离状态
int schedpolicy;// 线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void * stackaddr; //线程栈的位置
size_t stacksize;// 线程栈的大小
}pthread_attr_t;
对于这些属性,我们需要设定的是线程的分离状态,修改每个线程的栈大小。每个线程创建后默认是joinable(可会和)状态,该状态需要主线程调用 pthread_join 等待它退出,否则子线程在结束时,内存资源不能得到释放造成内存泄漏。所以我们创建线程时一般会将线程设置为分离状态,在创建线程的属性设置里设置PTHREAD_CREATE_DETACHED属性即可;
第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务函数;
第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去;
注意:最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,相互之间就是对等关系,不存在其他层次关系。
无论在windows中还是Linux中,主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有
子线程执行都会终止。这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁
后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资
源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销
毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:
- 可会合(joinable):主线程必须会合可会合的子线程,即主线程等待子进程执行完毕,在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。
- 相分离(detached):子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦自己运行结束了,线程也就终止了,马上释放系统资源。
4.使用多线程实现sub端与pub端:
简单思路:
sub端:
pub端:
我相信有很多人在刚使用mosquitto库的时候,都有疑问,connect成功了,为什么要用loop呢,publish成功了不就完成了通信吗?其实,mqtt是消息队列传输协议,我个人理解,publish只是把消息放入消息队列里面,并没有处理或者发送消息,而是通过调用loop函数,来处理这些放入消息队列的消息;举个例子就是: 你寄快递(publish)把快递(message)放到菜鸟驿站(消息队列) 登记了信息(topic),然后你就离开了,菜鸟驿站应该找一个快递员(loop)给你把快递送出去;当你的快递到达你所期望地方的菜鸟驿站(消息队列),菜鸟驿站也会派一个快递员(loop),把你的快递送到指定的地址(subscribe),只有接收人地址有效(topic),他才会收到快递(message);
当然loop函数也有mosquitto_loop, mosquitto_loop_start, mosquitto_loop_stop, mosquitto_loop_forever等;这些函数的功能和使用相信不用我多说都了解;
但是需要注意的是: mosquitto_loop_start是一个非阻塞调用,它创建一个单独的线程来不断调用mosquitto_loop() 函数处理网络事件,一旦运行过程中disconnect,需要调用mosquitto_loop_stop关闭创建的线程,该调用将一直阻塞,直到网络线程结束。为了使网络线程结束,必须事先调用mosquitto_disconnect或将第二个参数设置为true;而 mosquitto_loop_forever是在无限阻塞循环中调用mosquitto_loop()函数处理网络事件,如果服务器连接丢失,它将处理重新连接,如果在回调中调mosqitto_disconnect()函数它将返回(适用于短连接);
伪代码:
长连接:
connect;
loop_start;
while(1)
{
do something;
}
短连接:
connect_back
{
do something;
dis connect;
}
set_connect_callback(connect_back);
while(1)
{
connect;
loop_forever;
}
具体代码实现:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pthread.h>
#include <ctype.h>
#include <mosquitto.h>
#include "subscribe.h"
#include "mosquitto.h"
#include "temperature.h"
#include "mqttini.h"
#include "cJSON.h"
#include "cjson.h"
#include "logger.h"
static int g_stop = 0;
typedef void *(THREAD_BODY) (void *thread_arg);
int thread_start_init( pthread_attr_t *thread_attr);
void sig_handle(int signum);
void *thread_sub_work(void *ctx);
void *thread_pub_work(void *ctx);
int main(int argc,char **argv)
{
int mid = 0;
char *character = NULL;
pthread_t tid;
pthread_attr_t thread_attr;
mosquitto_data mosq_arg;
if(!argv[1])
{
printf("Please input valid argument to connect server:\n The character A indicates Aliyun\n The character T indicates Tengxunyun\n The character H indicates Huaweiyun\n");
printf("For example:%s A ---connect to Aliyun\n", argv[0]);
return 0;
}
character = argv[1];
memset(&mosq_arg, 0, sizeof(mosq_arg));
if(mqtt_ini(character, &mosq_arg) < 0) //从配置文件获取参数
{
printf("Aliyun inisparser failure:%s\n", strerror(errno));
goto Cleanup;
}
signal(SIGINT, sig_handle); //安装信号
signal(SIGTERM,sig_handle);
if(logger_init(mosq_arg.log_name, LOG_LEVEL_DEBUG) < 0) //初始化日志
{
fprintf(stderr,"Initial logger system failure\n");
goto Cleanup;
}
if( mosquitto_lib_init() != MOSQ_ERR_SUCCESS ) //mosquitto 初始化
{
log_error( "mosquitto lib init failure:%s\n", strerror(errno) );
goto Cleanup;
}
if( thread_start_init( &thread_attr ) < 0 ) //线程初始化
{
log_error( "Thread start init failure:%s\n", strerror(errno) );
goto Cleanup;
}
if( pthread_create( &tid, &thread_attr, thread_sub_work, (void *)&mosq_arg ) < 0 ) //创建sub线程
{
log_error( "Create subscript pthread failure:%s\n", strerror(errno) );
goto Cleanup;
}
if( pthread_create( &tid, &thread_attr, thread_pub_work, (void *)&mosq_arg ) < 0 ) //创建pub线程
{
log_error(" Create publish pthread failure:%s\n", strerror(errno) );
goto Cleanup;
}
while(!g_stop)
{
sleep(1);
}
Cleanup:
sqlite_close();
mosquitto_lib_cleanup();
pthread_attr_destroy( &thread_attr );
logger_term();
return 0;
}
int thread_start_init( pthread_attr_t *thread_attr )
{
int rv = -1;
if( pthread_attr_init(thread_attr) )
{
log_error( "Pthread attr init failure:%s\n", strerror(errno) );
goto CleanUp;
}
if( pthread_attr_setstacksize(thread_attr, 120*1024) )
{
log_error( "Pthread attr setstacksize failure:%s\n", strerror(errno) );
goto CleanUp;
}
if( pthread_attr_setdetachstate( thread_attr, PTHREAD_CREATE_DETACHED) )
{
log_error( "Pthread attr setdetachstate failure:%s\n", strerror(errno) );
goto CleanUp;
}
rv = 0;
CleanUp:
return rv;
}
void *thread_sub_work(void *ctx)
{
char *client_id = "subscript";
struct mosquitto *mosq = NULL;
mosquitto_data *arg;
if( !ctx )
{
log_error("Invalid input arguments\n");
pthread_exit(NULL);
}
arg = (mosquitto_data *)ctx;
mosq = mosquitto_new(client_id, true, (void *)arg);
if(!mosq)
{
log_error("Mosquitto new failure:%s\n", strerror(errno));
goto Cleanup;
}
if(mosquitto_username_pw_set(mosq, arg->user, arg->password) != MOSQ_ERR_SUCCESS) //设置连接代理账号密码
{
log_error("Mosquitto get username and password failure:%s\n", strerror(errno));
goto Cleanup;
}
mosquitto_connect_callback_set( mosq, my_connect_callback );
mosquitto_disconnect_callback_set( mosq, my_disconnect_callback );
mosquitto_message_callback_set( mosq, my_message_callback );
mosquitto_subscribe_callback_set( mosq, my_subscribe_callback );
if(mosquitto_connect(mosq, arg->hostname, arg->port, arg->keepalive) != MOSQ_ERR_SUCCESS)
{
log_error("mosquitto connect server failure:%s\n", strerror(errno));
goto Cleanup;
}
if(mosquitto_loop_start(mosq) != MOSQ_ERR_SUCCESS)
{
log_error("Mosquitto loop failure:%s\n", strerror(errno));
goto Cleanup;
}
while( !g_stop )
{
mosquitto_loop_stop( mosq, false );
pthread_exit(NULL);
}
Cleanup:
mosquitto_destroy(mosq);
pthread_exit(NULL);
}
void *thread_pub_work(void *ctx)
{
int flag = 0;
int row = 0;
int mid = 0;
int con_flag = 0;
char data_buf[128];
char id_data[128];
char *sn = "RPI9986";
time_t last_time;
all_data data;
mosquitto_data *arg;
struct mosquitto *mosq = NULL;
if( !ctx )
{
log_error( "Invalid input arguments\n" );
pthread_exit( NULL );
}
arg = (mosquitto_data *)ctx;
mosq = mosquitto_new(arg->client_id, false, (void *)arg); //创建一个新的客户端
if(!mosq)
{
log_error("Mosquitto new failure:%s\n", strerror(errno));
goto Cleanup;
}
if(mosquitto_username_pw_set(mosq, arg->user, arg->password) != MOSQ_ERR_SUCCESS) //设置连接代理账号密码
{
log_error("Mosquitto get username and password failure:%s\n", strerror(errno));
goto Cleanup;
}
last_time = time(NULL)-mosq_arg.second;
while(!g_stop)
{
flag = 0; //采样标志
if((time(NULL)-last_time) >= mosq_arg.second) //是否到采样时间
{
if((get_temperature(sn, &data)) < 0)
{
log_error("Get temperature failture:%s\n",strerror(errno));
goto Cleanup;
}
if(pack_json(data_buf, sizeof(data_buf), &data) < 0) //打包数据
{
log_error("Pack json failure:%s\n", strerror(errno));
goto Cleanup;
}
last_time = time(NULL);
flag = 1;
}
if(!con_flag) //未连接服务器则连接服务器
{
if(mosquitto_connect(mosq, arg->hostname, arg->port, arg->keepalive) != MOSQ_ERR_SUCCESS) //连接服务器失败
{
printf("Mosquitto connect failure:%s\n", strerror(errno));
con_flag = 0; //将连接标志设为断线
continue;
}
if(mosquitto_loop_start(mosq) != MOSQ_ERR_SUCCESS)
{
log_error("Mosquitto loop failure:%s\n", strerror(errno));
goto Cleanup;
}
con_flag = 1; //连接成功
}
if(flag) //如果已经采样
{
if((mosquitto_publish(mosq, &mid, arg->pub_topic, strlen(data_buf)+1, data_buf, 0, true)) != MOSQ_ERR_SUCCESS) //发布数据
{
log_error("Mosquitto Publish failure:%s\n", strerror(errno));
if(mosquitto_loop_stop(mosq, true) != MOSQ_ERR_SUCCESS) //关闭创建的进程
{
log_error("Mosquitto loop stop failure:%s\n", strerror(errno));
goto Cleanup;
}
con_flag = 0; //将标志设为断线
continue;
}
log_info("Mosquitto Publish data successfully!\n\n");
}
Cleanup:
mosquitto_destroy(mosq);
pthread_exit(NULL);
}
void sig_handle( int signum ) //define a function captures the signal
{
printf( "Catch signal [%d]\n", signum );
g_stop = 1; //break the cycle
}
(注意:如果发布端和订阅端都是长连接,mosquitto_new一个新的客户端时使用相同的client_id,同时运行时可能会产生connect冲突)
附:
sub端回调函数:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include "mosquitto.h"
#include "temperature.h"
#include "mqttini.h"
#include "cjson.h"
void my_connect_callback(struct mosquitto *mosq, void *obj, int rc)
{
int mid = 0;
mosquitto_data *arg;
arg = (mosquitto_data*)obj;
if(!rc)
{
if(mosquitto_subscribe(mosq, &mid, arg->sub_topic, 0) != MOSQ_ERR_SUCCESS)
{
printf("Mosquitto subscript failure:%s\n", strerror(errno));
exit(1);
}
}
}
void my_message_callback( struct mosquitto *mosq, void *obj, const struct mosquitto_message *msg )
{
if( msg->payload )
{
if( parse_json(msg->payload) < 0 )
{
printf( "Parse json failure:%s\n", strerror(errno) );
exit(1);
}
}
}
void my_disconnect_callback( struct mosquitto *mosq, void *obj, int rc )
{
int mid = 0;
mosquitto_data *arg;
arg = (mosquitto_data *)obj;
if(!rc)
{
printf("Mosquitto to server disconnected\n");
if(mosquitto_connect(mosq, arg->hostname, arg->port, arg->keepalive) != MOSQ_ERR_SUCCESS)
{
printf("Mosquitto connect server failure:%s\n", strerror(errno));
exit(1);
}
}
}
void my_subscribe_callback(struct mosquitto *mosq, void *obj, int mid, int qos_count, const int *granted_qos)
{
printf("Mosquitto subscribe successfully!\n");
}
temperature.h文件结构体以及函数:具体c文件可自行编写
typedef struct ALL_DATA_ctx
{
char sn_buf[16]; //序列号
char time_buf[32]; //时间
float temp; //温度
}all_data;
/* A function that gets the temperature,sn and time;
* You need to pass in a sn and a pointer tmp type of struct all_data;
* Success return 0, otherwise less than 0;*/
int get_temperature(char *sn, all_data *tmp);
当然也需要自己编写配置文件以及利用iniparser编写解析函数(mqttinit.c),编写存储这些参数的结构体.h文件,通过修改配置文件支持上报数据到阿里云,腾讯云和华为云;同时也可以自己添加数据库的相关操作,添加pub端回调函数,修改sub端的回调函数完成其他操作;
对于上传云平台(阿里云为例),设置产品功能时,可能没有自己想要的功能,可以通过自定义指定自己需要的功能,然后使用mqttfx测试,根据测试获取支持的格式利用cjson打包和解析:(具体阿里云创建产品和设备以及mqttfx使用可查看其他博主博客)
(下图在左侧设备管理产品栏)
cjson操作函数:
/*打包*/
int pack_json( char *buf, int buf_size, all_data *arg )
{
cJSON *root = NULL;
cJSON *params = NULL;
if( !buf || !buf_size || !arg )
{
printf( "Invalid input arguments!\n" );
return -1;
}
root = cJSON_CreateObject();
params = cJSON_CreateObject();
cJSON_AddNumberToObject( params,"Temperature", arg->temp );
cJSON_AddStringToObject( params,"Time", arg->time_buf );
cJSON_AddStringToObject( params,"SN", arg->sn_buf );
cJSON_AddItemToObject( root,"params", params );
memset( buf, 0, sizeof(buf) );
strncpy( buf, cJSON_Print(root), buf_size );
printf("Send the message:\n");
printf( "SN:%s\n", arg->sn_buf );
printf( "Time:%s\n", arg->time_buf );
printf( "Temperature:%.3f\n", arg->temp );
printf( "\n" );
cJSON_Delete( root );
return 0;
}
/*解析*/
int parse_json(void *payload)
{
cJSON *root = NULL;
cJSON *item = NULL;
cJSON *json = NULL;
if( !payload )
{
printf( "Invalid input arguments!\n" );
return -1;
}
printf("Receive the message:\n");
root = cJSON_CreateObject();
root = cJSON_Parse( payload );
json=cJSON_GetObjectItem( root, "params" );
item=cJSON_GetObjectItem( json, "SN" );
printf( "SN:%s\n", item->valuestring );
item=cJSON_GetObjectItem( json, "Time" );
printf( "Time:%s\n", item->valuestring );
item=cJSON_GetObjectItem( json, "Temperature" );
printf( "Temperature:%.3f\n", item->valuedouble );
printf( "\n" );
cJSON_Delete( root );
return 0;
}
最终结果:
(注:经过目前测试,通过阿里云无法同时实现自动完成订阅和发布,如果有其他方法能实现,会更新此博客,以上多线程建议在本机测试)
以上部分函数希望读者参考自行编写,理解其中的奥秘和函数的魅力。
同时以上仅为本人学习总结,如有错误或者疑问,欢迎斧正与留言!