一、DotNetty背景介绍

某天发现 dotnet  是个好东西,就找了个项目来练练手。于是有了本文的 Mqtt 客户端   (github:  MqttFx )

DotNetty是微软的Azure团队,使用C#实现的Netty的版本发布。不但使用了C#和.Net平台的技术特点,并且保留了Netty原来绝大部分的编程接口。让我们在使用时,完全可以依照Netty官方的教程来学习和使用DotNetty应用程序。

DotNetty同时也是开源的,它的源代码托管在Github上: https://github.com/azure/dotnetty

Netty 的官方文档 : http://netty.io/wiki/all-documents.html

二、Packet

    套件里是有个 DotNetty.Codecs.Mqtt, 本项目没有使用。直接写了一个。

FixedHeader:  固定报头

    /// <summary>
/// 固定报头
/// </summary>
public class FixedHeader
{
/// <summary>
/// 报文类型
/// </summary>
public PacketType PacketType { get; set; }
/// <summary>
/// 重发标志
/// </summary>
public bool Dup { get; set; }
/// <summary>
/// 服务质量等级
/// </summary>
public MqttQos Qos { get; set; }
/// <summary>
/// 保留标志
/// </summary>
public bool Retain { get; set; }
/// <summary>
/// 剩余长度
/// </summary>
public int RemaingLength { internal get; set; } public FixedHeader(PacketType packetType)
{
PacketType = packetType;
} public FixedHeader(byte signature, int remainingLength)
{
PacketType = (PacketType)((signature & 0xf0) >> 4);
Dup = ((signature & 0x08) >> 3) > 0;
Qos = (MqttQos)((signature & 0x06) >> 1);
Retain = (signature & 0x01) > 0;
RemaingLength = remainingLength;
} public void WriteTo(IByteBuffer buffer)
{
var flags = (byte)PacketType << 4;
flags |= Dup.ToByte() << 3;
flags |= (byte)Qos << 1;
flags |= Retain.ToByte(); buffer.WriteByte((byte)flags);
buffer.WriteBytes(EncodeLength(RemaingLength));
} static byte[] EncodeLength(int length)
{
var result = new List<byte>();
do
{
var digit = (byte)(length % 0x80);
length /= 0x80;
if (length > 0)
digit |= 0x80;
result.Add(digit);
} while (length > 0); return result.ToArray();
}
}

  

Packet:  消息基类

    /// <summary>
/// 消息基类
/// </summary>
public abstract class Packet
{
#region FixedHeader /// <summary>
/// 固定报头
/// </summary>
public FixedHeader FixedHeader { protected get; set; }
/// <summary>
/// 报文类型
/// </summary>
public PacketType PacketType => FixedHeader.PacketType;
/// <summary>
/// 重发标志
/// </summary>
public bool Dup => FixedHeader.Dup;
/// <summary>
/// 服务质量等级
/// </summary>
public MqttQos Qos => FixedHeader.Qos;
/// <summary>
/// 保留标志
/// </summary>
public bool Retain => FixedHeader.Retain;
/// <summary>
/// 剩余长度
/// </summary>
public int RemaingLength => FixedHeader.RemaingLength; #endregion public Packet(PacketType packetType) => FixedHeader = new FixedHeader(packetType); public virtual void Encode(IByteBuffer buffer) { } public virtual void Decode(IByteBuffer buffer) { }
}

  

PacketWithId: 消息基类(带ID)

    /// <summary>
/// 消息基类(带ID)
/// </summary>
public abstract class PacketWithId : Packet
{
public PacketWithId(PacketType packetType) : base(packetType)
{
} /// <summary>
/// 报文标识符
/// </summary>
public ushort PacketId { get; set; } /// <summary>
/// EncodePacketIdVariableHeader
/// </summary>
/// <param name="buffer"></param>
public override void Encode(IByteBuffer buffer)
{
var buf = Unpooled.Buffer();
try
{
EncodePacketId(buf); FixedHeader.RemaingLength = buf.ReadableBytes;
FixedHeader.WriteTo(buffer);
buffer.WriteBytes(buf);
buf = null;
}
finally
{
buf?.Release();
}
} /// <summary>
/// DecodePacketIdVariableHeader
/// </summary>
/// <param name="buffer"></param>
public override void Decode(IByteBuffer buffer)
{
int remainingLength = RemaingLength;
DecodePacketId(buffer, ref remainingLength);
FixedHeader.RemaingLength = remainingLength;
} protected void EncodePacketId(IByteBuffer buffer)
{
if (Qos > MqttQos.AtMostOnce)
{
buffer.WriteUnsignedShort(PacketId);
}
} protected void DecodePacketId(IByteBuffer buffer, ref int remainingLength)
{
if (Qos > MqttQos.AtMostOnce)
{
PacketId = buffer.ReadUnsignedShort(ref remainingLength);
if (PacketId == )
throw new DecoderException("[MQTT-2.3.1-1]");
}
}
}

