DotNetty完全教程(二)

第一个DotNetty应用程序

准备工作

NuGet包介绍

DotNetty由九个项目构成,在NuGet中都是单独的包,可以按需引用,其中比较重要的几个是以下几个:

  • DotNetty.Common 是公共的类库项目,包装线程池,并行任务和常用帮助类的封装
  • DotNetty.Transport 是DotNetty核心的实现
  • DotNetty.Buffers 是对内存缓冲区管理的封装
  • DotNetty.Codes 是对编码器解码器的封装,包括一些基础基类的实现,我们在项目中自定义的协议,都要继承该项目的特定基类和实现
  • DotNetty.Handlers 封装了常用的管道处理器,比如Tls编解码,超时机制,心跳检查,日志等,如果项目中没有用到可以不引用,不过一般都会用到

开始一个项目

  1. 新建一个解决方案
  2. 新建一个项目
  3. 到NuGet中引用 DotNetty.Common DotNetty.Transport DotNetty.Buffers
  4. 开始编写实例代码

编写测试程序

回声测试应用程序编写 源码下载

  1. 新建一个解决方案 名字叫NettyTest
  2. 新建一个项目 名字叫EchoServer
  3. 到NuGet中引用 DotNetty.Common DotNetty.Transport DotNetty.Buffers
  4. 新建一个类 EchoServerHandler
    using DotNetty.Buffers;
    using DotNetty.Transport.Channels;
    using System;
    using System.Text;
    
    namespace EchoServer
    {
        /// 
        /// 因为服务器只需要响应传入的消息,所以只需要实现ChannelHandlerAdapter就可以了
        /// 
        public class EchoServerHandler : ChannelHandlerAdapter
        {
            /// 
            /// 每个传入消息都会调用
            /// 处理传入的消息需要复写这个方法
            /// 
            /// 
            /// 
            public override void ChannelRead(IChannelHandlerContext ctx, object msg)
            {
                IByteBuffer message = msg as IByteBuffer;
                Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
                ctx.WriteAsync(message);
            }
            /// 
            /// 批量读取中的最后一条消息已经读取完成
            /// 
            /// 
            public override void ChannelReadComplete(IChannelHandlerContext context)
            {
                context.Flush();
            }
            /// 
            /// 发生异常
            /// 
            /// 
            /// 
            public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)
            {
                Console.WriteLine(exception);
                context.CloseAsync();
            }
        }
    }
    
    上面的代码注释已经非常详细了,相信看注释你就能明白这个类大致干了些什么,但是突如其来的一个类还是有点难以理解,那么本着认真负责的精神我会再详细解释一下没有学过Netty的同学难以理解的点:
    1. 问:EchoServerHandler 是干什么用的?回答:Netty帮我们封装了底层的通信过程让我们不需要再关心套接字等网络底层的问题,更加专注于处理业务,何为业务?就是数据来了之后我要怎么办,Handler就是一个处理数据的工厂,那么上面的Handler中我们做了什么事情呢?稍加分析就能发现,我们在接到消息之后打印在了控制台上,之后将消息再发送回去。
    2. 问:WriteAsync 是在干什么?Flush 又是在干什么?答:由于是初学,不灌输太多,大家现在只需要知道数据写入之后并不会直接发出去,Flush的时候才会发出去。
  5. 在自动生成的Program.cs中写入服务器引导程序。
    using DotNetty.Transport.Bootstrapping;
    using DotNetty.Transport.Channels;
    using DotNetty.Transport.Channels.Sockets;
    using System;
    using System.Threading.Tasks;
    
    namespace EchoServer
    {
        public class Program
        {
            static async Task RunServerAsync()
            {
                IEventLoopGroup eventLoop;
                eventLoop = new MultithreadEventLoopGroup();
                try
                {
                    // 服务器引导程序
                    var bootstrap = new ServerBootstrap();
                    bootstrap.Group(eventLoop);
                    bootstrap.Channel();
                    bootstrap.ChildHandler(new ActionChannelInitializer(channel =>
                    {
                        IChannelPipeline pipeline = channel.Pipeline;
                        pipeline.AddLast(new EchoServerHandler());
                    }));
                    IChannel boundChannel = await bootstrap.BindAsync(3000);
                    Console.ReadLine();
                    await boundChannel.CloseAsync();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
                finally
                {
                    await eventLoop.ShutdownGracefullyAsync();
                }
            }
            static void Main(string[] args) => RunServerAsync().Wait();
        }
    }
    
    这个程序中同样有很多需要解释的,但是对于初学者来说,先明白这些概念就好了:
    1. bootstrap是启动引导的意思,Netty中的bootstrap的意思就是启动一个网络应用程序,那在启动之前我们肯定需要设置很多参数,bootstrap可以接收参数,引导用户启动Netty应用。
    2. EventLoopGroup 是一系列EventLoop的集合
    3. EventLoop 就对应了一个选择器(选择器看上一节的图)
    4. 一个Channel都需要绑定到一个选择器(EventLoop)上
    5. 每一个选择器(EventLoop)和一个线程绑定
    6. 我们可以把Handler串起来处理数据,这个我们后面再讲,这里的做法是把Handler串到pipeline上。
  6. 再新建一个项目取名叫EchoClient
  7. 新建一个类 EchoClientHandler
    using DotNetty.Buffers;
    using DotNetty.Transport.Channels;
    using System;
    using System.Text;
    
    namespace EchoClient
    {
        public class EchoClientHandler : SimpleChannelInboundHandler
        {
            /// 
            /// Read0是DotNetty特有的对于Read方法的封装
            /// 封装实现了:
            /// 1. 返回的message的泛型实现
            /// 2. 丢弃非该指定泛型的信息
            /// 
            /// 
            /// 
            protected override void ChannelRead0(IChannelHandlerContext ctx, IByteBuffer msg)
            {
                if (msg != null)
                {
                    Console.WriteLine("Receive From Server:" + msg.ToString(Encoding.UTF8));
                }
                ctx.WriteAsync(Unpooled.CopiedBuffer(msg));
            }
            public override void ChannelReadComplete(IChannelHandlerContext context)
            {
                context.Flush();
            }
            public override void ChannelActive(IChannelHandlerContext context)
            {
                Console.WriteLine("发送Hello World");
                context.WriteAndFlushAsync(Unpooled.CopiedBuffer(Encoding.UTF8.GetBytes("Hello World!")));
            }
    
            public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)
            {
                Console.WriteLine(exception);
                context.CloseAsync();
            }
        }
    }
    
    Handler的编写方法于上面服务器的Handler基本一致,这里我们还是需要解释一些问题:
    1. SimpleChannelInboundHandler 继承自 ChannelHandlerAdapter,前者更强大的地方是对于资源的自动释放(这是一个伏笔)
    2. Read0方法在代码的注释中已经解释过了,有兴趣的同学可以看一下源码。这里我就不贴出来了
    3. ctx.WriteAsync(Unpooled.CopiedBuffer(msg));如果这里直接将msg发送出去,大家就会发现,实验失败了,这是为什么呢?简单解释就是因为引用计数器机制,IByteBuffer只能使用一次,而在我们使用Read0方法接收这个消息的时候,这个消息的引用计数就被归零了,这时候我们再次使用就会报出异常,所以这里需要将源消息再复制一份。当然,如果你使用的Read方法则不会有这样的问题。原则上来说,我们不应该存储指向任何消息的引用供未来使用,因为这些引用都会自动失效(意思就是消息收到了处理完就丢掉,消息不应该被长久保存)。
  8. 编写客户端引导程序
    using DotNetty.Transport.Bootstrapping;
    using DotNetty.Transport.Channels;
    using DotNetty.Transport.Channels.Sockets;
    using System;
    using System.Net;
    using System.Threading.Tasks;
    
    namespace EchoClient
    {
        class Program
        {
            static async Task RunClientAsync()
            {
                var group = new MultithreadEventLoopGroup();
                try
                {
                    var bootstrap = new Bootstrap();
                    bootstrap
                        .Group(group)
                        .Channel()
                        .Handler(new ActionChannelInitializer(channel =>
                        {
                            IChannelPipeline pipeline = channel.Pipeline;
                            pipeline.AddLast(new EchoClientHandler());
                        }));
                    IChannel clientChannel = await bootstrap.ConnectAsync(new IPEndPoint(IPAddress.Parse("10.10.10.158"), 3000));
                    Console.ReadLine();
                    await clientChannel.CloseAsync();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
                finally
                {
                    await group.ShutdownGracefullyAsync();
                }
            }
            static void Main(string[] args) => RunClientAsync().Wait();
        }
    }
    

写在最后

项目的完整代码我放在了码云上,你可以点击这里可以下载。我相信很多完全没有接触过Netty的同学在跟着写完了第一个项目之后还是很懵,虽然解释了很多,但是还是感觉似懂非懂,这很正常。就如同我们写完HelloWorld之后,仍然会纠结一下static void Main(string[] args)为什么要这么写。我要说的是,只要坚持写完了第一个应用程序,你就是好样的,关于Netty我们还有很多很多要讲,相信你学了之后的知识以后,回过头来再看这个实例,会有恍然大悟的感觉。如果你坚持看完了文章并且敲了程序并且试验成功了,恭喜你,晚饭加个鸡腿,我们还有很多东西要学。

你可能感兴趣的:(DotNetty,C#)