.NET Core微服务之路:利用DotNetty实现一个简单的通信过程

上一篇我们已经全面的介绍过《基于gRPC服务发现与服务治理的方案》,我们先复习一下RPC的调用过程(笔者会在这一节的几篇文章中反复的强调这个过程调用方案),看下图

640?wx_fmt=png

根据上面图,服务化原理可以分为3步:

  1. 服务端启动并且向注册中心发送服务信息,注册中心收到后会定时监控服务状态(常见心跳检测);

  2. 客户端需要开始调用服务的时候,首先去注册中心获取服务信息;

  3. 客户端创建远程调用连接,连接后服务端返回处理信息;

 

第3步又可以细分,下面说说远程过程调用的原理:

目标:客户端怎么调用远程机器上的公开方法

  1. 服务发现,向注册中心获取服务(这里需要做的有很多:拿到多个服务时需要做负载均衡,同机房过滤、版本过滤、服务路由过滤、统一网关等);

  2. 客户端发起调用,将需要调用的服务、方法、参数进行组装;

  3. 序列化编码组装的消息,这里可以使用json,也可以使用xml,也可以使用protobuf,也可以使用hessian,几种方案的序列化速度还有序列化后占用字节大小都是选择的重要指标,对内笔者建议使用高效的protobuf,它基于TCP/IP二进制进行序列化,体积小,速度快。

  4. 传输协议,可以使用传统的io阻塞传输,也可以使用高效的nio传输(Netty);

  5. 服务端收到后进行反序列化,然后进行相应的处理;

  6. 服务端序列化response信息并且返回;

  7. 客户端收到response信息并且反序列化;

 

  正如上面第三步的第4条所提到,C类向S类调用时,可以选择RPC或者RESTful,而作为内部通讯,笔者强烈建议使用RPC的方式去调用S类上的所有服务,RPC对比RESTful如下:

优点:

  1. 序列化采用二进制消息,性能好/效率高(空间和时间效率都很不错);

  2. 序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射(XML,JSON都是这种方式);

  3. 相比http协议,没有无用的header,简化传输数据的大小,且基于TCP层传输,速度更快,容量更小;

  4. Netty等一些框架集成(重点,也是本篇介绍的主要框架);

缺点:

  1. 使用复杂,维护成本和学习成本较高,调试困难;

  2. 因为基于HTTP2,绝大部多数HTTP Server、Nginx都尚不支持,即Nginx不能将GRPC请求作为HTTP请求来负载均衡,而是作为普通的TCP请求。(nginx1.9版本已支持);

  3. 二进制可读性差,或者几乎没有任何直接可读性,需要专门的工具进行反序列化;

  4. 默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持,后续会介绍利用Rosyln进行动态编译的特性);

 

通信传输利器Netty(Net is DotNetty)介绍

  (先埋怨一下微软大大)我们做NET开发,十分羡慕JAVA上能有NETTY, SPRING, STRUTS, DUBBO等等优秀框架,而我们NET就只有干瞪眼,哎,无赖之前生态圈没做好,恨铁不成钢啊。不过由于近来Net Core的发布,慢慢也拉回了一小部分属于微软的天下,打住,闲话扯到这儿。

  DotNetty是Azure团队仿照(几乎可以这么说)JAVA的Netty而出来的(目前已实现Netty的一部分),目前在Github上的Star有1.8K+,地址:https://github.com/Azure/DotNetty,没有任何文档,和代码中少量的注释。虽然比Netty出来晚了很多年,不过我们NET程序员们也该庆幸了,在自己的平台上终于能用上类似Netty这样强大的通信框架了。

传统通讯的问题:

  我们使用通用的应用程序或者类库来实现互相通讯,比如,我们经常使用一个 HTTP 客户端库来从 web 服务器上获取信息,或者通过 web 服务来执行一个远程的调用。

  然而,有时候一个通用的协议或他的实现并没有很好的满足需求。比如我们无法使用一个通用的 HTTP 服务器来处理大文件、电子邮件以及近实时消息,比如金融信息和多人游戏数据。我们需要一个高度优化的协议来处理一些特殊的场景。例如你可能想实现一个优化了的 Ajax 的聊天应用、媒体流传输或者是大文件传输器,你甚至可以自己设计和实现一个全新的协议来准确地实现你的需求。

  另一个不可避免的情况是当你不得不处理遗留的专有协议来确保与旧系统的互操作性。在这种情况下,重要的是我们如何才能快速实现协议而不牺牲应用的稳定性和性能。