ConnectPacket: 发起连接包

    /// <summary>
/// 发起连接
/// </summary>
internal sealed class ConnectPacket : Packet
{
public ConnectPacket()
: base(PacketType.CONNECT)
{
} #region Variable header /// <summary>
/// 协议名
/// </summary>
public string ProtocolName { get; } = "MQTT";
/// <summary>
/// 协议级别
/// </summary>
public byte ProtocolLevel { get; } = 0x04;
/// <summary>
/// 保持连接
/// </summary>
public short KeepAlive { get; set; } #region Connect Flags
/// <summary>
/// 用户名标志
/// </summary>
public bool UsernameFlag { get; set; }
/// <summary>
/// 密码标志
/// </summary>
public bool PasswordFlag { get; set; }
/// <summary>
/// 遗嘱保留
/// </summary>
public bool WillRetain { get; set; }
/// <summary>
/// 遗嘱QoS
/// </summary>
public MqttQos WillQos { get; set; }
/// <summary>
/// 遗嘱标志
/// </summary>
public bool WillFlag { get; set; }
/// <summary>
/// 清理会话
/// </summary>
public bool CleanSession { get; set; }
#endregion #endregion #region Payload /// <summary>
/// 客户端标识符 Client Identifier
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// 遗嘱主题 Will Topic
/// </summary>
public string WillTopic { get; set; }
/// <summary>
/// 遗嘱消息 Will Message
/// </summary>
public byte[] WillMessage { get; set; }
/// <summary>
/// 用户名 User Name
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 密码 Password
/// </summary>
public string Password { get; set; } #endregion public override void Encode(IByteBuffer buffer)
{
var buf = Unpooled.Buffer();
try
{
//variable header
buf.WriteString(ProtocolName); //byte 1 - 8
buf.WriteByte(ProtocolLevel); //byte 9 //connect flags; //byte 10
var flags = UsernameFlag.ToByte() << ;
flags |= PasswordFlag.ToByte() << ;
flags |= WillRetain.ToByte() << ;
flags |= ((byte)WillQos) << ;
flags |= WillFlag.ToByte() << ;
flags |= CleanSession.ToByte() << ;
buf.WriteByte((byte)flags); //keep alive
buf.WriteShort(KeepAlive); //byte 11 - 12 //payload
buf.WriteString(ClientId);
if (WillFlag)
{
buf.WriteString(WillTopic);
buf.WriteBytes(WillMessage);
}
if (UsernameFlag && PasswordFlag)
{
buf.WriteString(UserName);
buf.WriteString(Password);
} FixedHeader.RemaingLength = buf.ReadableBytes;
FixedHeader.WriteTo(buffer);
buffer.WriteBytes(buf);
}
finally
{
buf?.Release();
buf = null;
}
}
}

连接回执: ConnAckPacket

    /// <summary>
/// 连接回执
/// </summary>
internal sealed class ConnAckPacket : Packet
{
public ConnAckPacket() : base (PacketType.CONNACK)
{
} /// <summary>
/// 当前会话
/// </summary>
public bool SessionPresent { get; set; }
/// <summary>
/// 连接返回码
/// </summary>
public ConnectReturnCode ConnectReturnCode { get; set; } public override void Decode(IByteBuffer buffer)
{
SessionPresent = (buffer.ReadByte() & 0x01) == ;
ConnectReturnCode = (ConnectReturnCode)buffer.ReadByte();
}
}

剩余几个包,,大家看看源码。

三、包解码编码 MqttDecoder  MqttEncoder  

粘包拆包问题是处于网络比较底层的问题,在数据链路层、网络层以及传输层都有可能发生。我们日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生这个问题。

什么是粘包、拆包?

