一、概要
本系列文章主要講述由微軟Azure團隊研發的.net的版本的netty,Dotnetty。所有的開發都將基於.net core 3.1版本進行開發。
Dotnetty是什么,原本Netty是由JBOSS提供的一個java開源框架后來由微軟抄了一份.net的版本, 是業界最流行的NIO框架,整合了多種協議( 包括FTP、SMTP、 HTTP等各種二進制文本協議)的實現經驗,精心設計的框架,在多個大型商業項目中得到充分驗證。
個人使用感受如下:
1.Dotnetty各方面封裝的很好,不要開發者過度關系細節。除了消息協議處理方面(socket網絡通信的分包粘包處理)。
2.使用非常便捷,語法、和各組件結構清晰可重用性高。
3.也是.net core中為數不多看起來比較靠譜的框架,為什么會這么說呢在做股票相關項目時需求調研和技術選型的時候看了很多網絡通信框架。
要么就是.Net Framework的版本,就怕有些開源團隊不持續更新導致致命bug解決成本高等等問題。
二、簡介
本篇文章主要圍繞dotnetty基礎概念和相關知識點來講:
1. NIO和BIO的概念
2. 相關網絡知識
3. Dotnetty 框架介紹
4. Dotnetty Demo的講解
三、主要內容
NIO和BIO、AIO的概念(摘抄自:https://zhuanlan.zhihu.com/p/111816019)
- BIO(同步阻塞):客戶端在請求數據的過程中,保持一個連接,不能做其他事情。
- NIO(同步非阻塞):客戶端在請求數據的過程中,不用保持一個連接,不能做其他事情。(不用保持一個連接,而是用許多個小連接,也就是輪詢)
- AIO(異步非阻塞):客戶端在請求數據的過程中,不用保持一個連接,可以做其他事情。(客戶端做其他事情,數據來了等服務端來通知。)
Dotnetty 框架介紹
目前個人使用下來,主要用到的核心內容如上。
DotNetty.Common 是公共的類庫項目,包裝線程池,並行任務和常用幫助類的封裝
DotNetty.Transport 是DotNetty核心的實現例如:Bootstrapping程序引導類 ,Channels 管道類(socket每有一個連接客戶端就會創建一個channne)等等
DotNetty.Buffers 是對內存緩沖區管理的封裝(主要在接收和發出是對socket通訊內容進行緩存管理)
DotNetty.Codes 是對編碼器解碼器的封裝,包括一些基礎基類的實現,我們在項目中自定義的協議,都要繼承該項目的特定基類和實現(該類庫在整個通訊環節是重中之重)
DotNetty.Handlers 封裝了常用的管道處理器,比如Tls編解碼,超時機制,心跳檢查,日志等。(企業級開發中必不可少的處理類)
Dotnetty Demo的講解
源碼及演示代碼都在官方github上: https://github.com/Azure/DotNetty
開發參考文檔:https://netty.io/wiki/index.html (開發文檔是java的版本,dotnetty都是對着java抄的會有不一樣的地方但是大部分都相同。目前沒有看到比較權威.net版本的文檔)
下面主要分為兩個部分去講解:
Server部分
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Echo.Server
{
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using DotNetty.Codecs;
using DotNetty.Handlers.Logging;
using DotNetty.Handlers.Tls;
using DotNetty.Transport.Bootstrapping;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Sockets;
using DotNetty.Transport.Libuv;
using Examples.Common;
class Program
{
static async Task RunServerAsync()
{
ExampleHelper.SetConsoleLogger();
IEventLoopGroup bossGroup;//主要工作組,設置為2個線程
IEventLoopGroup workerGroup;//子工作組,推薦設置為內核數*2的線程數
if (ServerSettings.UseLibuv)
{
var dispatcher = new DispatcherEventLoopGroup();
bossGroup = dispatcher;
workerGroup = new WorkerEventLoopGroup(dispatcher);
}
else
{
bossGroup = new MultithreadEventLoopGroup(1);//主線程只會實例化一個
workerGroup = new MultithreadEventLoopGroup();//子線程組可以按照自己的需求在構造函數里指定數量
}
X509Certificate2 tlsCertificate = null;
if (ServerSettings.IsSsl)//是否使用ssl套接字加密
{
tlsCertificate = new X509Certificate2(Path.Combine(ExampleHelper.ProcessDirectory, "dotnetty.com.pfx"), "password");
}
try
{
/*
*ServerBootstrap是一個引導類,表示實例化的是一個服務端對象
*聲明一個服務端Bootstrap,每個Netty服務端程序,都由ServerBootstrap控制,
*通過鏈式的方式組裝需要的參數
*/
var bootstrap = new ServerBootstrap();
//添加工作組,其中內部實現為將子線程組內置到主線程組中進行管理
bootstrap.Group(bossGroup, workerGroup);
if (ServerSettings.UseLibuv)//這個ifelse中實例化的是工作頻道,就是處理讀取或者發送socket數據的地方
{
bootstrap.Channel<TcpServerChannel>();
}
else
{
bootstrap.Channel<TcpServerSocketChannel>();
}
bootstrap
.Option(ChannelOption.SoBacklog, 100)
.Option(ChannelOption.SoReuseport, true)//設置端口復用
.Handler(new LoggingHandler("SRV-LSTN"))//初始化日志攔截器
.ChildHandler(new ActionChannelInitializer<IChannel>(channel =>//初始化Tcp服務
{
/*
* 這里主要是配置channel中需要被設置哪些參數,以及channel具體的實現方法內容。
* channel可以理解為,socket通訊當中客戶端和服務端的連接會話,會話內容的處理在channel中實現。
*/
IChannelPipeline pipeline = channel.Pipeline;
if (tlsCertificate != null)
{
pipeline.AddLast("tls", TlsHandler.Server(tlsCertificate));//添加ssl加密
}
pipeline.AddLast(new LoggingHandler("SRV-CONN"));
pipeline.AddLast("framing-enc", new LengthFieldPrepender(2));//Dotnetty自帶的編碼器,將要發送的內容進行編碼然后發送
pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(ushort.MaxValue, 0, 2, 0, 2));//Dotnetty自帶的解碼器,將接受到的內容進行解碼然后根據內容對應到業務邏輯當中
pipeline.AddLast("echo", new EchoServerHandler());//server的channel的處理類實現
}));
IChannel boundChannel = await bootstrap.BindAsync(ServerSettings.Port);//指定服務端的端口號,ip地址donetty可以自動獲取到本機的地址。也可以在這里手動指定。
Console.ReadLine();
await boundChannel.CloseAsync();//關閉
}
finally
{
//關閉釋放並退出
await Task.WhenAll(
bossGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)),
workerGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)));
}
}
static void Main() => RunServerAsync().Wait();
}
}
channel的實現細節(socket會話內容處理和業務邏輯都可以在這里處理)
// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Echo.Server { using System; using System.Text; using DotNetty.Buffers; using DotNetty.Transport.Channels; /// <summary> /// 該類為Server的Channel具體和定義實現 /// </summary> public class EchoServerHandler : ChannelHandlerAdapter { /* * Channel的生命周期 * 1.ChannelRegistered 先注冊 * 2.ChannelActive 再被激活 * 3.ChannelRead 客戶端與服務端建立連接之后的會話(數據交互) * 4.ChannelReadComplete 讀取客戶端發送的消息完成之后 * error. ExceptionCaught 如果在會話過程當中出現dotnetty框架內部異常都會通過Caught方法返回給開發者 * 5.ChannelInactive 使當前頻道處於未激活狀態 * 6.ChannelUnregistered 取消注冊 */ /// <summary> /// 頻道注冊 /// </summary> /// <param name="context"></param> public override void ChannelRegistered(IChannelHandlerContext context) { base.ChannelRegistered(context); } /// <summary> /// socket client 連接到服務端的時候channel被激活的回調函數 /// </summary> /// <param name="context"></param> public override void ChannelActive(IChannelHandlerContext context) { //一般可用來記錄連接對象信息 base.ChannelActive(context); } /// <summary> /// socket接收消息方法具體的實現 /// </summary> /// <param name="context">當前頻道的句柄,可使用發送和接收方法</param> /// <param name="message">接收到的客戶端發送的內容</param> public override void ChannelRead(IChannelHandlerContext context, object message) { var buffer = message as IByteBuffer; if (buffer != null) { Console.WriteLine("Received from client: " + buffer.ToString(Encoding.UTF8)); } context.WriteAsync(message);//這里官方的例子是直接將客戶端發送的內容原樣返回給客戶端,WriteAsync()是講要發送的內容寫入到數據流的緩存中。如果不想進入數據流可以直接調用WirteAndFlusAsync()寫好了直接發送 } /// <summary> /// 該次會話讀取完成后回調函數 /// </summary> /// <param name="context"></param> public override void ChannelReadComplete(IChannelHandlerContext context) => context.Flush();//將WriteAsync寫入的數據流緩存發送出去 /// <summary> /// 異常捕獲 /// </summary> /// <param name="context"></param> /// <param name="exception"></param> public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) { Console.WriteLine("Exception: " + exception); context.CloseAsync(); } /// <summary> /// 當前頻道未激活狀態 /// </summary> /// <param name="context"></param> public override void ChannelInactive(IChannelHandlerContext context) { base.ChannelInactive(context); } /// <summary> /// 取消注冊當前頻道,可理解為銷毀當前頻道 /// </summary> /// <param name="context"></param> public override void ChannelUnregistered(IChannelHandlerContext context) { base.ChannelUnregistered(context); } } }
Client部分:
其他未注釋部分與服務端的解釋一樣
// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Echo.Client { using System; using System.IO; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using DotNetty.Codecs; using DotNetty.Handlers.Logging; using DotNetty.Handlers.Tls; using DotNetty.Transport.Bootstrapping; using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Sockets; using Examples.Common; class Program { static async Task RunClientAsync() { ExampleHelper.SetConsoleLogger(); var group = new MultithreadEventLoopGroup();//客戶端與服務端不同的是,只需要一個主工作組進行工作協調即可不需要創建子線程組 X509Certificate2 cert = null; string targetHost = null; if (ClientSettings.IsSsl) { cert = new X509Certificate2(Path.Combine(ExampleHelper.ProcessDirectory, "dotnetty.com.pfx"), "password"); targetHost = cert.GetNameInfo(X509NameType.DnsName, false); } try { var bootstrap = new Bootstrap(); bootstrap .Group(group) .Channel<TcpSocketChannel>() .Option(ChannelOption.TcpNodelay, true)//設置為true的話不允許延遲直接發出,因為dotnetty內部實現中會將消息積累到一定的字節之后才發出。 .Handler(new ActionChannelInitializer<ISocketChannel>(channel => { IChannelPipeline pipeline = channel.Pipeline; if (cert != null) { pipeline.AddLast("tls", new TlsHandler(stream => new SslStream(stream, true, (sender, certificate, chain, errors) => true), new ClientTlsSettings(targetHost))); } pipeline.AddLast(new LoggingHandler());//donetty框架內部日志 pipeline.AddLast("framing-enc", new LengthFieldPrepender(2)); pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(ushort.MaxValue, 0, 2, 0, 2)); pipeline.AddLast("echo", new EchoClientHandler());//client的channel的處理類實現 })); IChannel clientChannel = await bootstrap.ConnectAsync(new IPEndPoint(ClientSettings.Host, ClientSettings.Port));//設置服務端的端口號和ip地址 Console.ReadLine(); await clientChannel.CloseAsync(); } finally { await group.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)); } } static void Main() => RunClientAsync().Wait(); } }
廢話不多說官方的demo源碼看過了接下來看看效果,啟動順序為1.服務端-->2.客戶端
如果出現以上情況,則代表服務端啟動成功了。接下來啟動客戶端這時候有小伙伴會納悶我已經F5 vs已經跑起來了服務端這時候客戶端如何開啟呢下圖所示
運行效果:
到這里大致我們對dotnetty的框架有個初步的認識,后面的文章中將會逐漸加深對這套框架的理解並寫實戰項目以供大家學習。如果有想看教學視頻的可以在博客的下方留言如果留言人數較多則考慮在b站放出教學視頻更新頻率也會更高。
希望大家多多支持。不勝感激。
- E-Mail:zhuzhen723723@outlook.com
- QQ: 580749909(個人群)
- Blog: https://www.cnblogs.com/justzhuzhu/
- Git: https://github.com/JusterZhu
- 微信公眾號