解决:

  Netty 是一个提供 asynchronous event-driven (异步事件驱动)的网络应用框架,是一个用以快速开发高性能、可扩展协议的服务器和客户端。

  换句话说,Netty 是一个 NIO 客户端服务器框架,使用它可以快速简单地开发网络应用程序,比如服务器和客户端的协议。Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。

“快速和简单”并不意味着应用程序会有难维护和性能低的问题,Netty 是一个精心设计的框架,它从许多协议的实现中吸收了很多的经验比如 FTP、SMTP、HTTP、许多二进制和基于文本的传统协议.因此,Netty 已经成功地找到一个方式,在不失灵活性的前提下来实现开发的简易性,高性能,稳定性。

  有一些用户可能已经发现其他的一些网络框架也声称自己有同样的优势,所以你可能会问是 Netty 和它们的不同之处。答案就是 Netty 的哲学设计理念。Netty 从开始就为用户提供了用户体验最好的 API 以及实现设计。正是因为 Netty 的哲学设计理念,才让您得以轻松地阅读本指南并使用 Netty。

640?wx_fmt=jpeg

(DotNetty的框架和实现是怎么回事,笔者不太清楚,但完全可参考Netty官方的文档来学习和使用DotNetty相关的API接口)

 

DotNetty中几个重要的库(程序集):

DotNetty.Buffers: 对内存缓冲区管理的封装。

DotNetty.Codecs: 对编解码是封装,包括一些基础基类的实现,我们在项目中自定义的协议,都要继承该项目的特定基类和实现。

DotNetty.Codecs.Mqtt: MQTT(消息队列遥测传输)编解码是封装,包括一些基础基类的实现。

DotNetty.Codecs.Protobuf: Protobuf 编解码是封装,包括一些基础基类的实现。

DotNetty.Codecs.ProtocolBuffers: ProtocolBuffers编解码是封装,包括一些基础基类的实现。

DotNetty.Codecs.Redis: Redis 协议编解码是封装,包括一些基础基类的实现。

DotNetty.Common: 公共的类库项目,包装线程池,并行任务和常用帮助类的封装。

DotNetty.Handlers: 封装了常用的管道处理器,比如Tls编解码,超时机制,心跳检查,日志等。

DotNetty.Transport: DotNetty核心的实现,Socket基础框架,通信模式:异步非阻塞。

DotNetty.Transport.Libuv: DotNetty自己实现基于Libuv (高性能的,事件驱动的I/O库) 核心的实现。

常用的库有Codecs, Common, Handlers, Buffers, Transport,目前Azure团队正在实现其他Netty中的API(包括非公共Netty的API),让我们拭目以待吧。

 

直接上点对点之间通讯的栗子

  DotNetty的Example文件夹下有许多官方提供的实例,有抛弃服务实例(Discard),有应答服务实例(echo),有Telnet服务实例等等,为了实现直接点对点通讯,笔者采用了Echo的demo,此后的RPC调用也会基于Echo而实现,注释详细,直接上接收端(Server)的代码:

/*

* Netty 是一个半成品,作用是在需要基于自定义协议的基础上完成自己的通信封装

* Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。

* “快速和简单”并不意味着应用程序会有难维护和性能低的问题,

* Netty 是一个精心设计的框架,它从许多协议的实现中吸收了很多的经验比如 FTP、SMTP、HTTP、许多二进制和基于文本的传统协议。

* 因此,Netty 已经成功地找到一个方式,在不失灵活性的前提下来实现开发的简易性,高性能,稳定性。

*/


namespace Echo.Server

{

    using System;

    using System.Threading.Tasks;

    using DotNetty.Codecs;

    using DotNetty.Handlers.Logging;

    using DotNetty.Transport.Bootstrapping;

    using DotNetty.Transport.Channels;

    using DotNetty.Transport.Libuv;

    using Examples.Common;


    static class Program

    {

        static async Task RunServerAsync()

        {

            ExampleHelper.SetConsoleLogger();

            

            // 申明一个主回路调度组

            var dispatcher = new DispatcherEventLoopGroup();


            /*

             Netty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。

             在这个例子中我们实现了一个服务端的应用,因此会有2个 NioEventLoopGroup 会被使用。

             第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。

             如何知道多少个线程已经被使用,如何映射到已经创建的 Channel上都需要依赖于 IEventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。

             */


            // 主工作线程组,设置为1个线程

            IEventLoopGroup bossGroup = dispatcher; // (1)

            // 子工作线程组,设置为1个线程

            IEventLoopGroup workerGroup = new WorkerEventLoopGroup(dispatcher);


            try

            {

                // 声明一个服务端Bootstrap,每个Netty服务端程序,都由ServerBootstrap控制,通过链式的方式组装需要的参数

                var serverBootstrap = new ServerBootstrap(); // (2)

                // 设置主和工作线程组

                serverBootstrap.Group(bossGroup, workerGroup);


                if (ServerSettings.UseLibuv)

                {

                    // 申明服务端通信通道为TcpServerChannel

                    serverBootstrap.Channel<TcpServerChannel>(); // (3)

                }


                serverBootstrap

                    // 设置网络IO参数等

                    .Option(ChannelOption.SoBacklog, 100) // (5)


                    // 在主线程组上设置一个打印日志的处理器

                    .Handler(new LoggingHandler("SRV-LSTN"))


                    // 设置工作线程参数

                    .ChildHandler(

                        /*

                         * ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel。

                         * 也许你想通过增加一些处理类比如DiscardServerHandler 来配置一个新的 Channel 或者其对应的ChannelPipeline 来实现你的网络程序。

                         * 当你的程序变的复杂时,可能你会增加更多的处理类到 pipline 上,然后提取这些匿名类到最顶层的类上。

                         */

                        new ActionChannelInitializer<IChannel>( // (4)

                            channel =>

                            {

                                /*

                                 * 工作线程连接器是设置了一个管道,服务端主线程所有接收到的信息都会通过这个管道一层层往下传输,

                                 * 同时所有出栈的消息 也要这个管道的所有处理器进行一步步处理。

                                 */

                                IChannelPipeline pipeline = channel.Pipeline;


                                // 添加日志拦截器

                                pipeline.AddLast(new LoggingHandler("SRV-CONN"));


                                // 添加出栈消息,通过这个handler在消息顶部加上消息的长度。

                                // LengthFieldPrepender(2):使用2个字节来存储数据的长度。

                                pipeline.AddLast("framing-enc", new LengthFieldPrepender(2));


                                /*

                                  入栈消息通过该Handler,解析消息的包长信息,并将正确的消息体发送给下一个处理Handler

                                  1,InitialBytesToStrip = 0,       //读取时需要跳过的字节数

                                  2,LengthAdjustment = -5,         //包实际长度的纠正,如果包长包括包头和包体,则要减去Length之前的部分

                                  3,LengthFieldLength = 4,         //长度字段的字节数 整型为4个字节

                                  4,LengthFieldOffset = 1,         //长度属性的起始(偏移)位

                                  5,MaxFrameLength = int.MaxValue, //最大包长

                                 */

                                pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(ushort.MaxValue, 0, 2, 0, 2));


                                // 业务handler

                                pipeline.AddLast("echo", new EchoServerHandler());

                            }));


                // bootstrap绑定到指定端口的行为就是服务端启动服务,同样的Serverbootstrap可以bind到多个端口

                IChannel boundChannel = await serverBootstrap.BindAsync(ServerSettings.Port); // (6)


                Console.WriteLine("wait the client input");

                Console.ReadLine();


                // 关闭服务

                await boundChannel.CloseAsync();

            }

            finally

            {

                // 释放指定工作组线程

                await Task.WhenAll( // (7)

                    bossGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)),

