本文由博主原创,未经博主许可不得转载。
幸运的是,这个版本“麻雀虽小五脏俱全”,没有“特性蔓延”的问题,作为初学者研究非常合适。
下面进入具体剖析。
Mosquito V0.1版本,实现了独立、完整的MQTT V3.1协议的服务端(broker)。源码行数约3000行,使用C语言编写,.c文件13个,broker使用其中的10个文件。因为mosquitto基于sqlite3,其编译链接和运行,需要libsqlite3.so。
文件名 |
主要函数 |
描述 |
conf.c |
mqtt3_config_read |
读取并解析配置文件 |
context.c |
mqtt3_context_init mqtt3_context_cleanup |
提供mqtt3_context的初始化和清理接口。 mqtt3_context结构含socket fd,客户端id,最后一次收发时间,保活时间等参数。 |
memory.c |
mqtt3_calloc mqtt3_free等 |
提供内存分配和使用接口 |
database.c |
mqtt3_db_open mqtt3_db_close _mqtt3_db_tables_create _mqtt3_db_statement_prepare mqtt3_db_XXX_insert等 |
提供mqtt相关sqlite3数据库操作接口 |
net.c |
mqtt3_socket_listen |
提供TCP socket接口 |
mosquito.c |
handle_read |
入口函数所在文件 |
raw_send.c |
mqtt3_raw_publish mqtt3_raw_puback等 |
提供mqtt原始报文发送接口 |
raw_send_client.c |
mqtt3_raw_connect mqtt3_raw_disconnect mqtt3_raw_subscribe mqtt3_raw_unsubscribe |
提供客户端mqtt原始报文发送接口,broker不使用该文件 |
raw_send_server.c |
mqtt3_raw_connack |
提供connect ack发送接口 |
read_handle.c |
mqtt3_handle_publish mqtt3_handle_puback mqtt3_handle_pingreq等 |
提供socket上读入数据处理接口 |
read_handle_client.c |
mqtt3_handle_connack mqtt3_handle_suback mqtt3_handle_unsuback |
提供客户端socket上读入数据处理接口,broker不使用该文件 |
read_handle_server.c |
mqtt3_handle_connect mqtt3_handle_disconnect mqtt3_handle_subscribe mqtt3_handle_unsubscribe |
提供mqtt conn/disconn/sub/unsub命令处理接口 |
util.c |
mqtt3_command_to_string |
提供工具,未使用 |
mosquito.c - - > conf.c
mosquito.c - - > read_handle_server.c - - > context.c
|- - > database.c
|- - > raw_send.c
|- - > raw_send_server.c
|- - > net.c
mosquito.c - - > read_handle.c
|- - > database.c
| - - > net.c
mosquito.c - - > memory.c
mosquito.c - - > database.c
程序的运行,需要libsqlite3.so
略
mosquitto启动后共创建5个表:
1、主要用于版本信息的config
CREATE TABLE config ( [key] TEXT PRIMARY KEY, value TEXT);
2、记录客户端信息的clients
CREATE TABLE clients (
sock INTEGER,
id TEXT PRIMARY KEY,
clean_start INTEGER,
will INTEGER,
will_retain INTEGER,
will_qos INTEGER,
will_topic TEXT,
will_message TEXT,
last_mid INTEGER
);
3、订阅信息subs
CREATE TABLE subs ( client_id TEXT, sub TEXT, qos INTEGER);
4、持久化消息retain
CREATE TABLE retain (
sub TEXT,
qos INTEGER,
payloadlen INTEGER,
payload BLOB
);
5、客户端消息messages
CREATE TABLE messages (
client_id TEXT,
timestamp INTEGER,
direction INTEGER,
status INTEGER,
mid INTEGER,
dup INTEGER,
qos INTEGER,
retain INTEGER,
sub TEXT,
payloadlen INTEGER,
payload BLOB
);
typedef struct {
int port;
int msg_timeout;
int persistence;
char *persistence_location;
int sys_interval;
char *pid_file;
} mqtt3_config;
分别代表:TCP服务端监听端口; 发出消息无回应超时时间; 数据库是否保存及保存位置(不保存则存在于内存中);系统消息更新的时间间隔;PID文件(后台运行时使用)。
typedef struct _mqtt3_context{
int sock;
time_t last_msg_in;
time_t last_msg_out;
uint16_t keepalive;
bool clean_start;
char *id;
} mqtt3_context;
分别代表:socket fd;在该客户端socket上最近发送时间;最近接收时间;心跳周期;客户端connect flag之clean session;客户端id。
mqtt3_context方法:mqtt3_context_init:初始化,mqtt3_context_cleanup:关闭socket;如果clean_start置上,则还需要删除subs、messages、clients中与对应客户端有关的行
typedef enum {
ms_invalid = 0,
ms_publish = 1,
ms_publish_puback = 2,
ms_wait_puback = 3,
ms_publish_pubrec = 4,
ms_wait_pubrec = 5,
ms_resend_pubrel = 6,
ms_wait_pubrel = 7,
ms_resend_pubcomp = 8,
ms_wait_pubcomp = 9
} mqtt3_msg_status;
初始化除client_id之外的成员,client_id是应用层属性,只有在mqtt connect之后才可以获取(实际上还包括clean_start,初始化时默认置1)。
使用场景:收到新的客户端TCP连接时。
关闭该客户端socket。如果clean_start为true,则还需要清理客户端、订阅、消息,即删除subs、messages、clients中有关行。
使用场景:
类似的函数mqtt3_db_client_delete等。组合方式为:
mqtt3_db_{TABLE}_{ACTION},顾名思义,这类函数的作用是操作数据库表。
SELECT qos,payloadlen,payload FROM retain WHERE sub=?
使用关键字sub从retain表中搜索。
功能:将指定的消息加入队列,涉及数据库表messages和retain。
mqtt3_db_message_insert():将消息插入messages表,mqtt3_db_message_insert原型如下,其中的client_id来自于subs匹配行的client_id列(订阅者id)
int mqtt3_db_message_insert(const char *client_id, uint16_t mid, mqtt3_msg_direction dir, mqtt3_msg_status status, int retain, const char *sub, int qos, uint32_t payloadlen, const uint8_t *payload)
注意:该函数完成的插入操作,消息类型均为md_out。
mqtt3_config_read
读取系统配置文件,完成mqtt3_config的初始化(未提供配置时使用默认配置,例如默认监听端口1883)
mqtt3_db_open
mqtt3_socket_listen
开启TCP监听(默认端口1883)。
系统初始化完成后,进入处理循环。
收到新的TCP connect之后,accept并且调用mqtt3_context_init进行context初始化。
TCP socket读数据在handle_read()中根据mqtt报文类型进行处理。Broker可能收到的mqtt报文有connect、publish、puback、pubrec、pubrel、pubcomp、subscribe、unsubscribe、ping、disconnect共10种(不含错误的报文)。
【协议定义】客户端的connect应该回复ack,一个connect及ack命令示例:
0000 10 20 00 06 4d 51 49 73 64 70 03 02 00 3c 00 12
conn,len=0x20,6,MQIsdp V03,flag=2, kl=0x003c, id_len=18
0010 6d 6f 73 71 75 69 74 74 6f 5f 70 75 62 5f 34 32 34 37 mosquitto_pub_4247
ACK: 20 conn ack
02 00 00 len=2, reserved, accept
【处理】mqtt3_handle_connect为处理客户端mqtt connect的函数,最重要的是两个任务:
(1)设置context id域;
(2)将(发起mqtt connect的)客户端信息插入表clients。
context->id = client_id;
mqtt3_db_client_insert(context, will, will_retain, will_qos, will_topic, will_message);
mqtt3_raw_connack为回复conn ack的函数。该函数回复:20 02 00 00,4字节发送的方式是多次(4次,实际在TCP上是一次发送-如果Nagle算法未关闭的话),Wireshark抓包工具能够看出内容为一个TCP段mqtt connect ack。
【示例】客户端 connect时,mosquitto运行日志(在源码上增加的调试信息):
查看sqlite3数据库clients表:
【协议定义】disconnect消息:e0 00,不需要应答。
【处理】 mqtt3_handle_disconnect调用mqtt3_socket_close
int mqtt3_socket_close(mqtt3_context *context){
……
mqtt3_db_client_invalidate_socket(context->id, context->sock);
rc = close(context->sock);
context->sock = -1;
……
}
【协议定义】对客户端的ping request(c0 00)直接回复ping response(d0 00)。理论上broker收不到pong,因此即使收到,就简单丢弃。
【协议定义】subscribe需要ack,一个例子如下:
REQ
0000 82 08 00 01 00 03 6d 73 67 00
sub+qos=1, msglen=8, mid=1, tlen=3, topic="msg", req_qos=0
ACK
0000 90 03 00 01 00
Sub ack, len=3, mid=1, qos=0
【处理】int mqtt3_handle_subscribe(mqtt3_context *context)是处理客户端订阅的函数:
在MQTT协议里关于消息的持久化规定对于持久的、最新一条PUBLISH消息,服务器要马上推送给新的订阅者(注:仅最新的一条,不是所有)。
【示例】1:mqtt sub(-t msg)之后:
查看subs有该订阅信息:
【示例】2:有持久消息时,订阅者会收到推送,如下,retain存有主题为/messages/vb的消息:
在订阅(-t /messages/vb)之后立即收到该消息:
该新订阅者除了收到sub ack之外还收到了retain消息(qos=0):ret_vb,如下:
【协议定义】取消订阅需要ack。
【处理】int mqtt3_handle_unsubscribe(mqtt3_context *context)调用mqtt3_db_sub_delete删除subs表相应行。
注意:订阅者单纯的TCP断开,不会发送unsubscribe消息(因为根本来不及完成这个交互)。
【协议定义】publish有retain标志;publish根据QoS有不同处理:
1.QoS=0,broker应立即把消息推送给订阅者,不回ack,一个例子如下:
0000 30 0b 00 03 6d 73 67 71 30 5f 6d 73 67
Pub+qos=0,len=11,topic_len=3,topic="msg", content:"q0_msg"
2.QoS=1,broker应立即把消息推送给订阅者,并回复ACK,一个例子如下:
REQ:
0000 32 0d 00 03 6d 73 67 00 01 71 31 5f 6d 73 67
Pub+qos=1,len=13,topic_len=3,topic="msg",mid=0001, content:"q1_msg"
ACK:
0000 40 02 00 01
Puback, len=2, mid=0001
3.QoS=2,pub-recv-rel-comp,broker首先把消息暂存,然后经过recv-rel-comp握手之后,再把消息推送给订阅者。一个例子如下:
0000 34 0d 00 03 6d 73 67 00 01 71 32 5f 6d 73 67 C to S
0000 34 0d 00 03 6d 73 67 00 01 71 32 5f 6d 73 67 C to S
Pub+qos=2,len=13,topic_len=3,topic="msg",mid=0001, content:"q2_msg"
0000 50 02 00 01 S to C
Pub received
0000 62 02 00 01 C to S
Pub release
0000 70 02 00 01 S to C
Pub complete
【处理】mqtt3_handle_publish调用mqtt3_db_messages_queue或mqtt3_db_message_insert更新数据库,调用mqtt3_raw_XX回复客户端。
int mqtt3_handle_publish(mqtt3_context *context, uint8_t header)
{
……
switch(qos){
case 0:
if(mqtt3_db_messages_queue(sub, qos, payloadlen, payload, retain)) rc = 1;
break;
case 1:
if(mqtt3_db_messages_queue(sub, qos, payloadlen, payload, retain)) rc = 1;
if(mqtt3_raw_puback(context, mid)) rc = 1;
break;
case 2:
if(mqtt3_db_message_insert(context->id, mid, md_in, ms_wait_pubrec, retain, sub, qos, payloadlen, payload)) rc = 1;
if(mqtt3_raw_pubrec(context, mid)) rc = 1;
break;
}
……
}
区分QoS:
注意:发布者在发布之后,都会disconnect断连。这一点和订阅者不同,订阅者是和服务端保持长连接的!
【协议定义】什么情况下broker会收到puback并且需要处理呢?
---在broker向订阅客户publish QoS=1的消息时,收到回复puback表示客户端收到该消息。
【处理】mqtt3_handle_puback调用mqtt3_handle_puback (client_id, mid, dir)从messages中删除相应一条消息:client_id==订阅者的id。表示推送完成不再需要重复推送,因此从数据库中删除。
【示例】1:
客户端 id_sub01订阅:-t topic01
客户端id_pub01发布消息主题为msg,内容为:this is msg from pub01 qos1
根据就低原则(参见下面描述),id_sub01只会收到broker的publish(QoS=0),不需要回复puback:
【示例】2:客户端 id_sub01订阅:-t topic02 –q 1
客户端id_sub01在id_pub01发布QoS=1的消息后收到的是QoS=1的消息
【协议定义】什么情况下broker会收到pubrec和pubcomp?
---当broker向客户端publish QoS=2的消息时,收到客户端的回应不是puback,而是pubrec,表示客户端收到了publish消息,broker将回复pubrel,客户端收到后回复pubcomp。
【处理】
·调用mqtt3_db_message_update,根据参数更新messages中消息,设置消息状态为
ms_wait_pubcomp,同时更新时间戳,SQL语句可以伪码描述为:
UPDATE messages SET status=ms_wait_pubcomp, timestamp=now
WHERE client_id={client_id} AND mid={mid} AND direction=md_out
·调用mqtt3_raw_pubrel给客户端回复pubrel
·调用mqtt3_db_message_delete依据参数删除messages中消息。
DELETE FROM messages WHERE client_id={client_id} AND mid={mid} AND direction=md_out
【协议定义】什么情况下broker会收到pubrel?
---当客户端向broker publish QoS=2的消息时,broker首先回复pubrec,客户端收到后回复pubrel,broker应回复pubcomp。
【处理】mqtt3_handle_pubrel对收到pubrel进行处理:
·调用mqtt3_db_message_release:找到publish时暂存的md_in消息,取出其内容,并调用mqtt3_db_messages_queue将publish消息插入messages(以及retain如果是持久消息),完成后删除暂存md_in消息;
·调用mqtt3_raw_pubcomp回复pubcomp
【处理】mqtt3_handle_unsubscribe调用mqtt3_db_sub_delete从subs中删除客户端指定topic的订阅信息,并(直接组织应答报文)回复ack。
mqtt3_db_outgoing_check发送数据准备
int mqtt3_db_outgoing_check(fd_set *writefds, int *sockmax)
使用JION语句
SELECT sock FROM clients JOIN messages ON clients.id=messages.client_id WHERE (messages.status=1 OR messages.status=2 OR messages.status=4 OR messages.status=6 OR messages.status=8) AND messages.direction=1 AND sock<>-1
找出“有哪些messages要发送到哪些客户端”。
这里关键是clients.id=messages.client_id,怎么理解?
由此可见,通过这个sql语句,找到了本条messages有哪些订阅者,下一步的动作是推送给这些订阅者,在mqtt3_db_outgoing_check中的处理是找到这些订阅者的socket fd,并加入FD_SET中(FD_ISSET返回真),在接下来的pselect循环会进行处理。
messages.direction=1指的是md_out,发出消息。
messages.status=1 OR messages.status=2 OR messages.status=4 OR messages.status=6 OR messages.status=8 指的是下列状态:
mqtt3_db_message_write发送消息
在经过select及FD_ISSET后,已经进入了某个客户端的发送。mqtt3_db_message_write为完成发送的执行体。
SQL语句select绑定的是client_id,根据messages表中现有待发出的消息的状态,有不同的动作:
switch(status){
case ms_publish:
if(!mqtt3_raw_publish(context, false, qos, retain, mid, sub, payloadlen, payload)){
mqtt3_db_message_delete_by_oid(OID);
}
break;
case ms_publish_puback:
if(!mqtt3_raw_publish(context, false, qos, retain, mid, sub, payloadlen, payload)){
mqtt3_db_message_update(context->id, mid, md_out, ms_wait_puback);
}
break;
case ms_publish_pubrec:
if(!mqtt3_raw_publish(context, false, qos, retain, mid, sub, payloadlen, payload)){
mqtt3_db_message_update(context->id, mid, md_out, ms_wait_pubrec);
}
break;
case ms_resend_pubrel:
if(!mqtt3_raw_pubrel(context, mid)){
mqtt3_db_message_update(context->id, mid, md_out, ms_wait_pubrel);
}
break;
case ms_resend_pubcomp:
if(!mqtt3_raw_pubcomp(context, mid)){
mqtt3_db_message_update(context->id, mid, md_out, ms_wait_pubcomp);
}
break;
}
如前所述,客户端断开TCP,不会引起mqtt层相应操作,但broker会完成清理:
删除该订阅者的信息(clients、subs、messages),有遗言的把遗言加入messages(如果遗言持久化消息,还会保存到retain供后续订阅者使用)。
示例1:
Mosquitto记录在每个客户端socket上最近一次收到报文时间,按照1.5倍心跳周期内没有收到客户端任何数据作为客户端链路超时的依据,超时发生时主动断开和该客户端TCP连接。
【协议定义】不同于上面的socket链路超时,这里指的是mqtt协议层超时,即客户端可能还会发出ping,但是对broker的推送消息(QoS=1)不予回复ACK。
【处理】mqtt3_db_message_timeout_check使用两条SQL语句
SELECT OID,status FROM messages WHERE timestamp < time(NULL) - timeout
timestamp < time(NULL) – timeout 即time(NULL) - timestamp > timeout <,含义是:当前时间距离消息的时间戳已经超过了timeout。
UPDATE messages SET status=?,dup=1 WHERE OID=?
其中状态status变化为:
ms_wait_puback --> ms_publish_puback
ms_wait_pubrec --> ms_publish_pubrec
ms_wait_pubrel -->ms_resend_pubrel
ms_wait_pubcomp --> ms_resend_pubcomp
Cli send |
clients |
subs(key:sub) |
messages |
retain |
connect |
插入/更新 |
无 |
无 |
无 |
disconnect |
依据id和sock,重置sock = -1 |
无 |
无 |
无 |
TCP disconnect |
删除 |
删除 |
1、删除客户端相关联消息; 2、插入客户端的遗言消息 |
插入(订阅/发布)客户的遗言持久化消息 |
subscribe |
无 |
插入 |
依据右边表格(retain sub查询结果)插入mqtt3_db_messages_queue(retain_msg) md_out消息(QoS决定了消息状态) |
1、关键字sub查询,并给左边表格使用 2、带遗言持久化的订阅会插入行 |
unsubscribe |
无 |
删除 |
无 |
无 |
publish |
无 |
查询是否有订阅者,结果供右边表格使用 |
QoS=0/1:依据左边表格结果插入的mid_out消息; QoS=2:先插入md_in, ms_wait_pubrec消息,在握手完成后删除该临时消息,并插入publish消息md_out |
发布的消息是retain型则插入 |
ping |
无 |
无 |
无 |
无 |
|
方向 |
connect |
上行(即客户端到服务器) |
disconnect |
上行(即客户端到服务器) |
TCP disconnect |
双向 |
subscribe |
上行 |
unsubscribe |
上行 |
publish |
双向。Publish可以发送消息到broker,broker也使用mqtt publish把来自其他客户端的消息推送给subscribe |
ping |
上行 |
|
|
演示如下模型:
1.客户端sub01订阅主题msg,使用QoS=2;
./sub -i sub01 -t msg -q 2 -h 192.168.122.21 --will-qos 2 --will-retain --will-topic msg --will-payload "this is a will-retain qos=2 msg from sub01"&
2.客户端pub01发布主题msg QoS=2 retain=1消息”this is qos=2 topic=msg from pub01” ,will:QoS=2,retain=1,topic=msg, msg=”this is a will-retain qos=2 msg from pub01”
./pub -i pub01 -t msg -q 2 -r -h 192.168.122.21 --will-topic msg -m "this is qos=2 topic=msg from pub01" --will-qos 2 --will-retain --will-payload "this is a will-retain qos=2 msg from pub01"
3.客户端sub02订阅主题msg,使用QoS=2;
./sub -i sub02 -t msg -q 2 -h 192.168.122.21&
4.异常结束sub01
消息交互流程:
匆匆完成剖析,感觉意犹未尽。有如下几个问题,值得进一步思考: