基于 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. Hybrid App 技术路径带动性能的提升

    说到 Hybrid App(混合应用)大家都不陌生,因为这种开发模式大行其道发展的这些年取代了很多原生和 Web 应用,为什么大家对这种「Native + HTML5」的开发模式额外偏爱呢? 因为一方 ...

  2. 【JMeter】常用线程组设置策略

    常用线程组设置策略 目录 常用线程组设置策略 一.前言 二.单场景基准测试 1.介绍 2.线程组设计 3.测试结果 三.单场景并发测试 1.介绍 2.线程组设计 3.测试结果 四.单场景容量/爬坡测试 ...

  3. WPF-实现屏幕截图(一)

    源码路径:https://gitee.com/LiuShuiRuoBing/wpf_screen_cut 实现功能 实现基本的截屏窗体 鼠标随意选择截图区域 鼠标抬起时弹出按钮区 快捷键Ctrl+Al ...

  4. PHP写一个 Api接口需要注意哪些?考虑哪些?

    随着互联网的飞速发展,前后端分离的开发模式越来越流行.编写一个稳定.可靠和易于使用的 API 接口是现代互联网应用程序的关键.本文将介绍在使用 thinkphp6 框架开发 API 接口时需要注意的要 ...

  5. VulnStack - ATT&CK红队评估实战(一) Writeup

    VulnStack - ATT&CK红队评估实战(一) Writeup VulnStack(一)环境搭建 项目地址 http://vulnstack.qiyuanxuetang.net/vul ...

  6. JDK17和JDK8在windows上同时安装方便切换

    参考:https://blog.csdn.net/chencaw/article/details/121674479 一.JDK8的安装 1.电脑上已经安装了JDK8,安装主要步骤如下 (1)创建JA ...

  7. 【接口自动化测试】Eolink Apilkit 安装部署,支持 Windows、Mac、Linux 等系统

    Eolink Apikit 有三种客户端,可以依据自己的情况选择.三种客户端的数据是共用的,因此可以随时切换不同的客户端. 我们推荐使用新推出的 Apikit PC 客户端,PC 端拥有线上产品所有的 ...

  8. C语言条件运算符(?:)

    条件运算符(conditional operator)有时候也称为三元运算符(ternary operator,或者trinary operator),因为它是唯一需要 3 个操作数的运算符: 条件 ...

  9. D 算法模板(Boruvka's Algorithm)

    Description 为了方便你测试,本题为D题简化版. You are given a complete undirected graph with nn vertices. A number a ...

  10. client-go实战之八:更新资源时的冲突错误处理

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是<client-go实战> ...