                    workerGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1))

                );

            }

        }


        static void Main() => RunServerAsync().Wait();

    }

} 

  1. IEventLoopGroup 是用来处理I/O操作的多线程事件循环器,DotNetty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。在这个例子中我们实现了一个服务端的应用,因此会有2个 IEventLoopGroup 会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。

  2. ServerBootstrap 是一个启动 Transport 服务的辅助启动类。你可以在这个服务中直接使用 Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做。

  3. 这里我们指定使用 TcpServerChannel类来举例说明一个新的 Channel 如何接收进来的连接。

  4. ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel,当你的程序变的复杂时,可能你会增加更多的处理类到 pipline 上,然后提取这些匿名类到最顶层的类上。

  5. 你可以设置这里指定的 Channel 实现的配置参数。我们正在写一个TCP/IP 的服务端,因此我们被允许设置 socket 的参数选项比如tcpNoDelay 和 keepAlive。

  6. 绑定端口然后启动服务,这里我们在机器上绑定了机器网卡上的设置端口,当然现在你可以多次调用 bind() 方法(基于不同绑定地址)。

  7. 使用完成后,优雅的释放掉指定的工作组线程,当然,你可以选择关闭程序,但这并不推荐。

 

Server端的事件处理代码:

上一部分代码中加粗地方的实现

namespace Echo.Server

{

    using System;

    using System.Text;

    using DotNetty.Buffers;

    using DotNetty.Transport.Channels;


    /// <summary>

    /// 服务端处理事件函数

    /// </summary>

    public class EchoServerHandler : ChannelHandlerAdapter // ChannelHandlerAdapter 业务继承基类适配器 // (1)

    {

        /// <summary>

        /// 管道开始读

        /// </summary>

        /// <param name="context"></param>

        /// <param name="message"></param>

        public override void ChannelRead(IChannelHandlerContext context, object message) // (2)

        {

            if (message is IByteBuffer buffer)    // (3)

            {

                Console.WriteLine("Received from client: " + buffer.ToString(Encoding.UTF8));

            }


            context.WriteAsync(message); // (4)

        }


        /// <summary>

        /// 管道读取完成

        /// </summary>

        /// <param name="context"></param>

        public override void ChannelReadComplete(IChannelHandlerContext context) => context.Flush(); // (5)


        /// <summary>

        /// 出现异常

        /// </summary>

        /// <param name="context"></param>

        /// <param name="exception"></param>

        public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)

        {

            Console.WriteLine("Exception: " + exception);

            context.CloseAsync();

        }

    }

}

  1. DiscardServerHandler 继承自 ChannelInboundHandlerAdapter,这个类实现了IChannelHandler接口,IChannelHandler提供了许多事件处理的接口方法,然后你可以覆盖这些方法。现在仅仅只需要继承 ChannelInboundHandlerAdapter 类而不是你自己去实现接口方法。

  2. 这里我们覆盖了 chanelRead() 事件处理方法。每当从客户端收到新的数据时,这个方法会在收到消息时被调用,这个例子中,收到的消息的类型是 ByteBuf。

  3. 为了响应或显示客户端发来的信息,为此,我们将在控制台中打印出客户端传来的数据。

  4. 然后,我们将客户端传来的消息通过context.WriteAsync写回到客户端。

  5. 当然,步骤4只是将流缓存到上下文中,并没执行真正的写入操作,通过执行Flush将流数据写入管道,并通过context传回给传来的客户端。

 

 Client端代码:

 重点看注释的地方,其他地方跟Server端没有任何区别

namespace Echo.Client

{

    using System;

    using System.Net;

    using System.Text;

    using System.Threading.Tasks;

    using DotNetty.Buffers;

    using DotNetty.Codecs;

    using DotNetty.Handlers.Logging;

    using DotNetty.Transport.Bootstrapping;

    using DotNetty.Transport.Channels;

    using DotNetty.Transport.Channels.Sockets;

    using Examples.Common;


    static class Program

    {

        static async Task RunClientAsync()

        {

            ExampleHelper.SetConsoleLogger();

            

            var group = new MultithreadEventLoopGroup();


            try

            {

                var bootstrap = new Bootstrap();

                bootstrap

                    .Group(group)

                    .Channel<TcpSocketChannel>()

                    .Option(ChannelOption.TcpNodelay, true)

                    .Handler(

                        new ActionChannelInitializer<ISocketChannel>(

                            channel =>

                            {

                                IChannelPipeline pipeline = channel.Pipeline;

                                pipeline.AddLast(new LoggingHandler());

                                pipeline.AddLast("framing-enc", new LengthFieldPrepender(2));

                                pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(ushort.MaxValue, 0, 2, 0, 2));


                                pipeline.AddLast("echo", new EchoClientHandler());

                            }));


                IChannel clientChannel = await bootstrap.ConnectAsync(new IPEndPoint(ClientSettings.Host, ClientSettings.Port));

                

                // 建立死循环,类同于While(true)

                for (;;) // (4)

                {

                    Console.WriteLine("input you data:");

                    // 根据设置建立缓存区大小

                    IByteBuffer initialMessage = Unpooled.Buffer(ClientSettings.Size); // (1)

                    string r = Console.ReadLine();

                    // 将数据流写入缓冲区

                    initialMessage.WriteBytes(Encoding.UTF8.GetBytes(r ?? throw new InvalidOperationException())); // (2)

                    // 将缓冲区数据流写入到管道中

                    await clientChannel.WriteAndFlushAsync(initialMessage); // (3)

                    if(r.Contains("bye"))

                        break;

                }


                Console.WriteLine("byebye");

                


                await clientChannel.CloseAsync();

            }

            finally

            {

                await group.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1));

            }

        }


        static void Main() => RunClientAsync().Wait();

    }

}

  1. 初始化一个缓冲区的大小。

  2. 默认缓冲区接受的数据类型为bytes[],当然这样也更加便于序列化成流。

  3. 将缓冲区的流直接数据写入到Channel管道中。该管道一般为链接通讯的另一端(C端)。

  4. 建立死循环,这样做的目的是为了测试每次都必须从客户端输入的数据,通过服务端回路一次后,再进行下一次的输入操作。

 

Client端的事件处理代码:

namespace Echo.Client

{

    using System;

    using System.Text;

    using DotNetty.Buffers;

    using DotNetty.Transport.Channels;


    public class EchoClientHandler : ChannelHandlerAdapter

    {

        readonly IByteBuffer initialMessage;


        public override void ChannelActive(IChannelHandlerContext context) => context.WriteAndFlushAsync(this.initialMessage);


        public override void ChannelRead(IChannelHandlerContext context, object message)

        {

            if (message is IByteBuffer byteBuffer)

            {

                Console.WriteLine("Received from server: " + byteBuffer.ToString(Encoding.UTF8));

            }

        }


        public override void ChannelReadComplete(IChannelHandlerContext context) => context.Flush();


        public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)

        {

            Console.WriteLine("Exception: " + exception);

            context.CloseAsync();

        }

    }

}

非常简单,将数据流显示到控制台。

 

实现结果

  至此,我们使用DotNetty框架搭建简单的应答服务器就这样做好了,很简单,实现效果如下:

  C端主动向S端主动发送数据后,S端收到数据,在控制台打印出数据,并回传给C端,当然,S端还可以做很多很多的事情。

 640?wx_fmt=png

 

DotNetty内部调试记录分析

  虽然DotNetty官方没有提供任何技术文档,但官方却提供了详细的调试记录,很多时候,我们学习者其实也可以通过调试记录来分析某一个功能的实现流程。我们可以通过将DotNetty的内部输入输出记录打印到控制台上。

InternalLoggerFactory.DefaultFactory.AddProvider(new ConsoleLoggerProvider((s, level) => true, false));

  可以看到服务端的打印记录一下多出来了许多许多,有大部分是属于DotNetty内部调试时的打印记录,我们只着重看如下的部分。

dbug: SRV-LSTN[0]

      [id: 0x3e8afca1] HANDLER_ADDED

dbug: SRV-LSTN[0]

      [id: 0x3e8afca1] REGISTERED (1)

dbug: SRV-LSTN[0]

      [id: 0x3e8afca1] BIND: 0.0.0.0:8007 (2)

wait the client input

dbug: SRV-LSTN[0]

      [id: 0x3e8afca1, 0.0.0.0:8007] ACTIVE (3)

dbug: SRV-LSTN[0]

      [id: 0x3e8afca1, 0.0.0.0:8007] READ (4)

dbug: SRV-LSTN[0]

      [id: 0x3e8afca1, 0.0.0.0:8007] RECEIVED: [id: 0x7bac2775, 127.0.0.1:64073 :> 127.0.0.1:8007] (5)

dbug: SRV-LSTN[0]

      [id: 0x3e8afca1, 0.0.0.0:8007] RECEIVED_COMPLETE (6)

dbug: SRV-LSTN[0]

      [id: 0x3e8afca1, 0.0.0.0:8007] READ (7)

