订阅树的概念
Mosquitto通过订阅树的方式来管理所有的topic以及客户端的订阅关系,它首先将所有的topic按照/分割并组织成一棵树结构,从根节点到树中的每个节点即组成该节点所对应的一个topic,每个topic都保存一个订阅列表,该订阅列表中保存了所有订阅当前topic的客户端信息。例如有如下订阅关系:
客户端a1,a2,a3订阅了topic:A1/B1/C1m
客户端b1,b2订阅了topic:A2/B2/C2
客户端c1,c2订阅了topic:A1/B1/C3
客户端d1订阅了topic:A2/B3
则上述订阅树如图。
Mosquitto程序在实现中根据topic消息的性质将订阅树分为两颗子树:业务子树和系统子树;mosquitto程序中将topic分为两种类型来处理:系统topic和业务topic,前者主要用于发布和维护mosquitto内部的系统消息,后者的topic是用户订阅的业务topic,做这种区分的原因是因为这两种的类型的topic性质和实现方式上有许多差别,这种差别主要体现在以下3点:
1)生存周期不同,系统topic无论是否有用户订阅都会存在与订阅树中,而业务topic必须有客户端订阅才能存在(除非其消息字段retain设置为1)。
2)创建方式不同,系统topic在消息发布时进行创建,业务topic即可以在订阅时创建也可以在消息发布时创建(此时需要该消息retain字段设置为1)。
3)消息保存方式不同,凡是发布到系统topic的消息都会被保存下来,业务消息将直接挂到订阅列表的各context的消息队列中,如果没有连接订阅或未设置其retain字段,消息将不会被保存下来,消息的retain字段是否被设置在函数mqtt3_handle_publish进行检查。
订阅树的创建:(在src/database.c中的mqtt3_db_open函数实现)
mosquitto程序启动时将创建订阅树,该过程将创建三个节点:订阅树总根节点、业务子树根节点和系统子树根节点,这两个子树根节点作为订阅树总根节点的两个子节点,其中订阅树总根节点和业务子树的根节点中topic成员的值为空字符串,而系统子树根节点中保存的值为“$SYS”,如图:
搭建订阅树
1) 系统子树搭建过程
Mosquitto中,系统子树在发布系统消息时,自动检测topic片段是否存在,如果不存在则在系统topic上创建节点以搭建订阅树。搭建过程如下:
将Topic按照“/"分成 topic片段;根据第一个topic片段“$SYS”遍历订阅树的子节点找到系统子树的根节点;根据topic下一个片段查找系统子树,若没有则创建这个节点,依次方案处理直至topic片段解析完。
所用到的函数调用: mqtt3_db_messages_easy_queue(在src/database.c中) --->mqtt3_db_messages_queue (在src/subs.c中) ---> _sub_add(在src/subs.c中)
2)业务子树搭建过程
分为两种类型:订阅时创建和消息发布时创建。后者与系统Topic的方式类似。前者在收到订阅请求后将该客户端挂到对应的业务子树节点的订阅列表中,若不存在客户端所订阅的Topic,则会自动为之添加相应节点。
所用到的函数调用: mqtt3_handle_subscribe(在src/read_handle_server.c中) --->mqtt3_sub_add(在src/subs.c中) --->_sub_add(在src/subs.c中)
可以看到,在上面都使用了_sub_add函数,而调用它的分别是mqtt3_db_messages_queue 和mqtt3_sub_add函数,而且这三个函数都是在src/subs.c中,不妨来看看它们的逻辑。
mqtt3_db_messages_queue:(系统子树的搭建)
这几张图把订阅树的构建的大致逻辑勾勒出来。可以看见业务子树和系统子树的搭建大致逻辑相似,但是在局部处理上还是有区别,最大的区别就是如果创建业务子树的时候如果有没有找到topic片段,则会向订阅树中添加相应节点,而创建系统子树时则不会(原因见前面提到的二者的区别的第二点)。
还有就是这里在根据用户发布的topic(一个字符串)来在树结构中查询,用到了一个技巧,就是调用_sub_topic_tokenise函数将这个字符串分解,并组成一个链表的形式,然后通过遍历这个链表逐步完成对树结构的查询、添加等工作,最后释放掉这个链表。这里的链表就相当于一个缓冲区,值得借鉴。
例如一个Topic: year/month/day
就被转换为如下链表结构: