基于DotNetty实现一个接口自动发布工具 - 通信实现
基于 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,ActionNameDefaultNettyHandlerSelector提供注册处理器的方法RegisterHandlerTypes, 和选择处理器的方法SelectHandlerSelectHandler, 默认规则是查找已注册的处理器中以HandlerName开头的类型AbstractNettyHandler的ProcessAsync方法,通过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实现一个接口自动发布工具 - 通信实现的更多相关文章
- 自动发布工具版本从python2升级成python3后遇到的种种问题(涉及paramiko,Crypto,zipfile等等)
从在公司实习到正式入职,一直还在被同事使用的是我写的一个自动发布工具.该工具的主要功能是:开发人员给出需要更新的代码包(zip格式),测试人员将该代码包部署到测服,这些代码包和JIRA数据库里的项目信 ...
- 基于node实现一个简单的脚手架工具(node控制台交互项目)
实现控制台输入输出 实现文件读写操作 全原生实现一个简单的脚手架工具 实现vue-cli2源码 一.实现控制台输入输出 关于控制台的输入输出依然是基于node进程管理对象process,在proces ...
- 基于SOUI开发一个简单的小工具
基于DriectUI有很多库,比如 Duilib (免费) soui (免费) DuiVision (免费) 炫彩 (界面库免费,UI设计器付费,不提供源码) skinui (免费使用,但不开放源码, ...
- 跨平台自动构建工具v1.0.2 发布
XMake是一个跨平台自动构建工具,支持在各种主流平台上构建项目,类似cmake.automake.premake,但是更加的方便易用,工程描述语法更简洁直观,支持平台更多,并且集创建.配置.编译.打 ...
- BlogPublishTool - 博客发布工具
BlogPublishTool - 博客发布工具 这是一个发布博客的工具.本博客使用本工具发布. 本工具源码已上传至github:https://github.com/ChildishChange/B ...
- 基于Dubbo的http自动测试工具分享
公司是采用微服务来做模块化的,各个模块之间采用dubbo通信.好处就不用提了,省略了之前模块间复杂的http访问.不过也遇到一些问题: PS: Github的代码示例还在整理中... 测试需要配合写消 ...
- 基于数据库的代码自动生成工具,生成JavaBean、生成数据库文档、生成前后端代码等(v6.0.0版)
TableGo v6.0.0 版震撼发布,此次版本更新如下: 1.UI界面大改版,组件大调整,提升界面功能的可扩展性. 2.新增BeautyEye主题,界面更加清新美观,也可以通过配置切换到原生Jav ...
- Go/Python/Erlang编程语言对比分析及示例 基于RabbitMQ.Client组件实现RabbitMQ可复用的 ConnectionPool(连接池) 封装一个基于NLog+NLog.Mongo的日志记录工具类LogUtil 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!
Go/Python/Erlang编程语言对比分析及示例 本文主要是介绍Go,从语言对比分析的角度切入.之所以选择与Python.Erlang对比,是因为做为高级语言,它们语言特性上有较大的相似性, ...
- 5.7 Liquibase:与具体数据库独立的追踪、管理和应用数据库Scheme变化的工具。-mybatis-generator将数据库表反向生成对应的实体类及基于mybatis的mapper接口和xml映射文件(类似代码生成器)
一. liquibase 使用说明 功能概述:通过xml文件规范化维护数据库表结构及初始化数据. 1.配置不同环境下的数据库信息 (1)创建不同环境的数据库. (2)在resource/liquiba ...
- 一个基于Bootstrap实现的HMTL可视化编辑工具
疫情禁足在家,用原生的JS实现了一个HTML可视化编辑工具,页面布局基于Bootstrap.大约一个月时间,打通主要技术关卡,实现了第一版: 可以拖放编辑,实现了几乎所有的bootstrap预定义 ...
随机推荐
- springboot jar thin
springboot jar thin springboot 应用 jar 瘦身.springboot jar 太大.jar与依赖包分离. 两种方法,第一种,spring-boot-thin-laun ...
- .NET周刊【8月第3期 2023-08-20】
国内主题 抓的是周树人,与我鲁迅有什么关系? https://www.cnblogs.com/JulianHuang/p/17642511.html 问题:作者看到了一个关于Dictionary.Cl ...
- 《SQL与数据库基础》06. 函数
目录 函数 字符串函数 数值函数 日期函数 流程函数 本文以 MySQL 为例 函数 函数是指一段可以直接被另一段程序调用的程序或代码. 要查看函数操作的结果,可以使用 SELECT 函数(参数); ...
- 原来你是这样的JAVA--目录
.NET程序员转Java过程中遇到的一些经验分享,陆续更新中. 原来你是这样的Java[01]--基础一瞥 原来你是这样的Java[02]-包.传参.构造器 原来你是这样的Java[03]-继承.多态 ...
- Codeforces Round 882 div.2 A
Smiling&Weeping ----总有人间一两风,填我十万八千梦 A. The Man who became a God time limit per test 1 second mem ...
- Mysql优化篇-索引优化与查询优化
1.索引失败案列 如果查询时没有使用索引,查询语句就会扫描表中所有记录,在数据量大的情况下,查询会很慢. (1)全值匹配 (2)最佳左前缀法则 mysql可以为多个字段创建索引,一个索引可以包括16个 ...
- 6-MySQL查询条件
在MySQL中,高级查询是指使用更复杂的查询语句和操作符来检索和操作数据库中的数据.高级查询可以帮助您更精确地找到所需的信息,并提高查询的效率和灵活性. 以下是高级查询的一些常见应用场景和意义: 连接 ...
- 局域网内文件分享的简单方式:python - http.server
在局域网条件下,利用Python自带的HTTP服务功能提供文件共享服务是相对比较简单便捷的方式之一. 一.现实需求及前提条件 1. 文件的服务端(文件分享者)与接收端(文件接收者)在一个局域网,接收端 ...
- nginx Ingress Controller Packaged by Bitnami
环境介绍 节点 master01 work01 work02 主机/ip calico-master01/192.168.195.135 calico-master01/192.168.195.135 ...
- 【好玩】如何在github主页放一条贪吃蛇
前言 缘由 github放小蛇,就问你烧不烧 起因看到大佬github上有一条贪吃蛇扭来扭去,觉得好玩,遂给大家分享一下本狗的玩蛇历程 成果初展 贪吃蛇 访问地址 https://github.com ...