基于 DotNetty 实现通信

DotNetty : 是微软的 Azure 团队,使用 C#实现的 Netty 的版本发布。是.NET 平台的优秀网络库。

项目介绍

OpenDeploy.Communication 类库项目,是通信相关基础设施层

  • Codec 模块实现编码解码
  • Convention 模块定义约定,比如抽象的业务 Handler, 消息载体 NettyMessage, 消息上下文 'NettyContext' 等

自定义消息格式

消息类为 NettyMessage ,封装了消息头 NettyHeader 和消息体 Body

NettyMessage

封装了消息头 NettyHeader 和消息体 Body

NettyMessage 点击查看代码
/// <summary> Netty消息 </summary>
public class NettyMessage
{
/// <summary> 消息头 </summary>
public NettyHeader Header { get; init; } = default!; /// <summary> 消息体(可空,可根据具体业务而定) </summary>
public byte[]? Body { get; init; } /// <summary> 消息头转为字节数组 </summary>
public byte[] GetHeaderBytes()
{
var headerString = Header.ToString();
return Encoding.UTF8.GetBytes(headerString);
} /// <summary> 是否同步消息 </summary>
public bool IsSync() => Header.Sync; /// <summary> 创建Netty消息工厂方法 </summary>
public static NettyMessage Create(string endpoint, bool sync = false, byte[]? body = null)
{
return new NettyMessage
{
Header = new NettyHeader { EndPoint = endpoint, Sync = sync },
Body = body
};
} /// <summary> 序列化为JSON字符串 </summary>
public override string ToString() => Header.ToString();
}

NettyHeader

消息头,包含请求唯一标识,是否同步消息,终结点等, 在传输数据时会序列化为 JSON

NettyHeader 点击查看代码
/// <summary> Netty消息头 </summary>
public class NettyHeader
{
/// <summary> 请求消息唯一标识 </summary>
public Guid RequestId { get; init; } = Guid.NewGuid(); /// <summary> 是否同步消息, 默认false是异步消息 </summary>
public bool Sync { get; init; } /// <summary> 终结点 (借鉴MVC,约定为Control/Action模式) </summary>
public string EndPoint { get; init; } = string.Empty; /// <summary> 序列化为JSON字符串 </summary>
public override string ToString() => this.ToJsonString();
}

  • 请求消息唯一标识 RequestId , 用来唯一标识消息, 主要用于 发送同步请求, 因为默认的消息是异步的,只管发出去,不需要等待响应
  • 是否同步消息 Sync , 可以不需要,主要为了可视化,便于调试
  • 终结点 EndPoint , (借鉴 MVC,约定为 Control/Action 模式), 服务端直接解析出对应的处理器

编码器

DefaultEncoder 点击查看代码
public class DefaultEncoder : MessageToByteEncoder<NettyMessage>
{
protected override void Encode(IChannelHandlerContext context, NettyMessage message, IByteBuffer output)
{
//消息头转为字节数组
var headerBytes = message.GetHeaderBytes(); //写入消息头长度
output.WriteInt(headerBytes.Length); //写入消息头字节数组
output.WriteBytes(headerBytes); //写入消息体字节数组
if (message.Body != null && message.Body.Length > 0)
{
output.WriteBytes(message.Body);
}
}
}

解码器

DefaultDecoder 点击查看代码
public class DefaultDecoder : MessageToMessageDecoder<IByteBuffer>
{
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
//消息总长度
var totalLength = input.ReadableBytes; //消息头长度
var headerLength = input.GetInt(input.ReaderIndex); //消息体长度
var bodyLength = totalLength - 4 - headerLength; //读取消息头字节数组
var headerBytes = new byte[headerLength];
input.GetBytes(input.ReaderIndex + 4, headerBytes, 0, headerLength); byte[]? bodyBytes = null;
string? rawHeaderString = null;
NettyHeader? header; try
{
//把消息头字节数组,反序列化为JSON
rawHeaderString = Encoding.UTF8.GetString(headerBytes);
header = JsonSerializer.Deserialize<NettyHeader>(rawHeaderString);
}
catch (Exception ex)
{
Logger.Error($"解码失败: {rawHeaderString}, {ex}");
return;
} if (header is null)
{
Logger.Error($"解码失败: {rawHeaderString}");
return;
} //读取消息体字节数组
if (bodyLength > 0)
{
bodyBytes = new byte[bodyLength];
input.GetBytes(input.ReaderIndex + 4 + headerLength, bodyBytes, 0, bodyLength);
} //封装为NettyMessage对象
var message = new NettyMessage
{
Header = header,
Body = bodyBytes,
}; output.Add(message);
}
}