对于什么是粘包、拆包问题,我想先举两个简单的应用场景:

  1. 客户端和服务器建立一个连接,客户端发送一条消息,客户端关闭与服务端的连接。

  2. 客户端和服务器简历一个连接,客户端连续发送两条消息,客户端关闭与服务端的连接。

对于第一种情况,服务端的处理流程可以是这样的:当客户端与服务端的连接建立成功之后,服务端不断读取客户端发送过来的数据,当客户端与服务端连接断开之后,服务端知道已经读完了一条消息,然后进行解码和后续处理...。对于第二种情况,如果按照上面相同的处理逻辑来处理,那就有问题了,我们来看看第二种情况下客户端发送的两条消息递交到服务端有可能出现的情况:

第一种情况:

服务端一共读到两个数据包,第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费。

没有发生粘包、拆包示意图

第二种情况:

服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于之前逻辑实现的服务端就蒙了,因为服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种情况其实是发生了TCP粘包。

TCP粘包示意图

第三种情况:

服务端一共收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了TCP拆,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。

TCP拆包示意图

为什么会发生TCP粘包、拆包呢?

发生TCP粘包、拆包主要是由于下面一些原因:

  1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。

  2. 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。

  3. 进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。

  4. 接收方法不及时读取套接字缓冲区数据,这将发生粘包。

  5. ……

如何处理粘包、拆包问题?

知道了粘包、拆包问题及根源,那么如何处理粘包、拆包问题呢?TCP本身是面向流的,作为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:

  1. 使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。

  2. 设置定长消息,服务端每次读取既定长度的内容作为一条完整消息。

  3. 设置消息边界,服务端从网络流中按消息编辑分离出消息内容。

  4. ……

如何基于DotNetty处理粘包、拆包问题?

ChannelPipeline 网络层数据的流向

ChannelHandler 组件对网络数据的处理

  1. ByteToMessageDecoder

  2. MessageToMessageDecoder

这两个组件都实现了ChannelInboundHandler接口,这说明这两个组件都是用来解码网络上过来的数据的。而他们的顺序一般是ByteToMessageDecoder位于head channel handler的后面,MessageToMessageDecoder位于ByteToMessageDecoder的后面。DotNetty中,涉及到粘包、拆包的逻辑主要在ByteToMessageDecoder及其实现中。

ByteToMessageDecoder

顾名思义、ByteToMessageDecoder是用来将从网络缓冲区读取的字节转换成有意义的消息对象的

当上面一个channel handler传入的ByteBuf有数据的时候,这里我们可以把in参数看成网络流,这里有不断的数据流入,而我们要做的就是从这个byte流中分离出message,然后把message添加给out。分开将一下代码逻辑:

  1. 当out中有Message的时候,直接将out中的内容交给后面的channel handler去处理。

  2. 当用户逻辑把当前channel handler移除的时候,立即停止对网络数据的处理。

  3. 记录当前in中可读字节数。

  4. decode是抽象方法,交给子类具体实现。

  5. 同样判断当前channel handler移除的时候,立即停止对网络数据的处理。

  6. 如果子类实现没有分理出任何message的时候,且子类实现也没有动bytebuf中的数据的时候,这里直接跳出,等待后续有数据来了再进行处理。

  7. 如果子类实现没有分理出任何message的时候,且子类实现动了bytebuf中的数据,则继续循环,直到解析出message或者不在对bytebuf中数据进行处理为止。

  8. 如果子类实现解析出了message但是又没有动bytebuf中的数据,那么是有问题的,抛出异常。

  9. 如果标志位只解码一次,则退出。

可以知道,如果要实现具有处理粘包、拆包功能的子类,及decode实现,必须要遵守上面的规则,我们以实现处理第一部分的第二种粘包情况和第三种情况拆包情况的服务器逻辑来举例:

对于粘包情况的decode需要实现的逻辑对应于将客户端发送的两条消息都解析出来分为两个message加入out,这样的话callDecode只需要调用一次decode即可。

对于拆包情况的decode需要实现的逻辑主要对应于处理第一个数据包的时候第一次调用decode的时候out的size不变,从continue跳出并且由于不满足继续可读而退出循环,处理第二个数据包的时候,对于decode的调用将会产生两个message放入out,其中两次进入callDecode上下文中的数据流将会合并为一个bytebuf和当前channel handler实例关联,两次处理完毕即清空这个bytebuf。

