Dotnetty项目提供了mqtt的编码和解码,但没有提供mqtt客户端和服务端的例子,Azure的另一个项目 azure-iot-protocol-gateway 是基于 dotnetty 实现的 mqtt 网关,该项目用途是设备通过 mqtt 与 网关通讯,网关再通过AMQP协议与 Azure IoT Hub 通讯,从而实现了设备与hub的桥接。本文通过翻译文档的部分内容,来说明 mqtt server 的主要逻辑。
一、启动服务端
return new ServerBootstrap()
.Group(this.parentEventLoopGroup, this.eventLoopGroup)
.Option(ChannelOption.SoBacklog, ListenBacklogSize)
.Option(ChannelOption.AutoRead, false)
.ChildOption(ChannelOption.Allocator, UnpooledByteBufferAllocator.Default)
.ChildOption(ChannelOption.AutoRead, false)
.Channel<TcpServerSocketChannel>()
.Handler(acceptLimiter)
.ChildHandler(new ActionChannelInitializer<ISocketChannel>(channel =>
{
channel.Pipeline.AddLast(
TlsHandler.Server(this.tlsCertificate),
new AcceptLimiterTlsReleaseHandler(acceptLimiter),
MqttEncoder.Instance,
new MqttDecoder(true, maxInboundMessageSize),
new MqttAdapter(
this.settings,
this.sessionStateManager,
this.authProvider,
this.qos2StateProvider,
bridgeFactory));
}));
- 程序入口处初始化 MqttAdapter(处理 Mqtt 的 Handler) 所需的参数(settings, sessionStateManager, authProvider, qos2StateProvider, bridgeFactory);
- 执行new ServerBootstrap(),即上面代码部分,属于 netty 服务端的常规套路;
Bootstrapper
根据 CPU 核数来初始化 EventLoopGroup;Bootstrapper
创建并配置ServerBootstrap
.TcpServerSocketChannel 用作监听通道.
将 ActionChannelInitializer<ISocketChannel>
添加到监听通道的 管道(pipeline)中,当客户端连接通道建立后,为其创建管道,管道中的handler有:TlsHandler
<->MqttEncoder
<->MqttDecoder
<->MqttAdapter
.
Bootstrapper
调用ServerBootstrap.BindAsync
:- 创建一个监听通道实例
- 将监听通道注册到Event Loop
- 将
ServerBootstrapperAcceptor
添加到监听通道的 管道(
译注:见Dotnetty 代码 ServerBootstrap init)
- 调用监听通道的BindAsync方法
- 监听通道内部创建socket对象,绑定到地址/端口。
二、建立连接
- 一旦接收到一个客户端socket连接,监听通道创建
TcpSocketChannel
(客户端通道),并将TcpSocketChannel传给自己管道的
ServerBootstrapAcceptor
. ServerBootstrapAcceptor
将TcpSocketChannel
加入Event Loop,并将ActionChannelInitializer添加到TcpSocketChannel的管道中
.ActionChannelInitializer
将自己从管道中移除,并顺次执行管道中的handler(TlsHandler
<->MqttEncoder
<->MqttDecoder
<->MqttAdapter)
.- 如果有
TlsHandler
, 会执行TLS握手,不向后续handler发送数据. - 如果有
MqttAdapter
, 会在ChannelActive时调用context.Read().TlsHandler
记录该请求但在TLS握手完成前不会放行该请求. - 一旦TLS握手完成,
TlsHandler
放行上一步的读请求,TcpSocketChannel
从 socket 读取数据. - socket收到数据后放入
ByteBuffer,发送给管道
. TlsHandler
收到数据后解析SSL帧. 如果数据不足帧的预期长度, 追加读请求. 否则解密帧,将解密数据发给下一个handler.MqttEncoder
不属于InBound handler(因为没有重载任何 inbound 方法),跳过.MqttDecoder
接收解密数据,尝试解析 MQTT 包(packet). 如果数据不足,向前一个handler 追加读请求。一旦解析到MQTT包,发送到下一个handler. 根据MQTT规范, 客户端首先发送 CONNECT 包. 我们假设MqttDecoder
首先解析到的是 CONNECT 包.MqttAdapter
收到 CONNECT 包.MqttAdapter
创建连接(译注:本段是 Azure IoT Hub 的建立连接步骤,仅供参考):MqttAdapter
调用IDeviceIdentityProvider 使用
CONNECT包的client ID, user name, and password认证设备.IDeviceIdentityProvider
返回用于与IoT Hub建立连接的设备信息.MqttAdapter
创建 IoT Hub 连接,如果成功,返回一个IDeviceClient
对象用于将来跟 IoT Hub 通讯.MqttAdapter
调用ISessionStateManager
查询设备的缓存会话. 如果CONNECT包的 CleanSession为真,MqttAdapter
会删除缓存会话.MqttAdapter 开始从
IoT Hub接收消息.MqttAdapter 调用
context.WriteAsync() 发送
CONNACK 包.
- CONNACK 包发送给前一个handler.
MqttDecoder
不属于OutBound handler(因为没有重写任何
outbound 方法), 跳过.MqttEncoder
编码 CONNACK 包到字节数组并发给TlsHandler
.TlsHandler
加密 CONNACK 包为 SSL 帧并放入字节数组.TlsHandler
是打头的handler,因此字节数组被发送到socket对象. 须知,实际发送行为在调用Channel.Flush()发生
.- 在
MqttAdapter处理
CONNECT包时,如果又来了其他包,会被入队到 CONNACK 成功发给设备后再处理.
三、设备发布数据到网关
- 当设备发送 PUBLISH 包给网关时, 步骤同处理 CONNECT 包的 7-10 步.
- 一旦
MqttAdapter 收到
PUBLISH 包, 就发给MessageAsyncProcessor
做顺序异步处理. MessageAsyncProcessor
处理完再回调MqttAdapter.PublishToServerAsync
.MqttAdapter
根据PUBLISH包创建 Iot Hub 消息.MqttAdapter
从PUBLISH 包解析数据.MqttAdapter
将数据加到消息属性并发给IDeviceClient(
上面第12步创建).- 如果QoS=1 (最少一次),
MqttAdapter
会发送 PUBACK 包, 步骤类似于上面的第 13-16 步.
四、网关从IoT Hub接收数据
一旦 MqttAdapter
与 IoT hub 建立接收链路, 就可以在任何时间接收到消息,然后:
MqttAdapter
找到对应的topic,准备 PUBLISH 给设备.(译注:设备对应的 PUBLISH topic,形如 ‘devices/{deviceId}/messages/events’)MqttAdapter
根据topic找设备的订阅列表,如果没有设备订阅该主题,则拒绝处理该消息..- 根据匹配的订阅和 QoS,分别处理.
- 如果 QoS=0,
MqttAdapter发送
PUBLISH 包(见上面的第 13-16 步)- 同时从 IoT hub 删除该消息.
- 如果 QoS=1,
MqttAdapter 给RequestAckPairProcessor
发送
PUBLISH 包,进行 PUBLISH-PUBACK 通讯.RequestAckPairProcessor
发送 PUBLISH 包(见上面的第 13-16 步).- 同时,
RequestAckPairProcessor
入队消息关键字. - 正常情况下, PUBLISH对应的 PUBACK 包回到
MqttAdapter
. MqttAdapter
将 PUBACK 包转给RequestAckPairProcessor
.RequestAckPairProcessor
从队列中 匹配消息,如果没匹配到就丢弃 PUBACK 包,停止后续处理.RequestAckPairProcessor
出队消息,调用MqttAdapter
通知处理完成.MqttAdapter
从IoT hub删除消息.
- 如果 QoS=2,
MqttAdapter 给RequestAckPairProcessor
发送
PUBLISH 包,进行 PUBLISH-PUBREC 通讯.RequestAckPairProcessor
发送 PUBLISH 包(见上面的第 13-16 步).- 同时,
RequestAckPairProcessor
入队消息关键字. - 正常情况下, PUBLISH对应的 PUBREC 包回到
MqttAdapter
. MqttAdapter
将 PUBREC 包转给RequestAckPairProcessor
.RequestAckPairProcessor
从队列中 匹配消息,如果没匹配到就丢弃 PUBREC 包,停止后续处理.RequestAckPairProcessor
出队消息,调用MqttAdapter
通知处理完成.MqttAdapter
调用IQos2StatePersistenceProvider.SetMessageAsync 持久化
PUBLISH 包已处理完毕.MqttAdapter
发送 PUBREL 包给RequestAckPairProcessor
进行 PUBREL-PUBCOMP 通讯.RequestAckPairProcessor
发送 PUBREL 包(见上面的第 13-16 步).- 同时,
RequestAckPairProcessor
入队消息关键字. - 正常情况下, PUBREL对应的 PUBCOMP 包回到
MqttAdapter
. MqttAdapter
将 PUBCOMP 包转给RequestAckPairProcessor
.RequestAckPairProcessor
从队列中 匹配消息,如果没匹配到就丢弃 PUBCOMP 包,停止后续处理.RequestAckPairProcessor
出队消息,调用MqttAdapter
通知处理完成.MqttAdapter
从IoT hub删除消息.MqttAdapter
调用IQos2StatePersistenceProvider.DeleteMessageAsync
删除 QoS 2 记录.
总结
azure-iot-protocol-gateway 项目在dotnetty基础上,实现了azure mqtt 网关(一种特殊的服务端),其代码可作为基于dotnetty 开发mqtt服务端的参考。