NettyServer 实现

NettyServer 点击查看代码
public static class NettyServer
{
/// <summary>
/// 开启Netty服务
/// </summary>
public static async Task RunAsync(int port = 20007)
{
var bossGroup = new MultithreadEventLoopGroup(1);
var workerGroup = new MultithreadEventLoopGroup(); try
{
var bootstrap = new ServerBootstrap().Group(bossGroup, workerGroup); bootstrap
.Channel<TcpServerSocketChannel>()
.Option(ChannelOption.SoBacklog, 100)
.Option(ChannelOption.SoReuseaddr, true)
.Option(ChannelOption.SoReuseport, true)
.ChildHandler(new ActionChannelInitializer<IChannel>(channel =>
{
IChannelPipeline pipeline = channel.Pipeline;
pipeline.AddLast("framing-enc", new LengthFieldPrepender(4));
pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(int.MaxValue, 0, 4, 0, 4));
pipeline.AddLast("decoder", new DefaultDecoder());
pipeline.AddLast("encoder", new DefaultEncoder());
pipeline.AddLast("handler", new ServerMessageEntry());
})); var boundChannel = await bootstrap.BindAsync(port); Logger.Info($"NettyServer启动成功...{boundChannel}"); Console.ReadLine(); await boundChannel.CloseAsync(); Logger.Info($"NettyServer关闭监听了...{boundChannel}");
}
finally
{
await Task.WhenAll(
bossGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)),
workerGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1))
); Logger.Info($"NettyServer退出了...");
} }
}
  • 服务端管道最后我们添加了 ServerMessageEntry ,作为消息处理的入口
ServerMessageEntry 点击查看代码
public class ServerMessageEntry : ChannelHandlerAdapter
{
/// <summary> Netty处理器选择器 </summary>
private readonly DefaultNettyHandlerSelector handlerSelector = new(); public ServerMessageEntry()
{
//注册Netty处理器
handlerSelector.RegisterHandlerTypes(typeof(EchoHandler).Assembly.GetTypes());
} /// <summary> 通道激活 </summary>
public override void ChannelActive(IChannelHandlerContext context)
{
Logger.Warn($"ChannelActive: {context.Channel}");
} /// <summary> 通道关闭 </summary>
public override void ChannelInactive(IChannelHandlerContext context)
{
Logger.Warn($"ChannelInactive: {context.Channel}");
} /// <summary> 收到客户端的消息 </summary>
public override async void ChannelRead(IChannelHandlerContext context, object message)
{
if (message is not NettyMessage nettyMessage)
{
Logger.Error("从客户端接收消息为空");
return;
} try
{
Logger.Info($"收到客户端的消息: {nettyMessage}"); //封装请求
var nettyContext = new NettyContext(context.Channel, nettyMessage); //选择处理器
AbstractNettyHandler handler = handlerSelector.SelectHandler(nettyContext); //处理请求
await handler.ProcessAsync();
}
catch(Exception ex)
{
Logger.Error($"ServerMessageEntry.ChannelRead: {ex}");
}
}
}
  • 按照约定, 把继承 AbstractNettyHandler 的类视为业务处理器

  • ServerMessageEntry 拿到消息后,首先把消息封装为 NettyContext, 类似与 MVC 中的 HttpContext, 封装了请求和响应对象, 内部解析请求的 EndPoint, 拆分为 HandlerName, ActionName

  • DefaultNettyHandlerSelector 提供注册处理器的方法 RegisterHandlerTypes, 和选择处理器的方法 SelectHandler

  • SelectHandler, 默认规则是查找已注册的处理器中以 HandlerName 开头的类型

  • AbstractNettyHandlerProcessAsync 方法,通过 ActionName, 反射拿到 MethodInfo, 调用终结点

NettyClient 实现

NettyClient 点击查看代码
public sealed class NettyClient(string serverHost, int serverPort) : IDisposable
{
public EndPoint ServerEndPoint { get; } = new IPEndPoint(IPAddress.Parse(serverHost), serverPort); private static readonly Bootstrap bootstrap = new();
private static readonly IEventLoopGroup eventLoopGroup = new SingleThreadEventLoop(); private bool _disposed;
private IChannel? _channel;
public bool IsConnected => _channel != null && _channel.Open;
public bool IsWritable => _channel != null && _channel.IsWritable; static NettyClient()
{
bootstrap
.Group(eventLoopGroup)
.Channel<TcpSocketChannel>()
.Option(ChannelOption.SoReuseaddr, true)
.Option(ChannelOption.SoReuseport, true)
.Handler(new ActionChannelInitializer<ISocketChannel>(channel =>
{
IChannelPipeline pipeline = channel.Pipeline;
//pipeline.AddLast("ping", new IdleStateHandler(0, 5, 0));
pipeline.AddLast("framing-enc", new LengthFieldPrepender(4));
pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(int.MaxValue, 0, 4, 0, 4));
pipeline.AddLast("decoder", new DefaultDecoder());
pipeline.AddLast("encoder", new DefaultEncoder());
pipeline.AddLast("handler", new ClientMessageEntry());
}));
} /// <summary> 连接服务器 </summary>
private async Task TryConnectAsync()
{
try
{
if (IsConnected) { return; }
_channel = await bootstrap.ConnectAsync(ServerEndPoint);
}
catch (Exception ex)
{
throw new Exception($"连接服务器失败 : {ServerEndPoint} {ex.Message}");
}
} /// <summary>
/// 发送消息
/// </summary>
/// <param name="endpoint">终结点</param>
/// <param name="sync">是否同步等待响应</param>
/// <param name="body">正文</param>
public async Task SendAsync(string endpoint, bool sync = false, byte[]? body = null)
{
var message = NettyMessage.Create(endpoint, sync, body);
if (sync)
{
var task = ClientMessageSynchronizer.TryAdd(message);
try
{
await SendAsync(message);
await task;
}
catch
{
ClientMessageSynchronizer.TryRemove(message);
throw;
}
}
else
{
await SendAsync(message);
}
} /// <summary>
/// 发送消息
/// </summary>
private async Task SendAsync(NettyMessage message)
{
await TryConnectAsync();
await _channel!.WriteAndFlushAsync(message);
} /// <summary> 释放连接(程序员手动释放, 一般在代码使用using语句,或在finally里面Dispose) </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
} /// <summary> 释放连接 </summary>
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
} //释放托管资源,比如嵌套的对象
if (disposing)
{ } //释放非托管资源
if (_channel != null)
{
_channel.CloseAsync();
_channel = null;
} _disposed = true;
} ~NettyClient()
{
Dispose(true);
}
}
  • NettyClient 封装了 Netty 客户端逻辑,提供发送异步请求(默认)和发布同步请求方法
  • DotNetty 默认不提供同步请求,但是有些情况我们需要同步等待服务器的响应,所有需要自行实现,实现也很简单,把消息 ID 缓存起来,收到服务器响应后激活就行了,具体实现在消息同步器 ClientMessageSynchronizer, 就不贴了

总结

至此,我们实现了基于 DotNetty 搭建通信模块, 实现了客户端和服务器的编解码,处理器选择,客户端实现了同步消息等,大家可以在 ConsoleHost 结尾的控制台项目中,测试下同步和异步的消息,实现的简单的 Echo 模式

下一步

计划下一步,基于WPF的客户端, 实现接口项目的配置与发现

