基於 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
開頭的型別 -
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
模式
程式碼倉庫
專案暫且就叫
OpenDeploy
吧
歡迎大家拍磚,Star
下一步
計劃下一步,基於WPF
的客戶端, 實現介面專案的配置與發現