Mqttnet内存与性能改进录

1 MQTTnet介绍

MQTTnet是一个高性能的 .NET MQTT库,它提供MQTT客户端和MQTT服务器的功能,支持到最新MQTT5协议版本,支持.Net Framework4.5.2版本或以上。

MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker) and supports the MQTT protocol up to version 5. It is compatible with mostly any supported .NET Framework version and CPU architecture.

2 我与MQTTnet

我有一些小型项目,需要安装在局域网环境下的windows或linux系统,这个安装过程需要小白也能安装,而且每天都有可能有多份新的安装部署的新环境,所以流行的mqtt服务器emqx可能变得不太适合我的选型,因为让小白来大量部署它不是非常方便。

我的这个小项目主体是一个Web项目,浏览器用户对象是管理员,数据的产生者是N多个廉价linux小型设备,设备使用mqtt协议高频提交数据到后台,后台也需要使用mqtt协议来主动控制设备完成一些操作动作。除此之后,Web浏览器也需要使用mqtt over websocket来订阅一些主题,达到监控某台设备的实时数据目的。

经过比较,MQTTnet变成了我意向使用的mqtt库,尤其是MQTTnet.AspNetCore子项目,基于kestrel来使用tcp或websocket做传输层,增加mqtt应用层协议的解析,最后让mqtt与asp.netcore完美地融合在一起。

3 Bug发现

项目有后台主动发送mqtt到设备以控制设备的需求,在mqttnet里有个对应的InjectApplicationMessage()扩展方法可以从server主动发送mqtt到client,但这个方法总是抛出ArgumentNullException。但如果使用InjectApplicationMessage (InjectedMqttApplicationMessage)这个基础方法来注入mqtt消息不有异常。

经过一段时间后,闲时的我决定迁出mqttnet项目的源代码来调试分析。最后发现是因为这个扩展方法没有传递SenderClientId导致的异常,所以我决定尝试修改并推送一个请求到mqttnet项目。

5e80f1692689d0300c4c53b505c9d135.png

4 改进之路

经过尝试修改一个小小bug之后,我开始认真的阅读MQTTnet.AspNetCore的源代码,陆续发现一些可以减少内存复制和内存分配的优化点:

  1. ReadOnlyMemory<byte>转为ReceivedMqttPacket过程优化;

  2. MqttPacketBuffer发送过程的优化;

  3. Array.Copy()的改进;

  4. Byte[] -> ArraySegment<byte>的优化;

4.1 避免不必要的ReadOnlyMemory<byte>转为byte[]

原始代码

var bodySlice = copy.Slice(0, bodyLength);
var buffer = bodySlice.GetMemory().ToArray();
var receivedMqttPacket = new ReceivedMqttPacket(fixedHeader, new ArraySegment<byte>(buffer, 0, buffer.Length), buffer.Length + 2);static ReadOnlyMemory<byte> GetMemory(this in ReadOnlySequence<byte> input)
{if (input.IsSingleSegment){return input.First;}// Should be rarereturn input.ToArray();
}

原始代码设计了一个GetMemory()方法,目的是在两个地方调用到。但它的一句var buffer = bodySlice.GetMemory().ToArray(),就会无条件的产生一次内存分配和一次内存拷贝。

改进代码

var bodySlice = copy.Slice(0, bodyLength);
var bodySegment = GetArraySegment(ref bodySlice); 
var receivedMqttPacket = new ReceivedMqttPacket(fixedHeader, bodySegment, headerLength + bodyLength);static ArraySegment<byte> GetArraySegment(ref ReadOnlySequence<byte> input)
{if (input.IsSingleSegment && MemoryMarshal.TryGetArray(input.First, out var segment)){return segment;}// Should be rarevar array = input.ToArray();return new ArraySegment<byte>(array);
}

因为有其它地方的优化,GetMemory()不再需要复用,所以我们直接改为GetArraySegment(),里面使用MemoryMarshal.TryGetArray()方法尝试从ReadOnlyMemory<byte>获取ArraySegment<byte>对象。而mqttnet的ReceivedMqttPacket对象是支持ArraySegment<byte>类型参数的。

054bac89b702345ed6cd81c559a7e0c0.png

在我提交请求之后,@gfoidl给了很多其它特别好的性能方面的建议,有兴趣的同学可以点此查看。

