一、概要

在上一篇文章讲到Dotnetty的基本认识,本文这次会讲解dotnetty非常核心的模块是属于比较硬核的干货了,然后继续往下讲解如何根据自己的需求或者自己的喜好去配置Dotnetty而不是生搬硬套官网的示例源码。如果看了本文有收获的话麻烦关注一下文章尾部的公众号和技术讨论群。各位的支持是对我莫大的帮助。

二、简介

主要讲解一下几个知识点:

  • EventLoopGroup & EventLoop
  • Bootstrap
  • Channel
  • ChannelPipeline & ChannelHandler
  • ChannelHandlerContext
  • ChannelHandler

三、详细内容

1.EventLoopGroup & EventLoop

  • 高性能RPC框架的3个要素:IO模型、数据协议、线程模型
  • EventLoop好比一个线程,1个EventLoop可以服务多个Channel,1个Channel只有一个EventLoop可以创建多个 EventLoop 来优化资源利用,也就是EventLoopGroup。
  • EventLoopGroup 负责分配 EventLoop 到新创建的 Channel,里面包含多个EventLoop
  • EventLoopGroup →多个 EventLoop ,EventLoop →维护一个 Selector。

2.服务器启动引导类:ServerBootstrap

Group :设置线程组模型,Reactor线程模型对比EventLoopGroup

  • 单线程

  • 多线程

  • 主从线程

Channel:设置channel通道类型NioServerSocketChannel、OioServerSocketChannel

Option: 作用于每个新建立的channel,设置TCP连接中的一些参数,如下:

  • ChannelOption.SO_BACKLOG: 存放已完成三次握手的请求的等待队列的最大长度;

  • ChannelOption.TCP_NODELAY: 为了解决Nagle的算法问题,默认是false, 要求高实时性,有数据时马上发送,就将该选项设置为true关闭Nagle算法;如果要减少发送次数,就设置为false,会累积一定大小后再发送。

  • ChildOption: 作用于被accept之后的连接

  • ChildHandler: 用于对每个通道里面的数据处理

3.连接通道类:Channel

Channel: 客户端和服务端建立的一个连接通道(可以理解为一个channel就是一个socket连接) ChannelHandler: 负责Channel的逻辑处理 ChannelPipeline: 负责管理ChannelHandler的有序容器

关系: 一个Channel包含一个ChannelPipeline,所有ChannelHandler都会顺序加入到ChannelPipeline中 创建 Channel时会自动创建一个ChannelPipeline,每个Channel都有一个管理它的pipeline,这关联是永久 性的Channel当状态出现变化,就会触发对应的事件。

生命周期:

  • ChannelRegistered: channel注册到一个EventLoop

  • ChannelActive: 变为活跃状态(连接到了远程主机),可以接受和发送数据

  • ChannelInactive: channel处于非活跃状态,没有连接到远程主机

  • ChannelUnregistered: channel已经创建,但是未注册到一个EventLoop里面,也就是没有和Selector绑定

4.频道的内部实现 ChannelHandler & ChannelPipeline

  • ChannelInboundHandler:(入站) 处理输入数据和Channel状态类型改变,适配器。

  • ChannelInboundHandlerAdapter(适配器设计模式) 常用的:SimpleChannelInboundHandler

  • ChannelOutboundHandler:(出站) 处理输出数据,适配器 ChannelOutboundHandlerAdapter

  • ChannelPipeline: 好比厂里的流水线一样,可以在上面添加多个ChannelHanler,也可看成是一串

  • ChannelHandler 实例,拦截穿过 Channel 的输入输出 event, ChannelPipeline 实现了拦截器的一种高级形 式,使得用户可以对事件的处理以及ChannelHanler之间交互获得完全的控制权。

5.频道的内部实现 ChannelHandler & ChannelPipeline

ChannelHandlerContext是连接ChannelHandler和ChannelPipeline的桥梁,ChannelHandlerContext部分方法和Channel及ChannelPipeline重合。

  • 好比调用write方法Channel、ChannelPipeline、ChannelHandlerContext 都可以调用此方法,前两者都会在整个管道流里 传播,而ChannelHandlerContext就只会在后续的Handler里面传播。
  • AbstractChannelHandlerContext类双向链表结构,next/prev分别是后继节点,和前驱节点。
  • DefaultChannelHandlerContext 是实现类,但是大部分都是父类那边完成,这个只是简单的实现一些方法 主要就是判断Handler的类型。
  • ChannelInboundHandler之间的传递,主要通过调用ctx里面的FireXXX()方法来实现下个handler的调用。

6.Handler执行顺序

一般的项目中,inboundHandler和outboundHandler有多个,在Pipeline中的执行顺序?

InboundHandler顺序执行,OutboundHandler逆序执行

  • InboundHandler顺序执行,OutboundHandler逆序执行
  • InboundHandler之间传递数据,通过context.fireChannelRead(message)
  • InboundHandler通过context.write(message),则会传递到outboundHandler
  • 使用context.write(msg)传递消息,Inbound需要放在结尾,在Outbound之后,不然outboundhandler会不执行; 但是使用channel.write(msg)、pipline.write(msg)情况会不一致,都会执行。
  • OutBound和Inbound谁先执行,针对客户端和服务端而言,客户端是发起请求再接受数据,先outbound再 inbound,服务端则相反。

四、实战环节

以上概念性的东西介绍完了之后开始编写本章实战代码(完整的案例代码将在qq群文件共享里上传,文章末尾有QQ群二维码和联系方式)。接下来我们先看一下项目结构。

Handlers - 主要存放所有处理相关类。

Initializer - 存放初始化tcp服务的相关内容。

appsetting.json - 主要存放的内容为,服务端的相关配置例如:ip地址、端口号等。

dotnetty - 安全证书

Program - 启动类


项目结构介绍完毕之后,我大致将这个demo分为5个部分来实现具体根据自己需求去设计搭建结构都是可以的,这里的内容仅供参考。

  • 第一步,配置构建引导类

         //主要工作组,设置为2个线程
private static readonly IEventLoopGroup bossGroup = new MultithreadEventLoopGroup();
//子工作组,默认为内核数*2的线程数
private static readonly IEventLoopGroup workerGroup = new MultithreadEventLoopGroup(); static async Task RunAsync() {
/*
*初始化服务端引导对象。
*声明一个服务端Bootstrap,每个Netty服务端程序,都由ServerBootstrap控制,
*通过链式的方式组装需要的参数
*/
ServerBootstrap bootstrap = new ServerBootstrap();
//添加工作组
bootstrap.Group(bossGroup, workerGroup);
//初始化工作频道
bootstrap.Channel<TcpServerSocketChannel>();
bootstrap
//存放已完成三次握手的请求的等待队列的最大长度;
.Option(ChannelOption.SoBacklog, )
//ByteBuf的分配器(重用缓冲区)大小
.Option(ChannelOption.Allocator, UnpooledByteBufferAllocator.Default)
//接收字符的长度
.Option(ChannelOption.RcvbufAllocator, new FixedRecvByteBufAllocator( * ))
//保持长连接
.ChildOption(ChannelOption.SoKeepalive, true)
//取消延迟发送
.ChildOption(ChannelOption.TcpNodelay, true)
//端口复用
.ChildOption(ChannelOption.SoReuseport, true)
//初始化日志拦截器,可以不加
.Handler(new LoggingHandler("SRV-LSTN"))
//自定义初始化Tcp服务
.ChildHandler(new EchoServerInitializer()); //绑定服务端,端口号。IP地址默认读取项目配置文件。
await bootstrap.BindAsync(ServerSettings.Port);
}
  • 第二步,初始化Channel相关处理类
  /// <summary>