MqttDecoder :  Mqtt 解码器

    public sealed class MqttDecoder : ByteToMessageDecoder
{
readonly bool _isServer;
readonly int _maxMessageSize; public MqttDecoder(bool isServer, int maxMessageSize)
{
_isServer = isServer;
_maxMessageSize = maxMessageSize;
} protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
try
{
if (!TryDecodePacket(context, input, out Packet packet))
return; output.Add(packet);
}
catch (DecoderException)
{
input.SkipBytes(input.ReadableBytes);
throw;
}
} bool TryDecodePacket(IChannelHandlerContext context, IByteBuffer buffer, out Packet packet)
{
if (!buffer.IsReadable())
{
packet = null;
return false;
} byte signature = buffer.ReadByte(); if (!TryDecodeRemainingLength(buffer, out int remainingLength) || !buffer.IsReadable(remainingLength))
{
packet = null;
return false;
} //DecodePacketInternal
var fixedHeader = new FixedHeader(signature, remainingLength);
switch (fixedHeader.PacketType)
{
case PacketType.CONNECT: packet = new ConnectPacket(); break;
case PacketType.CONNACK: packet = new ConnAckPacket(); break;
case PacketType.DISCONNECT: packet = new DisconnectPacket(); break;
case PacketType.PINGREQ: packet = new PingReqPacket(); break;
case PacketType.PINGRESP: packet = new PingRespPacket(); break;
case PacketType.PUBACK: packet = new PubAckPacket(); break;
case PacketType.PUBCOMP: packet = new PubCompPacket(); break;
case PacketType.PUBLISH: packet = new PublishPacket(); break;
case PacketType.PUBREC: packet = new PubRecPacket(); break;
case PacketType.PUBREL: packet = new PubRelPacket(); break;
case PacketType.SUBSCRIBE: packet = new SubscribePacket(); break;
case PacketType.SUBACK: packet = new SubAckPacket(); break;
case PacketType.UNSUBSCRIBE: packet = new UnsubscribePacket(); break;
case PacketType.UNSUBACK: packet = new UnsubscribePacket(); break;
default:
throw new DecoderException("Unsupported Message Type");
}
packet.FixedHeader = fixedHeader;
packet.Decode(buffer); //if (remainingLength > 0)
// throw new DecoderException($"Declared remaining length is bigger than packet data size by {remainingLength}."); return true;
} bool TryDecodeRemainingLength(IByteBuffer buffer, out int value)
{
int readable = buffer.ReadableBytes; int result = ;
int multiplier = ;
byte digit;
int read = ;
do
{
if (readable < read + )
{
value = default;
return false;
}
digit = buffer.ReadByte();
result += (digit & 0x7f) * multiplier;
multiplier <<= ;
read++;
}
while ((digit & 0x80) != && read < ); if (read == && (digit & 0x80) != )
throw new DecoderException("Remaining length exceeds 4 bytes in length"); int completeMessageSize = result + + read;
if (completeMessageSize > _maxMessageSize)
throw new DecoderException("Message is too big: " + completeMessageSize); value = result;
return true;
} //static int DecodeRemainingLength(IByteBuffer buffer)
//{
// byte encodedByte;
// var multiplier = 1;
// var remainingLength = 0;
// do
// {
// encodedByte = buffer.ReadByte();
// remainingLength += (encodedByte & 0x7f) * multiplier;
// multiplier *= 0x80;
// } while ((encodedByte & 0x80) != 0); // return remainingLength;
//}
}

MqttEncoder:  mqtt 编码器

    public sealed class MqttEncoder : MessageToMessageEncoder<Packet>
{
public static readonly MqttEncoder Instance = new MqttEncoder(); protected override void Encode(IChannelHandlerContext context, Packet message, List<object> output) => DoEncode(context.Allocator, message, output); public static void DoEncode(IByteBufferAllocator bufferAllocator, Packet packet, List<object> output)
{
IByteBuffer buffer = bufferAllocator.Buffer();
try
{
packet.Encode(buffer);
output.Add(buffer);
buffer = null;
}
finally
{
buffer?.SafeRelease();
}
}
}

未完待续。。。