戏剧性的是,在我尝试改进这个问题的时候,我发现了mqttnet的另外一个BUG:当bodySegment的Offset不是0开始的时候,mqttnet会产生异常。这足以说明,mqttnet项目从未使用Offset大于0的ArraySegment<byte>,所以这个bug才一直没有发现。本为不是MQTTnet.AspNetCore子项目的代码我就不改的原则,我向mqttnet提了问题:https://github.com/dotnet/MQTTnet/issues/1592 作者也很认真看待这个问题,于是自己加班解决:https://github.com/dotnet/MQTTnet/pull/1593

更戏剧性的是,我开心地合并main代码过来验证之后,发现作者改的BUG里又带入了BUG!现在Offset大于0还是有问题。于是我心急啊,我决定为这个BUG中BUG提交一个修改的请求:https://github.com/dotnet/MQTTnet/pull/1598

6ab02cdca7729ab714d1c6d24d58aae3.png

最后,这个MemoryMarshal.TryGetArray()的优化终于提到合并,改进后CPU时间时间也减少了,内存分配更是减少了50%。

4.2 MqttPacketBuffer发送过程的优化

MqttPacketBuffer有两个数据段:Pacaket段和Payload段,我看到它原始发送代码如下:

var buffer = formatter.Encode(packet);
var msg = buffer.Join().AsMemory();
var output = _output;
var result = await output.WriteAsync(msg, cancellationToken).ConfigureAwait(false);

我也没有经过认证思考,觉得这里可以将Pacaket段和Payload直接两次发送即可。

var buffer = PacketFormatterAdapter.Encode(packet);
await _output.WriteAsync(buffer.Packet, cancellationToken).ConfigureAwait(false);if (buffer.Payload.Count > 0)
{ await _output.WriteAsync(buffer.Payload, cancellationToken).ConfigureAwait(false);
}

后来作者说,当mqtt over websocket时,有些客户端在实现上没能兼容一个mqtt包分多个websocket帧传输的处理,所以需要合并发送。那我就想,如果我检测传输层是websocket的话再Join合并就行了,于是改为如下:

if (_isOverWebSocket == false)
{await _output.WriteAsync(buffer.Packet, cancellationToken).ConfigureAwait(false);if (buffer.Payload.Count > 0){await _output.WriteAsync(buffer.Payload, cancellationToken).ConfigureAwait(false);}
}
else
{     var bufferSegment = buffer.Join();await _output.WriteAsync(bufferSegment, cancellationToken).ConfigureAwait(false);
}

虽然觉得这个方案比之前要好了一些,但感觉Jion里的 new byte[]的分配让我耿耿于怀。再经过几将进改,最后的代码如下,虽然也有拷贝,但至少已经没有分配:

if (buffer.Payload.Count == 0)
{// zero copy// https://github.com/dotnet/runtime/blob/main/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/StreamPipeWriter.cs#L279await _output.WriteAsync(buffer.Packet, cancellationToken).ConfigureAwait(false);
}
else
{WritePacketBuffer(_output, buffer);await _output.FlushAsync(cancellationToken).ConfigureAwait(false);
}static void WritePacketBuffer(PipeWriter output, MqttPacketBuffer buffer)
{// copy MqttPacketBuffer's Packet and Payload to the same buffer block of PipeWriter// MqttPacket will be transmitted within the bounds of a WebSocket frame after PipeWriter.FlushAsyncvar span = output.GetSpan(buffer.Length);buffer.Packet.AsSpan().CopyTo(span);buffer.Payload.AsSpan().CopyTo(span.Slice(buffer.Packet.Count));output.Advance(buffer.Length);
}

4.3 Array.Copy()的改进

mqttnet由于要兼容很多.net框架和版本,所以往往能使用的api不多,比如在内存拷贝了,还保留了最初的Array.Copy(),我们可以较新的框架下使用更好的api来复制,最高可达25%的复制性能提升,这个改进的工作量非常小,但产出是相当的可喜啊。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Copy(byte[] source, int sourceIndex, byte[] destination, int destinationIndex, int length)
{
#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1source.AsSpan(sourceIndex, length).CopyTo(destination.AsSpan(destinationIndex, length));
#elif NET461_OR_GREATER || NETSTANDARD1_3_OR_GREATERunsafe{fixed (byte* pSoure = &source[sourceIndex]){fixed (byte* pDestination = &destination[destinationIndex]){System.Buffer.MemoryCopy(pSoure, pDestination, length, length);}}}
#elseArray.Copy(source, sourceIndex, destination, destinationIndex, length);
#endif
}