dbug: SRV-CONN[0]

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] HANDLER_ADDED (8)

dbug: SRV-CONN[0]

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] REGISTERED (9)

dbug: SRV-CONN[0]

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] ACTIVE (10)

dbug: SRV-CONN[0]

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] READ (11)

dbug: DotNetty.Buffers.AbstractByteBuffer[0]    (12)

      -Dio.netty.buffer.bytebuf.checkAccessible: True

dbug: SRV-CONN[0]

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] RECEIVED: 14B (13)

               +-------------------------------------------------+

               |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |

      +--------+-------------------------------------------------+----------------+

      |100000000| 00 0C 68 65 6C 6C 6F 20 77 6F 72 6C 64 21      |..hello world!  |

      +--------+-------------------------------------------------+----------------+

Received from client: hello world!

dbug: SRV-CONN[0]    (14)

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] WRITE: 2B

               +-------------------------------------------------+

               |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |

      +--------+-------------------------------------------------+----------------+

      |100000000| 00 0C                                          |..              |

      +--------+-------------------------------------------------+----------------+

dbug: SRV-CONN[0] (15)

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] WRITE: 12B

               +-------------------------------------------------+

               |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |

      +--------+-------------------------------------------------+----------------+

      |100000000| 68 65 6C 6C 6F 20 77 6F 72 6C 64 21            |hello world!    |

      +--------+-------------------------------------------------+----------------+

dbug: SRV-CONN[0] (16)

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] RECEIVED_COMPLETE

dbug: SRV-CONN[0] (17)

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] FLUSH

dbug: SRV-CONN[0] (18)

      [id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] READ

咋一看,有18个操作,好像有点太多了,其实不然,还有很多很多的内部调试细节并没打印到控制台上。

  1. 通过手动建立的工作线程组,并将这组线程注册到管道中,这个管道可以是基于SOCKER,可以基于IChannel(1);

  2. 绑定自定的IP地址和端口号到自定义管道上(2);

  3. 激活自定义管道(3);

  4. 开始读取(其实也是开始监听)(4);

  5. 收到来自id为0x7bac2775的客户端连接请求,建立连接,并继续开始监听(5)(6)(7);

  6. 从第8步开始,日志已经变成id为0x7bac2775的记录了,当然一样包含注册管道,激活管道,开始监听等等与S端一模一样的操作(8)(9)(10)(11)

  7. 当笔者输入一条"hello world!"数据后,DotNetty.Buffers.AbstractByteBuffer会进行数据类型检查,以便确认能将数据放入到管道中。(12)

  8. 将数据发送到S端,数据大小为14B,hello world前有两个点,代表这是数据头,紧接着再发送两个点,但没有任何数据,代表数据已经结束。DotNetty将数据的十六进制存储位用易懂的方式表现了出来,很人性化。(13)(14)

  9. S端收到数据没有任何加工和处理,马上将数据回传到C端。(15)(16)

  10. 最后,当这个过程完成后,需要将缓存区的数据强制写入到管道中,所以会执行一次Flush操作,整个传输完成。接下来,不管是C端还是S端,继续将自己的状态改成READ,用于监听管道中的各种情况,比如连接状态,数据传输等等(17)。

 

总结

  对于刚开始接触Socket编程的朋友而言,这是个噩梦,因为Socket编程的复杂性不会比多线程容易,甚至会更复杂。协议,压缩,传输,多线程,监听,流控制等等一系列问题摆在面前,因此而诞生了Netty这样优秀的开源框架,但是Netty是个半成品,因为你需要基于他来实现自己想要的协议,传输等等自定义操作,而底层的内容,你完全不用关心。不像某些框架,比如Newtonsoft.Json这样的功能性框架,不用配置,不用自定义,直接拿来用就可以了。

  虽然DotNetty帮我们实现了底层大量的操作,但如果不熟悉或者一点也不懂网络通信,同样对上面的代码是一头雾水,为何?行情需要,我们程序员天天都在赶业务,哪有时间去了解和学习更多的细节...通过将调试记录打印出来,并逐行挨个的对照代码进行分析,就会慢慢开始理解最简单的通信流程了。

  本篇只是实现了基于DotNetty最简单的通讯过程,也只是将数据做了一下回路,并没做到任何与RPC有关的调用,下一篇我们开始讲这个例子深入,介绍基于DotNetty的RPC调用。

