Adapter,适配器模式:将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。
http://www.cnblogs.com/PatrickLiu/p/7660554.html
http://www.cnblogs.com/zhili/p/AdapterPattern.html
Adapter,适配器模式:将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。
http://www.cnblogs.com/PatrickLiu/p/7660554.html
http://www.cnblogs.com/zhili/p/AdapterPattern.html
Excerpt
写本系列文章的目的我一直以来都在从事.NET相关的工作,做过工控,做过网站,工作初期维护过别人写的网络库,后来自己写网络库,我发现在使用C#编程的程序员中,能否写出高性能的网络库一直都是考验一个程序员能力的标杆。为了写出高性能的网络库,我查阅了很多资料,发现Java的Netty有着得天独厚的设计以及实现优势,Java也因为Netty的存在,在开发大吞吐量的应用程序中得心应手。我想,.NET程序…
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
我一直以来都在从事.NET相关的工作,做过工控,做过网站,工作初期维护过别人写的网络库,后来自己写网络库,我发现在使用C#编程的程序员中,能否写出高性能的网络库一直都是考验一个程序员能力的标杆。为了写出高性能的网络库,我查阅了很多资料,发现Java的Netty有着得天独厚的设计以及实现优势,Java也因为Netty的存在,在开发大吞吐量的应用程序中得心应手。
我想,.NET程序员为什么不能使用这么好的应用程序框架。好在,Azure团队写出了DotNetty,使得.NET程序员也可以迅速的,便捷的搭建一个高性能的网络应用程序,但是,DotNetty并没有多少资料,项目代码中也没有多少注释,这对我们的学习以及使用带来了极大的障碍。
我通过对于Netty的研究,一步步的使用DotNetty来创建应用程序,分析DotNetty实现了哪些,没有实现哪些,实现的有何不同,希望通过最简单的描述,让读者能够了解DotNetty,无论是在工作学习中快速搭建网络应用程序还是通过分析Netty的思想,为自己写的网络库添砖加瓦都是十分有意义的。
本系列文章参考了《Netty实战》,感兴趣的同学可以去看看这本书。
Netty 是一款用于创建高性能网络应用程序的高级框架。
Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器
和客户端
DotNetty是微软的Azure团队仿造Netty编写的网络应用程序框架。
异步和事件驱动是Netty设计的关键
1 | public class ConnectHandler : SimpleChannelInboundHandler<string> |
Future:通知的另一种方式,可以认为ChannelFuture是包装了一系列Channel事件的对象。回调和Future相互补充,相互结合同时也可以理解Future是一种更加精细的回调。
但是ChannelFuture在DotNetty中被Task取代
事件和ChannelHandler
ChannelHandler是事件处理器,负责处理入站事件和出站事件。通常每一个事件都由一系列的Handler处理。
本文参考资料以及截图来自《Netty实战》
Excerpt
ChannelPipeline和ChannelHandleContext介绍ChannelPipeline是一系列ChannelHandler连接的实例链,这个实例链构成了应用程序逻辑处理的核心。下图反映了这种关联:ChannelHandlerContext提供了一个ChannelPipeline的上下文,用于ChannelHandler在Pipeline中的交互,这种交互十分的灵活,不仅…
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
ChannelPipeline是一系列ChannelHandler连接的实例链,这个实例链构成了应用程序逻辑处理的核心。下图反映了这种关联:
ChannelHandlerContext提供了一个ChannelPipeline的上下文,用于ChannelHandler在Pipeline中的交互,这种交互十分的灵活,不仅是信息可以交互,甚至可以改变其他Handler在Pipeline中的位置。
我们可以看到fire方法都是调用下一个Handler中的方法,我们可以在合适的时机调用下一个Handler中的方法以实现数据的流动。
这里我们注意一下,Write方法并不会将消息写入Socket中,而是写入消息队列中,等待Flush将数据冲刷下去。
我们可以发现,Pipeline上也有fire–的方法,Context也有类似的方法,他们的差别在于,Pipeline或者Channel上的这些方法引发的事件流将从Pipeline的头部开始移动,而Context上的方法会让事件从当前Handler开始移动,所以为了更短的事件流,我们应该尽可能的使用Context的方法。
获取当前Channel
1 | IChannelHandlerContext ctx = ...; |
获取当前pipeline
1 | // 注意一下在Netty中可以直接通过context获取pipeline,在DotNetty中需要从Channel中获取 |
写入pipeline让事件从尾端开始移动
1 | IChannel channel = ctx.Channel; |
注意,Write是出站事件,他的流动方向是从末尾到头部,这个一定要注意。在pipeline或者channel中写入事件,都是从最末尾开始流动,在Context中写入是从当前Handler中开始移动,这个我们已经在很多地方都说明了这样的不同。
在Netty中,如果想要将一个Handler用于多个Pipeline中,需要标注Shared,同时需要保证线程安全,因为这里可能有多线程的重入问题。
Excerpt
组件介绍ChannelChannel是Socket的封装,提供绑定,读,写等操作,降低了直接使用Socket的复杂性。EventLoop我们之前就讲过EventLoop这里回顾一下:一个 EventLoopGroup 包含一个或者多个 EventLoop;一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;所有由 EventLoop 处理的 I/O 事件都将在它…
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
Channel是Socket的封装,提供绑定,读,写等操作,降低了直接使用Socket的复杂性。
我们之前就讲过EventLoop这里回顾一下:
本身是Channel中消息的回调,在DotNetty中被Task取代。
ChannelHandler是处理数据的逻辑容器
ChannelInboundHandler是接收并处理入站事件的逻辑容器,可以处理入站数据以及给客户端以回复。
ChannelPipeline是将ChannelHandler穿成一串的的容器。
需要说明的是:
(尝试一下)在 Netty 中,有两种发送消息的方式。你可以直接写到 Channel 中,也可以 写到和 ChannelHandler相关联的ChannelHandlerContext对象中。前一种方式将会导致消息从ChannelPipeline 的尾端开始流动,而后者将导致消息从 ChannelPipeline 中的下一个 ChannelHandler 开始流动。
Netty中内置了一些编码器和解码器,用来进行处理字节流数据,编码器用来将消息编码为字节流,解码器用来将字节流解码为另一种格式(字符串或一个对象)。
需要注意的是,编码器和解码器都实现了ChannelInboundHandler和 ChannelOutboundHandler接口用于处理入站或出站数据。
Excerpt
引导Bootstrap引导一个应用程序是指对他进行配置并且使他运行的过程。体系结构注意,DotNetty没有实现Cloneable的接口,而是直接实现了一个Clone方法。Netty实现这个接口是为了创建两个有着相同配置的应用程序,可以把一个配置整体应用到另一个上面,需要注意的是EventLoopGroup是一个浅拷贝,这就导致了拷贝的Bootstrap都会使用同一个EventLoopGr…
引导一个应用程序是指对他进行配置并且使他运行的过程。
注意,DotNetty没有实现Cloneable的接口,而是直接实现了一个Clone方法。Netty实现这个接口是为了创建两个有着相同配置的应用程序,可以把一个配置整体应用到另一个上面,需要注意的是EventLoopGroup是一个浅拷贝,这就导致了拷贝的Bootstrap都会使用同一个EventLoopGroup,这在每个Channel生命周期很短的时候是没有太大影响的。
服务器引导和普通引导有什么区别呢?区别在于,服务器接收到客户端的连接请求,会用一个Channel接受连接,然后用另一个Channel与客户端进行交流,但是客户端只需要一个Channel就可以与服务器进行交互。
我们发现Bootstrap类可以通过流式语法进行链式调用,这要归功于Bootstrap类的特殊定义。下面我们来看一下:
1 | // 定义 |
1 | var group = new MultithreadEventLoopGroup(); |
API:
注意上面箭头指示的是与Bootstrap不一样的方法。
为什么会有子Channel的概念呢,我们看下面这个图:
因为服务器是一对多的,所以有子Channel的概念。
1 | IEventLoopGroup eventLoop; |
场景
如果我们的服务器需要去第三方获取数据,这时候服务器就需要充当客户端去第三方取数据,这时候就需要在Channel中再开一个客户端获取数据。
方式
我们最好是从Channel中获取当前EventLoop,这样新开的客户端就跟当前Channel在一个线程中,减少了线程切换带来的开销,尽可能的重用了EventLoop
实现
1 | // 从Context创建客户端引导 |
如果要添加的Handler不止一个,我们就需要用到ChannelInitializer,在DotNetty中,我们有十分简单的方法可以初始化一个pipeline
1 | var bootstrap = new Bootstrap(); |
ChannelOption可以在引导的时候将设置批量的设置到所有Channel上,而不必要在每一个Channel建立的时候手动的去指定它的配置,应用场景是比如设置KeepAlive或者设置超时时间。
1 | bootstrap.Option(ChannelOption.SoKeepalive, true) |
UDP的全称是“User Datagram Protocol”,在DotNetty中实现了SocketDatagramChannel来创建无连接的引导,需要注意的是无连接的引导不需要Connect只需要bind即可,代码如下:
1 | var bootstrap = new Bootstrap(); |
Channel的关闭:
1 | await clientChannel.CloseAsync(); |
EventLoopGroup的关闭:
1 | await group.ShutdownGracefullyAsync(); |
Excerpt
第一个DotNetty应用程序准备工作NuGet包介绍DotNetty由九个项目构成,在NuGet中都是单独的包,可以按需引用,其中比较重要的几个是以下几个:DotNetty.Common 是公共的类库项目,包装线程池,并行任务和常用帮助类的封装DotNetty.Transport 是DotNetty核心的实现DotNetty.Buffers 是对内存缓冲区管理的封装DotNett…
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
DotNetty由九个项目构成,在NuGet中都是单独的包,可以按需引用,其中比较重要的几个是以下几个:
新建一个解决方案 名字叫NettyTest
新建一个项目 名字叫EchoServer
到NuGet中引用 DotNetty.Common DotNetty.Transport DotNetty.Buffers
新建一个类 EchoServerHandler
1 | using DotNetty.Buffers; |
上面的代码注释已经非常详细了,相信看注释你就能明白这个类大致干了些什么,但是突如其来的一个类还是有点难以理解,那么本着认真负责的精神我会再详细解释一下没有学过Netty的同学难以理解的点:
在自动生成的Program.cs中写入服务器引导程序。
1 | using DotNetty.Transport.Bootstrapping; |
这个程序中同样有很多需要解释的,但是对于初学者来说,先明白这些概念就好了:
再新建一个项目取名叫EchoClient
新建一个类 EchoClientHandler
1 | using DotNetty.Buffers; |
Handler的编写方法于上面服务器的Handler基本一致,这里我们还是需要解释一些问题:
编写客户端引导程序
1 | using DotNetty.Transport.Bootstrapping; |
项目的完整代码我放在了码云上,你可以点击这里可以下载。我相信很多完全没有接触过Netty的同学在跟着写完了第一个项目之后还是很懵,虽然解释了很多,但是还是感觉似懂非懂,这很正常。就如同我们写完HelloWorld之后,仍然会纠结一下static void Main(string[] args)为什么要这么写。我要说的是,只要坚持写完了第一个应用程序,你就是好样的,关于Netty我们还有很多很多要讲,相信你学了之后的知识以后,回过头来再看这个实例,会有恍然大悟的感觉。如果你坚持看完了文章并且敲了程序并且试验成功了,恭喜你,晚饭加个鸡腿,我们还有很多东西要学。
Excerpt
ChannelHandler本篇文章着重介绍ChannelHandlerChannel的生命周期我们复习一下,Channel是Socket的抽象,可以被注册到一个EventLoop上,EventLoop相当于Selector,每一个EventLoop又有自己的处理线程。复习了这部分的知识,我们就知道在Channel的生命中,有以下这么几个关键的时间节点。ChannelHandler的生…
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本篇文章着重介绍ChannelHandler
我们复习一下,Channel是Socket的抽象,可以被注册到一个EventLoop上,EventLoop相当于Selector,每一个EventLoop又有自己的处理线程。复习了这部分的知识,我们就知道在Channel的生命中,有以下这么几个关键的时间节点。
我们复习一下,ChannelHandler是定义了如何处理数据的处理器,被串在ChannelPipeline中用于入站或者出站数据的处理。既然是处理Channel中的数据,就需要关注很多的时间节点,比如Channel被激活,比如,读取到了数据,所以,ChannelHandler不仅需要关心数据何时来,还需要关注Channel处于一个什么样的状态,所以ChannelHandler的生命周期如下:
你可以使用 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter
类作为自己的 ChannelHandler 的起始点。使用的时候我们只需要扩展使用这些适配器类,然后重新我们需要的方法即可。
注意适配器类都有IsSharable属性,标识这个Hanlder能不能被添加到多个Pipeline中。
Excerpt
EventLoop介绍我们先回顾一下,EventLoop就是我们在最开始的示意图中的Selector,每个EventLoop和一个线程绑定,用于处理多个Channel。任务调度如果我们想实现延时任务的调度,比如连接成功5s之后发送一包数据,就可以用到EventLoop的计划任务ctx.Channel.EventLoop.Schedule(() =>{ Console.Wr…
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
我们先回顾一下,EventLoop就是我们在最开始的示意图中的Selector,每个EventLoop和一个线程绑定,用于处理多个Channel。
如果我们想实现延时任务的调度,比如连接成功5s之后发送一包数据,就可以用到EventLoop的计划任务
1 | ctx.Channel.EventLoop.Schedule(() => |
一个任务引发后,会判断当前是否在需要处理这个任务的EventLoop中(程序知道自己目前在执行哪个线程,线程又跟EventLoop对应),如果在就直接执行该任务,如果不在该任务中,则任务入队稍后处理
永远不要把一个需要耗费长时间的任务放到EventLoop执行队列来执行,需要使用我们前面介绍的EventExecutor的方法。
许多Channel对应一个EventLoop,但是EventLoop能分配给她的Channel个数是有限的,要处理可以扩展的无数个Channel就需要EventLoopGroup。他们的结构关系如下图:
我们之前讲过,Netty不仅能够完成NIO系统的搭建,也能通过一些简单的配置,变成OIO阻塞IO系统,阻塞IO的话,就不能多个Channel共享一个EventLoop了,就需要一个Channel分配一个EventLoop。总的来说,EventLoop跟线程的关系是不会改变的。
需要注意的是:
Excerpt
资源管理目的在处理数据的过程中,我们需要确保没有任何的资源泄漏。这时候我们就得很关心资源管理。引用计数的处理使用完ByteBuf之后,需要调整其引用计数以确保资源的释放内存内漏探测Netty提供了ResourceLeakDetector来检测内存泄漏,因为其是采样检测的,所以相关开销并不大。泄露日志检测级别手动释放消息ReferenceCountUtil.SafeRelea…
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
在处理数据的过程中,我们需要确保没有任何的资源泄漏。这时候我们就得很关心资源管理。
使用完ByteBuf之后,需要调整其引用计数以确保资源的释放
Netty提供了ResourceLeakDetector来检测内存泄漏,因为其是采样检测的,所以相关开销并不大。
泄露日志
检测级别
手动释放消息
1 | ReferenceCountUtil.SafeRelease(this.Message); |
分析SimpleChannelInboundHandler
1 | public override void ChannelRead(IChannelHandlerContext ctx, object msg) |
由上面的源码可以看出,Read0事实上是Read的封装,区别就是Read0方法在调用的时候,消息一定是被释放了,这就是手动释放的例子。
Excerpt
编码器和解码器定义编码器负责将应用程序可以识别的数据结构转化为可传输的数据流,解码器反之。对于应用程序来说,编码器操作出站数据,解码器操作入站数据。解码器和Handler解码器因为是处理入站数据的,所以继承了ChannelInBoundHandler.我们理解的时候可以认为解码器就是一种特殊的Handler,用于处理信息。解码器的类型ByteToMessageDecoderRepl…
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
编码器负责将应用程序可以识别的数据结构转化为可传输的数据流,解码器反之。对于应用程序来说,编码器操作出站数据,解码器操作入站数据。
解码器因为是处理入站数据的,所以继承了ChannelInBoundHandler.我们理解的时候可以认为解码器就是一种特殊的Handler,用于处理信息。
ByteToMessageDecoder
1 | // ByteToMessageDecoder |
ReplayingDecoder
1 | // 不需要判断ReadableBytes的ReplayingDecoder |
MessageToMessageDecoder
1 | public class IntToStringDecoder : MessageToMessageDecoder<int> |
根据我们之前的知识可以轻易的推导出,Encoder继承了ChannelOutBoundHandler
MessageToByteEncoder
1 | public class ShortToByteEncoder : MessageToByteEncoder<short> |
MessageToMessageEncoder
1 | public class IntToStringEncoder : MessageToMessageEncoder<int> |
MessageToMessageCodec,它拥有encode和decode两个方法,用于实现来回的转换数据,这种编解码器我们在后面实例的时候再举例说明。
这种编解码器可以把数据的转换,逆转换过程封装,但是同时他的缺点是,不如分开写重用方便。那我们就会想了,既然如此的话,为什么我们不能把一个编码器,一个解码器结合起来,作为一个编解码器呢?这样的话,编码器解码器分别可以重用,结合出来的编解码器也可以方便的使用 CombinedChannelDuplexHandler 就可以实现这样的作用。
1 | // 提供结合的编解码器 |