基于DotNetty实现一个接口自动发布工具 - 通信实现的更多相关文章

  1. 自动发布工具版本从python2升级成python3后遇到的种种问题(涉及paramiko,Crypto,zipfile等等)

    从在公司实习到正式入职,一直还在被同事使用的是我写的一个自动发布工具.该工具的主要功能是:开发人员给出需要更新的代码包(zip格式),测试人员将该代码包部署到测服,这些代码包和JIRA数据库里的项目信 ...

  2. 基于node实现一个简单的脚手架工具(node控制台交互项目)

    实现控制台输入输出 实现文件读写操作 全原生实现一个简单的脚手架工具 实现vue-cli2源码 一.实现控制台输入输出 关于控制台的输入输出依然是基于node进程管理对象process,在proces ...

  3. 基于SOUI开发一个简单的小工具

    基于DriectUI有很多库,比如 Duilib (免费) soui (免费) DuiVision (免费) 炫彩 (界面库免费,UI设计器付费,不提供源码) skinui (免费使用,但不开放源码, ...

  4. 跨平台自动构建工具v1.0.2 发布

    XMake是一个跨平台自动构建工具,支持在各种主流平台上构建项目,类似cmake.automake.premake,但是更加的方便易用,工程描述语法更简洁直观,支持平台更多,并且集创建.配置.编译.打 ...

  5. BlogPublishTool - 博客发布工具

    BlogPublishTool - 博客发布工具 这是一个发布博客的工具.本博客使用本工具发布. 本工具源码已上传至github:https://github.com/ChildishChange/B ...

  6. 基于Dubbo的http自动测试工具分享

    公司是采用微服务来做模块化的,各个模块之间采用dubbo通信.好处就不用提了,省略了之前模块间复杂的http访问.不过也遇到一些问题: PS: Github的代码示例还在整理中... 测试需要配合写消 ...

  7. 基于数据库的代码自动生成工具,生成JavaBean、生成数据库文档、生成前后端代码等(v6.0.0版)

    TableGo v6.0.0 版震撼发布,此次版本更新如下: 1.UI界面大改版,组件大调整,提升界面功能的可扩展性. 2.新增BeautyEye主题,界面更加清新美观,也可以通过配置切换到原生Jav ...

  8. Go/Python/Erlang编程语言对比分析及示例 基于RabbitMQ.Client组件实现RabbitMQ可复用的 ConnectionPool(连接池) 封装一个基于NLog+NLog.Mongo的日志记录工具类LogUtil 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!

    Go/Python/Erlang编程语言对比分析及示例   本文主要是介绍Go,从语言对比分析的角度切入.之所以选择与Python.Erlang对比,是因为做为高级语言,它们语言特性上有较大的相似性, ...

  9. 5.7 Liquibase:与具体数据库独立的追踪、管理和应用数据库Scheme变化的工具。-mybatis-generator将数据库表反向生成对应的实体类及基于mybatis的mapper接口和xml映射文件(类似代码生成器)

    一. liquibase 使用说明 功能概述:通过xml文件规范化维护数据库表结构及初始化数据. 1.配置不同环境下的数据库信息 (1)创建不同环境的数据库. (2)在resource/liquiba ...

  10. 一个基于Bootstrap实现的HMTL可视化编辑工具

    疫情禁足在家,用原生的JS实现了一个HTML可视化编辑工具,页面布局基于Bootstrap.大约一个月时间,打通主要技术关卡,实现了第一版:   可以拖放编辑,实现了几乎所有的bootstrap预定义 ...

随机推荐

  1. 银河麒麟等 Linux系统 安装 .net 5,net 6及更高版本的方法

    最近项目上用到 银河麒麟的操作系统,需要搭建 .net 跨平台方案.一开始使用各种命令都安装不上,很多提示命令找不到,或者下载包时候网络无法下载. 网上教程很多,但没有一个是成功的,多数使用 apt ...

  2. React Router@3.x 升级到 @6.x 的实施方案

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:景明 升级背景 目前公司产品有关 react 的工具版本普 ...

  3. WPF的前世今生

    1.WPF的布局 WPF的布局分为相对定位和绝对定位两种. 绝对定位一般用Canvas 相对定位一般用Grid.StackPanel.DockPanel.WrapPanel 2.MVVM模式是什么 M ...

  4. 【译】Silverlight 不会消亡 XAML for Blazor 到来

    Userware 正在使用早已消失的.令人怀念的微软 Silverlight Web 开发平台的遗留来支持其新的"XAML for Blazor"产品,该产品允许 .NET 开发人 ...

  5. Zimbra禁止接收带有加密的文件邮件 提醒病毒(Heuristics.Encrypted.PDF)

    最近碰到一个国际性大客户,一定要发送经过加密的文件,因为是合约相关的文件,对方公司有这方面要求.但是Zimbra默认是禁止接收加密的文件 - 'Block encrypted archives',这样 ...

  6. iFiles浏览iphone文件

    我们希望能在iphone中浏览文件系统的目录

  7. FastDFS入门

    一.系统架构 二.构成部分 1.Tracker Server:跟踪服务器,记录文件信息,可单台或集群部署. 2.Storage Server:存储服务器,文件存储位置,分卷或分组部署. 3.Clien ...

  8. IDEA集成码云gitee

    参考链接:https://blog.csdn.net/bing_bg/article/details/106437008 1.下载安装git https://git-scm.com/download ...

  9. 当你使用Taro时,你需要了解的一些事儿

    2017 年 1 月 9 日凌晨,万众期待的微信小程序正式上线,前有跳一跳等爆圈小游戏的带动,后有特殊时期下各类健康码小程序的加持,小程序成为了国内技术圈独树一帜的存在.但随着小程序的迅猛发展,其实在 ...

  10. Oracle为表添加约束

    转载自:https://blog.csdn.net/qq_38662525/article/details/94192475 创建一个学生表和院系表:院系表为主表,学生表为从表   create ta ...