4.4 Byte[] -> ArraySegment<byte>的优化

当前的mqttnet,由于历史设计的局限原因,现在还不能创建ArraySegment<byte>Memory<byte>作为payload的mqtt消息包。如果我们从ArrayPool申请1000字节的buffer,实际我们会得到一个到1024字节的buffer,想拿租赁的buffer的前1000字节做mqtt消息的payload,我们现在不得不再创建一个1000字节的byte[1000] newpayload,然后拷贝buffer到newpayload。

这种局限对服务端来说弊端是很大的,我现在尝试如何不破坏原始的byte[]支持的设计提前下,让mqttnet也支持ArraySegment<byte>的数据发送。当然,保持兼容性的新Api加入对项目来说是一种大的变化,自然有一定的风险性。

如果你也关注这个mqttnet项目,你可以查看 https://github.com/dotnet/MQTTnet/pull/1585 这个提议,也许未来它会变成现实。

5 最后

开源项目让大众受益,尤其是核心作者真的不容易,为其呕心沥血。我们在受益的同时,如果有能力的话可以反抚开源项目,在参与过程中,自身也会学到一些知识的,就当作被学习的过程吧。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/280060.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DataArtisans战略联手阿里云 Apache Flink服务能力云化

近日&#xff0c;Apache Flink商业公司 CEO、联合创始人Kostas Tzoumas在云栖大会上宣布和阿里集团达成战略合作伙伴关系&#xff0c;希望能够借助全球最大的云计算公司之一阿里云&#xff0c;服务更多的大数据实时流计算的客户。同时期待通过加强和阿里集团技术合作&#xff0…

高清、免版权美图资源大全

正所谓“一图胜千言”&#xff0c;当在写文章、做设计、搞 PPT、发朋友圈&#xff0c;搭配一些合适的图&#xff0c;这无疑将极大提升内容的表现力。鉴于此&#xff0c;在倾城之链的美图板块&#xff0c;收录了来自世界各地的优质图片网站&#xff0c;它们所提供高品质且免费的…

如何在WhatsApp中将群聊静音

Group Chats are awesome if you’re in a club, want to keep in touch with all your friends, or are trying organize something. Unfortunately, if you’re busy and the other members decide to have a long, detailed conversation about the latest episode of Game …

Django进阶之session

Django进阶之session 基于cookie做用户验证时&#xff1a;敏感信息不适合放在cookie中 session依赖cookie session原理 cookie是保存在用户浏览器端的键值对 session是保存在服务器端的键值对 session服务端中存在的数据为&#xff1a; session {随机字符串1&#xff1a;{用户…

Facebook开源 PyTorch版 fairseq,准确性最高、速度比循环神经网络快9倍

今年5月&#xff0c;Facebook AI研究院&#xff08;FAIR&#xff09;发表了他们的研究成果fairseq&#xff0c;在fairseq中&#xff0c;他们使用了一种新型的卷积神经网络来做语言翻译&#xff0c;比循环神经网络的速度快了9倍&#xff0c;而且准确性也是现有模型中最高的。此外…

推荐一个开源的现代化的 PDF 生成组件

你好&#xff0c;这里是 Dotnet 工具箱&#xff0c;定期分享 Dotnet 有趣&#xff0c;实用的工具和组件&#xff0c;希望对您有用&#xff01;前言QuestPDF 是一个开源免费的 .NET 组件库&#xff0c;可以用来生成 PDF 文档。在 Github 上有4千多的 Star。项目充分考虑了 PDF 文…

小程序调用阿里云身份证识别OCR(附带七牛云上传图片)

写在前面&#xff1a;实现的逻辑是拍照上传调用后端封装好的身份证接口&#xff0c;然后同时调用七牛云接口把照片传过去以便后台管理系统审核看1:首选需要这么一张页面接下来就写我是怎么做的首先是布局&#xff08;以下是wxml&#xff09; <view><view classidcard&…

windows 安装yaml支持和pytest支持等