原文地址: https://www.cnblogs.com/SteveLee/p/9860507.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/319259.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[译]ASP.NET Core中使用MediatR实现命令和中介者模式

在本文中&#xff0c;我将解释命令模式&#xff0c;以及如何利用基于命令模式的第三方库来实现它们&#xff0c;以及如何在ASP.NET Core中使用它来解决我们的问题并使代码简洁。因此&#xff0c;我们将通过下面的主题来进行相关的讲解。什么是命令模式?命令模式的简单实例以及…

.NET in Browser - Blazor

什么是BlazorBlazor 是一个实验性的. NET web 框架, 使用 C# 和 HTML 在任何浏览器中不需要插件即可运行 WebAssembly 程序集。什么是WebAssemblyWebAssembly是一种新的适合于编译到Web的&#xff0c;可移植的&#xff0c;大小和加载时间高效的格式&#xff0c;是一种新的字节码…

在碰撞中成长 - 北京银行的DevOps实践之路

2018年10/27日&#xff0c;在上海召开的微软年度最大规模的技术盛会—微软2018技术暨生态大会上&#xff0c;北京银行渠道系统负责人&敏捷团队负责人周兵女士和大家一起分享了北京银行的DevOps 实践转型经验&#xff0c;得到了大会听众的热烈评价和共鸣&#xff0c;会后众多…

2020牛客国庆集训派对day4 Emergency Evacuation

Emergency Evacuation 题意&#xff1a; 有n个人在不同的位置上&#xff0c;在最后面有一个出口exit&#xff0c;所有人都要逃离出去&#xff08;走出出口&#xff09;&#xff0c;且每个格子最多容纳一个人&#xff0c;当有人挡在前面时&#xff0c;后面的人必须停留&#x…

【活动(广州)】MonkeyFest2018 微软最有价值专家讲座

MonkeyFest2018微软最有价值专家讲座Monkey Fest 是一个一年一度由全球Microsoft Xamarin跨平台开发者发起的全球性社区活动&#xff0c;主要是推广在云、人工智能、大数据、移动开发等技术。本次活动同时在新加坡&#xff0c;美国&#xff0c;日本&#xff0c;加拿大&#xff…

互联网公司为什么普遍996而不是666

根据skinshoe wu的遭遇&#xff0c;解释一下互联网行业的12小时工作制以及996。题目说的有点绝对&#xff0c;这里先澄清一下&#xff1a;有的公司是10106&#xff0c;9106&#xff0c;10126&#xff0c;995&#xff0c;甚至955&#xff0c;007的都有&#xff0c;我只说大多数&…

IdentityServer4之JWT签名(RSA加密证书)及验签

一、前言在IdentityServer4中有两种令牌&#xff0c;一个是JWT和Reference Token&#xff0c;在IDS4中默认用的是JWT&#xff0c;那么这两者有什么区别呢&#xff1f;二、JWT与Reference Token的区别1、JWT(不可撤回)  JWT是一个非常轻巧的规范&#xff0c;一般被用来在身份提…

.NET Core使用IO合并技巧轻松实现千万级消息推送

之前讲述过多路复用实现单服百万级别RPS吞吐,但在文中有一点是没有说的就是消息IO合并&#xff0c;如果缺少了消息IO合并即使怎样多路复用也很难达到百万级别的请求响毕竟所有应用层面的网络IO读写都是非常损耗性能的&#xff08;需要硬件配置很高的服务器&#xff09;。这一章…

天气情况图像分类练习赛 第三阶段(赛中感)

第三阶段也是实战阶段&#xff0c;不同于前两个阶段的填空而是实打实的预测分析 题目会给出8000张照片数据&#xff0c;其中6000作为训练集而另外2000张作位测试集&#xff0c;通过对6000张的训练来预测2000的结果&#xff0c;并将结果输出到csv文件中&#xff0c;提交检验成功…

一码阻塞,万码等待:ASP.NET Core 同步方法调用异步方法“死锁”的真相

在我们 2015 年开始的从 .NET Framework 向 .NET Core 迁移的工程中&#xff0c;遇到的最大的坑就是标题中所说的——同步方法中调用异步方法发生”死锁”。虽然在 .NET Framework 时代就知道不能在同步方法中调用异步方法&#xff0c;但我们却明知路有坑&#xff0c;偏向此路行…