DotNetty 版 mqtt 开源客户端 (MqttFx)的更多相关文章

  1. 20款PHP版WebMail开源项目

    20款PHP版WebMail开源项目 如今互联网巨头提供的企业应用套件中邮件托管是必备服务,而且还始终秉承免费的优良光荣传统,最为让人熟识的恐怕非"瘟多死里屋管理中心"和" ...

  2. 【分布式】Zookeeper使用--开源客户端

    一.前言 上一篇博客已经介绍了如何使用Zookeeper提供的原生态Java API进行操作,本篇博文主要讲解如何通过开源客户端来进行操作. 二.ZkClient ZkClient是在Zookeepe ...

  3. Android版的菜谱客户端应用源码完整版

    Android版的菜谱客户端应用源码完整版,这个文章是从安卓教程网转载过来的,不是本人的原创,希望能够帮到大家的学习吧. <ignore_js_op> 152936qc7jdnv6vo0c ...

  4. Asynchronous MQTT client library for C (MQTT异步客户端C语言库-paho)

    原文:http://www.eclipse.org/paho/files/mqttdoc/MQTTAsync/html/index.html MQTT异步客户端C语言库   用于C的异步 MQTT 客 ...

  5. 凡信(超仿微信Android版)开源了,内有源码下载 -

    韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha 凡信(超仿微信Android版)开源了,内有源码下载 - IM Geek开发者社区-移动 ...

  6. c#版 mqtt 3.1.1 client 实现

    c# 版 mqtt 3.1.1 client http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html 上面为 3.1.1 协议报文 一 ...

  7. redis 开源客户端下载

    redis 开源客户端下载地址: https://github.com/qishibo/AnotherRedisDesktopManager/releases

  8. java版ftp简易客户端(可以获取文件的名称及文件大小)

    java版ftp简易客户端(可以获取文件的名称及文件大小) package com.ccb.ftp; import java.io.IOException; import java.net.Socke ...

  9. zookeeper系列(三)zookeeper的使用--开源客户端

    作者:leesf    掌控之中,才会成功:掌控之外,注定失败, 原创博客地址:http://www.cnblogs.com/leesf456/ 奇文共欣赏,大家共同学习进步. 一.前言 上一篇博客已 ...

随机推荐

  1. Maven实现直接部署Web项目到Tomcat7

    如题目,自动部署到Web服务器,直接上过程. 1.Tomcat7的用户及权限配置:在conf目录下,找到tomcat-users.xml,添加manager权限的用户. <role rolena ...

  2. Linux 优秀软件

    本文由Suzzz原创,发布于 http://www.cnblogs.com/Suzzz/p/4038925.html ,转载请保留此声明 一些Linux下的优秀软件,个人非常喜欢.都在Ubuntu14 ...

  3. Excel中函数row和column的特殊应用

    版本:2016,数据来源:我要自学网-曾贤志老师 row在英文中是行,排的意思,在Excel中的作用是返回所引用的行号.​   column在英文中是列,总队的意思,其作用是返回所引用的列号.   假 ...

  4. nginx提示Error: Too many open files的解决办法

    nginx提示:Too many open files这种错误问题的原因是因为linux文件系统最大可打开文件数为1024,而你的nginx中的error.log出现大量的Too many open ...

  5. EMIPLIB简介

    EMIPLIB(http://research.edm.uhasselt.be/emiplib)的全称是'EDM Media over IP libray' .EDM是Hasselt Universi ...

  6. html5 日常小结

    HTML5新标签汇总 1.  html5新的 (input type=类型) 元素 <input type="number" name="quantity" ...

  7. HDFS之四:HDFS原理解析(总体架构,读写操作流程)

    前言 HDFS 是一个能够面向大规模数据使用的,可进行扩展的文件存储与传递系统.是一种允许文件通过网络在多台主机上分享的文件系统,可让多机器上的多用户分享文件和 存储空间.让实际上是通过网络来访问文件 ...

  8. 创建,查看,删除pool,查看,修改pool参数命令总结

    标签(空格分隔): ceph,ceph运维,pool 1. 创建pool命令: ceph的pool有两种类型,一种是副本池,一种是ec池,创建时也有所区别 1.1 创建副本池: $ sudo ceph ...

  9. 想开发VR游戏?你需要注意这些东西

    转自:http://www.gamelook.com.cn/2016/03/246620 开发VR游戏很难吗?有些人会说是,但在HTC虚拟现实新科技部门副总经理鲍永哲看来,VR游戏的门槛并不比一般的游 ...

  10. stdin和STDIN_FILENO的区别

    STDIN_FILENO与stdin的区别: STDIN_FILENO: 1).数据类型:int 2).层次:系统级的API,是一个文件句柄,定义在<unistd.h>中. 3).相应的函 ...