打开cmd 输入pip install pyyaml #yaml文件支持 输入pip install pytest #pytest框架支持 输入pip install requests #requests接口测试支持 输入pip install pyopenssl #openssl支持 前提是电脑上的python已经配置好了转载于:https://www.cnblogs.com/mghhzAnne/p/92…

史上最好记的神经网络结构速记表(上)

本文讲的是史上最好记的神经网络结构速记表&#xff08;上&#xff09;&#xff0c;新的神经网络结构不断涌现&#xff0c;我们很难一一掌握。哪怕一开始只是记住所有的简称&#xff08; DCIGN&#xff0c;BiLSTM&#xff0c;DCGAN &#xff09;&#xff0c;也会让同学们吃不消…

厚积薄发,微软OFFICE云时代宏脚本来临,Excel Srcipt已经推进到桌面端可用

前一阵子&#xff0c;已经发现微软在Excel上发布了Office Script For Excel&#xff0c;当时只能在网页端的Excel上使用&#xff0c;今天打开桌面端的Excel&#xff0c;发现多了一个【自动执行】选项卡。再一次看了下&#xff0c;比起以前的Office Addin&#xff0c;要先进得多…

如何使用Amazon Echo控制您的Eero Wi-Fi网络

Thanks to the power of Alexa and its open API, you’re able to control a vast number of devices using just your voice. If you have an Eero Wi-Fi system, you can even control your home network with the Amazon Echo. 得益于Alexa的强大功能及其开放的API&#xf…

H5在WebView上开发小结

背景 来自我司业务方要求&#xff0c;需开发一款APP。但由于时间限制&#xff0c;只能采取套壳app方式&#xff0c;即原生app内嵌webview展示前端页面。本文主要记述JavaScript与原生app间通信&#xff0c;以及内嵌webview开发时&#xff0c;前端方面可能踩的一些坑。 技术架构…

C#的?和??

1.&#xff1f;&#xff1f; 为了实现Nullable数据类型转换成non-Nullable类型数据&#xff0c;才有的一个操作符&#xff1b; 意义&#xff1a;一变量取值&#xff0c;取符号左边的值&#xff0c;若左边为null&#xff0c;那么取赋值&#xff1f;&#xff1f;右边的&#xff1…

odoo 自定义视图_如何使用Windows的五个模板自定义文件夹视图

odoo 自定义视图If you’re particular about how Windows displays the contents of your folders, you can cut your customization time down considerably by taking advantage of File Explorer’s five built-in folder templates. 如果您特别想知道Windows如何显示文件夹…

C#之ILC和C++的CLR前者更快?

楔子ILC是C#写的&#xff0c;CLR是C。.Net 7中&#xff0c;为何微软执意用一个托管的模型去尝试取代非托管框架呢&#xff1f;至少native code方面它是这么做的这个问题一直萦绕脑海。非托管和托管十年前出版的那本久负盛名的《CLR via C#》至今都是不可或缺的存在&#xff0c;…

历史

python的历史 kfsaldkfsdf fdskfdsa fdsjkafsjda fdshkfjsdja View Codefjdskaffdsjkaffdsjakflsad;fjdsklaf 转载于:https://www.cnblogs.com/jin-xin/articles/10448286.html

typescript+react+antd基础环境搭建

typescriptreactantd基础环境搭建&#xff08;包含样式定制&#xff09; tsconfig.json 配置 // 具体配置可以看上面的链接 这里module moduleResolution的配置都会影响到antd的显示 // allowSyntheticDefaultImports 是antd官网给的配置 必须加上 {"compilerOptions&quo…

最小生成树Prim算法和Kruskal算法

https://www.cnblogs.com/JoshuaMK/p/prim_kruskal.html 转载于:https://www.cnblogs.com/DixinFan/p/9225105.html

如何重新打开Windows防火墙提示?

If you are setting up a new program that needs network access, but are not paying close enough attention, you might end up accidentally causing Windows firewall to block the program. How do you fix such a mistake? Today’s SuperUser Q&A post helps a f…

判断字符串出现次数最多的字符 及 次数

分析 题目的意思大致就是找出每个字符出现的次数&#xff0c;然后比较大小。那么每个字符都应该对应它出现的次数。既然是一一对应的&#xff0c;那我们就想到用对象的key和value来储存字符和其出现的次数。具体做法 新建一个空对象obj 遍历给定的字符串接下来就是最重要的 把字…