Communication based on DotNetty
DotNetty : It is a version of Netty implemented by Microsoft's Azure team using C#. It is an excellent network library for the .NET platform.
Project Introduction
OpenDeploy.Communication
Class library project is a communication-related infrastructure layer
-
Codec
Module implements encoding and decoding
-
Convention
Module definition conventions, such as abstract business Handler, message carrierNettyMessage
, message context 'NettyContext', etc.
Custom message format
The message class is NettyMessage
, which encapsulates the message header NettyHeader
and the message body Body
NettyMessage
encapsulates the message header
NettyHeader
and message bodyBody
NettyMessage click to view code
/// <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
Message header, including the unique identifier of the request, whether to synchronize the message, endpoint, etc., will be serialized into JSON when transmitting data.
NettyHeader click to view code
/// <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();
}
-
The unique identifier of the request message
RequestId
is used to uniquely identify the message. It is mainly used to send synchronous requests, because the default message is asynchronous, just send it out without waiting for a response a>
-
Whether to synchronize messages
Sync
, it is not necessary, mainly for visualization and easy debugging
-
Endpoint
EndPoint
, (drawing lessons from MVC, agreed to be Control/Action mode), the server directly parses out the corresponding processor
Encoder
DefaultEncoder click to view code
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);
}
}
}
decoder
DefaultDecoder click to view code
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 implementation
NettyServer click to view code
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退出了...");
}
}
}
-
Finally we added
ServerMessageEntry
to the server pipeline as the entry point for message processing
ServerMessageEntry click to view code
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}");
}
}
}
-
According to the convention, the class that inherits
AbstractNettyHandler
is regarded as a business processor
-
ServerMessageEntry
After getting the message, first encapsulate the message asNettyContext
, similar to HttpContext in MVC, which encapsulates the request and response objects, and internally parses the requestEndPoint
, split intoHandlerName
,ActionName
-
DefaultNettyHandlerSelector
Provides methods for registering processorsRegisterHandlerTypes
, and methods for selecting processorsSelectHandler
-
SelectHandler
, the default rule is to find types starting withHandlerName
in registered processors
-
AbstractNettyHandler
'sProcessAsync
method, throughActionName
, getsMethodInfo
through reflection, and calls the endpoint
NettyClient implementation
NettyClient click to view code
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
Encapsulates Netty client logic and provides methods for sending asynchronous requests (default) and publishing synchronous requests
-
DotNetty
does not provide synchronous requests by default, but in some cases we need to wait for the server's response synchronously. All needs to be implemented by ourselves. The implementation is also very simple. Cache the message ID and activate it after receiving the server response. The specific implementation is as follows Message synchronizerClientMessageSynchronizer
, I won’t post it again
Summarize
So far, we have implemented the communication module based on DotNetty
, implemented the encoding and decoding of the client and the server, processor selection, and the client has implemented synchronization messages, etc. You can see it in < In the console project at the end of /span> modeConsoleHost
, test synchronous and asynchronous messages and implement a simple Echo
code repository
Let’s call the project
OpenDeploy
for now
Welcome everyone to make bricks, Star
Next step
Plan the next step to realize the configuration and discovery of interface projects based onWPF
’s client
Article reprinted from:Broadm
Original link:https://www.cnblogs.com/broadm/p/17875559.html