take

take 题解参考 题目描述 Kanade has n boxes , the i-th box has p[i] probability to have an diamond of d[i] size. At the beginning , Kanade has a diamond of 0 size. She will open the boxes from 1-st to n-th. When she open a box,if there is a diamond in it an…

将传统 ASP.NET 应用迁移到 .NET Core

点击蓝字关注我现在越来越多的人在谈论. NET Core。诚然&#xff0c;.NET Core 是未来, 但是.NET Framework 仍在支持, 因为大量的应用程序无法在短时间内迁移。.NET Core 和 .NET Framework 就像电动汽车和汽油动力汽车。汽油车是成熟的&#xff0c;你可以毫无任何问题驾驶它&…

[翻译] 初看 ASP.NET Core 3.0 即将到来的变化

原文: A first look at changes coming in ASP.NET Core 3.0在我们努力完成下一个 minor 版本的 ASP.NET Core 的同时&#xff0c;我们也在对下一个 major 版本进行更新&#xff0c;其中包括如何使用框架组合项目、更紧密的 .NET Core 集成以及第三方开源集成&#xff0c;所有这…

aspnet core 2.1中使用jwt从原理到精通二

在aspnet core中&#xff0c;自定义jwt管道验证有了上一节的内容作为基础&#xff0c;那这点也是非常容易的&#xff0c;关键点在中间件&#xff0c;只是把上一级在测试类中的自定义验证放到中间件中来即可&#xff0c;不过需要注意&#xff1a;中间件 的位置很重要&#xff0c…

CentOS开发ASP.NET Core入门教程

因为之前一直没怎么玩过CentOS&#xff0c;大多数时间都是使用Win10进行开发&#xff0c;然后程序都部署在Window Server2008或者Window Server2012上&#xff01;因此想尝试下Linux系统。最后经过选型选了比较流行的CentOS系统。正好&#xff0c;今晚要加班&#xff0c;所以在…

工厂参观记:.NET Core 中 HttpClientFactory 如何解决 HttpClient 臭名昭著的问题

在 .NET Framework 与 .NET Core 中 HttpClient 有个臭名昭著的问题&#xff0c;HttpClient 实现了 IDispose 接口&#xff0c;但当你 Dispose 它时&#xff0c;它不会立即关闭所使用的 tcp 连接&#xff0c;而是将 tcp 连接置为 TIME_WAIT 状态&#xff0c;240秒&#xff08;4…

ASP.NET Core2读写InfluxDB时序数据库

在我们很多应用中会遇到有一种基于一系列时间的数据需要处理&#xff0c;通过时间的顺序可以将这些数据点连成线&#xff0c;再通过数据统计后可以做成多纬度的报表&#xff0c;也可通过机器学习来实现数据的预测告警。而时序数据库就是用于存放管理这种有着时间顺序数据的&…

Nature 新研究发布,GPT 驱动的机器人化学家能够自行设计和进行实验,这对科研意味着什么?

文章目录 前言揭秘Coscientist不到四分钟&#xff0c;设计并改进了程序能力越大&#xff0c;责任越大 前言 有消息称&#xff0c;AI 大模型 “化学家” 登 Nature 能够自制阿司匹林、对乙酰氨基酚、布洛芬&#xff0c;甚至连复杂的钯催化交叉偶联反应&#xff0c;也能完成。 …

SmartCode 常见问题

SmartCode 能干什么&#xff1f;SmartCode IDataSource -> IBuildTask -> IOutput > Build EverythingSmartCode的执行流是 数据源->构建任务->输出&#xff0c;也就是说应用场景非常广泛。从DB读取数据结构&#xff0c;最终生成整个解决方案代码生成器&#x…

全面支持开源,微软加速 Visual Studio 和 Azure DevOps 云升级

在 2018 微软技术暨生态大会&#xff08;Microsoft Tech Summit&#xff09;上&#xff0c;微软宣布围绕 Visual Studio 和 Visual Studio Code 开发平台提供一系列新功能与服务&#xff0c;并对 Azure DevOps 研发云进行整合升级&#xff0c;通过 Visual Studio 开发平台与微软…