/// 初始化
/// </summary>
public class EchoServerInitializer : ChannelInitializer<ISocketChannel>
{
/// <summary>
/// No interaction time.300s
/// </summary>
public const int AllTimeOut = * ; /// <summary>
/// Read Time Out.60s
/// </summary>
public const int ReadTimeOut = ; /// <summary>
/// Recive Time Out.60s
/// </summary>
public const int WriterTimeOut = ; protected override void InitChannel(ISocketChannel channel)
{
/*
* 工作线程连接器是设置了一个频道,服务端主线程所有接收到的信息都会通过这个管道一层层往下传输
* 同时所有出栈的消息 也要这个频道的所有处理器进行一步步处理
*/
IChannelPipeline pipeline = channel.Pipeline;
//初始化Dotnetty日志拦截器
pipeline.AddLast(new LoggingHandler("SRV-CONN"));
//心跳超时时间配置
pipeline.AddLast(new IdleStateHandler(
ReadTimeOut,
WriterTimeOut,
AllTimeOut));
//消息内容编码逻辑处理类
pipeline.AddLast("encoder", new EncoderHandler());
//解码逻辑处理类
pipeline.AddLast("decoder", new DecoderHandler());
//心跳逻辑处理
pipeline.AddLast(new HeartBeatHandler());
//每个频道请求消息处理类
pipeline.AddLast(new ServerHandler());
}
}
  • 第三步,配置、实现心跳处理机制
  public class HeartBeatHandler : ChannelHandlerAdapter
{
/// <summary>
/// 每个频道都有自己的心跳管理,如果频道长时间不操作踢掉线的逻辑可以写在这里
/// </summary>
/// <param name="context"></param>
/// <param name="evt"></param>
public override void UserEventTriggered(IChannelHandlerContext context, object evt)
{
var eventState = evt as IdleStateEvent;
if (eventState != null)
{
String type = string.Empty;
if (eventState.State == IdleState.ReaderIdle)
{
type = "read idle";//没有任何接受
}
else if (eventState.State == IdleState.WriterIdle)
{
type = "write idle";//没有任何写入
}
else if (eventState.State == IdleState.AllIdle)
{
type = "all idle";
context.CloseAsync();//5分钟内无任何交互则断开该客户端连接
}
}
else
{
base.UserEventTriggered(context, evt);
}
}
}
  • 第四步,编码、解码
  /// <summary>
/// 解码
/// </summary>
public class DecoderHandler : ByteToMessageDecoder
{
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
throw new NotImplementedException();
}
} public class EncoderHandler : MessageToByteEncoder<byte[]>
{
/// <summary>
/// 编码
/// </summary>
/// <param name="context"></param>
/// <param name="message"></param>
/// <param name="output"></param>
protected override void Encode(IChannelHandlerContext context, byte[] message, IByteBuffer output)
{
throw new NotImplementedException();
}
}
  • 第五步,Channel逻辑处理实现
 public class ServerHandler : 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);//发送给客户端方法
} /// <summary>
/// 该次会话读取完成后回调函数
/// </summary>
/// <param name="context"></param>
public override void ChannelReadComplete(IChannelHandlerContext context) => context.Flush();// /// <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);
}
}

希望大家多多支持。不胜感激。

