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服务端的参考。