.NET Core3.1 Dotnetty实战第二章的更多相关文章

  1. .NET Core3.1 Dotnetty实战第一章

    一.概要 本系列文章主要讲述由微软Azure团队研发的.net的版本的netty,Dotnetty.所有的开发都将基于.net core 3.1版本进行开发. Dotnetty是什么,原本Netty是 ...

  2. #Spring实战第二章学习笔记————装配Bean

    Spring实战第二章学习笔记----装配Bean 创建应用对象之间协作关系的行为通常称为装配(wiring).这也是依赖注入(DI)的本质. Spring配置的可选方案 当描述bean如何被装配时, ...

  3. AS开发实战第二章学习笔记——其他

    第二章学习笔记(1.19-1.22)像素Android支持的像素单位主要有px(像素).in(英寸).mm(毫米).pt(磅,1/72英寸).dp(与设备无关的显示单位).dip(就是dp).sp(用 ...

  4. RxJava2实战--第二章 RxJava基础知识

    第二章 RxJava基础知识 1. Observable 1.1 RxJava的使用三步骤 创建Observable 创建Observer 使用subscribe()进行订阅 Observable.j ...

  5. .NET ORM框架HiSql实战-第二章-使用Hisql实现菜单管理(增删改查)

    一.引言 上一篇.NET ORM框架HiSql实战-第一章-集成HiSql 已经完成了Hisql的引入,本节就把 项目中的菜单管理改成hisql的方式实现. 菜单管理界面如图: 二.修改增删改查相关代 ...

  6. .NET Core3.1 Dotnetty实战第三章

    一.概要 本章主要内容就是讲解如何在dotnetty的框架中进行网络通讯以及编解码对象.数据包分包拆包的相关知识点. 后续会专门开一篇避坑的文章,主要会描述在使用dotnetty的框架时会遇到的哪些问 ...

  7. activiti实战--第二章--搭建Activiti开发环境及简单示例

    (一)搭建开发环境 学习资料:<Activiti实战> 第一章 认识Activiti 2.1 下载Activiti 官网:http://activiti.org/download.html ...

  8. 2017.2.20 activiti实战--第二章--搭建Activiti开发环境及简单示例(二)简单示例

    学习资料:<Activiti实战> 第一章 搭建Activiti开发环境及简单示例 2.5 简单流程图及其执行过程 (1)leave.bpmn 后缀名必须是bpmn.安装了activiti ...

  9. 2017.2.20 activiti实战--第二章--搭建Activiti开发环境及简单示例(一)搭建开发环境

    学习资料:<Activiti实战> 第一章 认识Activiti 2.1 下载Activiti 官网:http://activiti.org/download.html 进入下载页后,可以 ...

随机推荐

  1. PHP popen() 函数

    定义和用法 popen() 函数使用 command 参数打开进程文件指针. 如果出错,该函数返回 FALSE. 语法 popen(command,mode) 参数 描述 command 必需.规定要 ...

  2. PDOStatement::bindColumn

    PDOStatement::bindColumn — 绑定一列到一个 PHP 变量(PHP 5 >= 5.1.0, PECL pdo >= 0.1.0) 说明 语法 bool PDOSta ...

  3. CF R 632 div2 1333D Challenges in school №41

    LINK:Challenges in school №41 考试的时候读错题了+代码UB了 所以wa到自闭 然后放弃治疗. 赛后发现UB的原因是 scanf读int类型的时候 宏定义里面是lld的类型 ...

  4. rabbitMQ结合spring-boot使用(1)

    从这一节开始我们进入rabbitMQ的实战环节,项目环境是spring-boot 加maven.首先让我们创建一个spring-boot项目,然后引入web依赖和 rabbitMQ的依赖 <de ...

  5. Java基础高级篇 NIO

    nio模型与io模型的对比 netty 是什么 怎么使用

  6. 为何选择spark!

    随着大数据处理的应用场景越来越多,人们对Hadoop的要求也越来越高,开发出的对应的系统也越来越多,人们迫切的需要一个综合的计算框架,Spark应运而生,我们可以看看Spark可以干些什么. 那么为什 ...

  7. Vouch-proxy 实现 Zabbix4.4 对接 SSO

    Vouch-proxy 实现 Zabbix 对接 SSO Zabbix 自身不支持 SSO 对接,我使用 Nginx 代理 Zabbix,将请求转发至 Vouch-proxy,由 Vouch-prox ...

  8. NLP Github

    作者:cstghitpku链接:https://zhuanlan.zhihu.com/p/51279338来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 1.分词 Wo ...

  9. 【模式识别与机器学习】——PCA主成分分析

    基本思想 其基本思想就是设法提取数据的主成分(或者说是主要信息),然后摒弃冗余信息(或次要信息),从而达到压缩的目的.本文将从更深的层次上讨论PCA的原理,以及Kernel化的PCA. 引子 首先我们 ...

  10. 2020-07-20:你觉得redis有什么缺点,给你改进的话你会怎么改进?

    福哥答案2020-07-20: 1.由于 Redis 是内存数据库,短时间内大量增加数据,可能导致内存不够用.2.redis是单线程的,单台服务器无法充分利用多核服务器的CPU.3.遇到大量查询时容易 ...