Dubbo教程-03-netty框架

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_31922571/article/details/84891132

写在前面

hello 大家好
欢迎大家收看御风大世界
那么这次课呢使我们Dubbo系列教程的第三课
在本次课我将为大家介绍
dubbo的底层RPC通信框架 netty
并且我将为大家演示一个 netty的服务端 客户端通信程序

什么是netty?

1)本质:JBoss做的一个Jar包

2)目的:快速开发高性能、高可靠性的网络服务器和客户端程序

3)优点:提供异步的、事件驱动的网络应用程序框架和工具

通俗的说:一个好使的处理Socket的东东

如果没有Netty?

远古:java.net + java.io

近代:java.nio

其他:Mina,Grizzly

imagepng

netty为什么性能高?

高性能的三大要素

1) 传输:用什么样的通道将数据发送给对方,BIO、NIO或者AIO,IO模型在很大程度上决定了框架的性能。

2) 协议:采用什么样的通信协议,HTTP或者内部私有协议。协议的选择不同,性能模型也不同。相比于公有协议,内部私有协议的性能通常可以被设计的更优。

3) 线程:数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,Reactor线程模型的不同,对性能的影响也非常大。

异步非阻塞通信

在IO编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者IO多路复用技术进行处理。
IO多路复用技术通过把多个IO的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。
与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,
系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。

NIO的多路复用模型图

与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。
这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式正好相反。
开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞IO以降低编程复杂度。
但是对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。

零拷贝

零拷贝是Netty的重要特性之一,而究竟什么是零拷贝呢?

“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

从WIKI的定义中,我们看到“零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。
而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。

Non-Zero Copy方式:

Non-Zero Copy方式

Zero Copy方式:

从上图中可以清楚的看到,Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。
Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能,
而在Netty中也通过在FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝。

而在Netty中还有另一种形式的零拷贝,即Netty允许我们将多段数据合并为一整段虚拟数据供用户使用,
而过程中不需要对数据进行拷贝操作,这也是我们今天要讲的重点。我们都知道在stream-based transport(如TCP/IP)的传输过程中,
数据包有可能会被重新封装在不同的数据包中,例如当你发送如下数据时:

有可能实际收到的数据如下:

因此在实际应用中,很有可能一条完整的消息被分割为多个数据包进行网络传输,而单个的数据包对你而言是没有意义的,
只有当这些数据包组成一条完整的消息时你才能做出正确的处理,而Netty可以通过零拷贝的方式将这些数据包组合成一条完整的消息供你来使用。
而此时,零拷贝的作用范围仅在用户空间中。

内存池

为什么要使用内存池?

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。
但是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。
而且这些实例随着消息的处理朝生夕灭,这就会给服务器带来沉重的GC压力,同时消耗大量的内存。
为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。性能测试表明,采用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。

如何启动并初始化内存池?

在Netty4或Netty5中实现了一个新的ByteBuf内存池,它是一个纯Java版本的 jemalloc (Facebook也在用)。
现在,Netty不会再因为用零填充缓冲区而浪费内存带宽了。 不过,由于它不依赖于GC,开发人员需要小心内存泄漏。
如果忘记在处理程序中释放缓冲区,那么内存使用率会无限地增长。 Netty默认不使用内存池,需要在创建客户端或者服务端的时候在引导辅助类中进行配置:

work线程配置

如何在自己的业务代码中使用内存池?

首先,介绍一下Netty的ByteBuf缓冲区的种类:ByteBuf支持堆缓冲区和堆外直接缓冲区,根据经验来说,
底层IO处理线程的缓冲区使用堆外直接缓冲区,减少一次IO复制。业务消息的编解码使用堆缓冲区,分配效率更高,而且不涉及到内核缓冲区的复制问题。

ByteBuf的堆缓冲区又分为内存池缓冲区PooledByteBuf和普通内存缓冲区UnpooledHeapByteBuf。
PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。
UnpooledHeapByteBuf每次都会新建一个缓冲区对象。在高并发的情况下推荐使用PooledByteBuf,可以节约内存的分配。
在性能能够保证的情况下,可以使用UnpooledHeapByteBuf,实现比较简单。

在此说明这是当我们在业务代码中要使用池化的ByteBuf时的方法:

第一种情况:若我们的业务代码只是为了将数据写入ByteBuf中并发送出去,那么我们应该使用堆外直接缓冲区DirectBuffer.使用方式如下:

高效的Reactor线程模型

Reactor模式是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;
这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler

从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理;
而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会立刻的根据不同的Event类型将其分发给对应的Request Handler来处理。

这个做的好处有很多,首先我们可以将处理event的Request handler实现一个单独的线程,即:

Request handler线程

这样Service Handler 和request Handler实现了异步,加快了service Handler处理event的速度,
那么每一个request同样也可以以多线程的形式来处理自己的event,即Thread1 扩展成Thread pool 1,

Netty的Reactor线程模型1 Reactor单线程模型 Reactor机制中保证每次读写能非阻塞读写

Reactor单线程模型

一个线程(单线程)来处理CONNECT事件(Acceptor),一个线程池(多线程)来处理read,一个线程池(多线程)来处理write,那么从Reactor Thread到handler都是异步的,从而IO操作也多线程化。

到这里跟BIO对比已经提升了很大的性能,但是还可以继续提升,由于Reactor Thread依然为单线程,从性能上考虑依然有所限制

Reactor多线程模型

Reactor多线程模型

这样通过Reactor Thread Pool来提高event的分发能力

3 Reactor主从模型

Netty的高效并发编程主要体现在如下几点:

1) volatile的大量、正确使用;

2) CAS和原子类的广泛使用;

3) 线程安全容器的使用;

4) 通过读写锁提升并发性能。

Netty除了使用reactor来提升性能,当然还有

1、零拷贝,IO性能优化

2、通信上的粘包拆包

2、同步的设计

3、高性能的序列

dubbo为什么选择netty?

Dubbo通信层(利用Netty**)的实现过程**

dubbo 的 provider 和 consumer 的通信 交互
实际就是 RPC调用
而也就是 JAVA的代理
只不过 我们需要把数据通过网络相互传递
因此一个 网络 服务端线程模型 或者说 事件驱动的 设计模式
显得 十分对路子
我想这就是Dubbo为什么 选择用 netty的 原因吧

netty怎么用?

netty 的编程模型其实并不难
我们来演示一下

netty的hello world

首先我们需要导入的maven依赖

<dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>5.0.0.Alpha2</version>
</dependency>

接下来就是快乐的编码过程
首先给大家 说明一下 基于网络编程 离不开连个角色
server 服务端 client 客户端
我们的第一个 hello world 程序主要给大家演示
如何使用 netty写一个 server
并用我们的 CMD telnet 实现 client的一些操作

package cn.bywind;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * 丢弃任何进入的数据 启动服务端的DiscardServerHandler
 */
public class DiscardServer {
    private int port;

    public DiscardServer(int port) {
        super();
        this.port = port;
    }

    public void run() throws Exception {

        /***
         * NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,
         * Netty提供了许多不同的EventLoopGroup的实现用来处理不同传输协议。 在这个例子中我们实现了一个服务端的应用,
         * 因此会有2个NioEventLoopGroup会被使用。 第一个经常被叫做‘boss’,用来接收进来的连接。
         * 第二个经常被叫做‘worker’,用来处理已经被接收的连接, 一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。
         * 如何知道多少个线程已经被使用,如何映射到已经创建的Channels上都需要依赖于EventLoopGroup的实现,
         * 并且可以通过构造函数来配置他们的关系。
         */
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        System.out.println("准备运行端口:" + port);
        try {
            /**
             * ServerBootstrap 是一个启动NIO服务的辅助启动类 你可以在这个服务中直接使用Channel
             */
            ServerBootstrap b = new ServerBootstrap();
            /**
             * 这一步是必须的,如果没有设置group将会报java.lang.IllegalStateException: group not
             * set异常
             */
            b = b.group(bossGroup, workerGroup);
            /***
             * ServerSocketChannel以NIO的selector为基础进行实现的,用来接收新的连接
             * 这里告诉Channel如何获取新的连接.
             */
            b = b.channel(NioServerSocketChannel.class);
            /***
             * 这里的事件处理类经常会被用来处理一个最近的已经接收的Channel。 ChannelInitializer是一个特殊的处理类,
             * 他的目的是帮助使用者配置一个新的Channel。
             * 也许你想通过增加一些处理类比如NettyServerHandler来配置一个新的Channel
             * 或者其对应的ChannelPipeline来实现你的网络程序。 当你的程序变的复杂时,可能你会增加更多的处理类到pipline上,
             * 然后提取这些匿名类到最顶层的类上。
             */
            b = b.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new DiscardServerHandler());// demo1.discard
                    // ch.pipeline().addLast(new
                    // ResponseServerHandler());//demo2.echo
                    // ch.pipeline().addLast(new
                    // TimeServerHandler());//demo3.time
                }
            });
            /***
             * 你可以设置这里指定的通道实现的配置参数。 我们正在写一个TCP/IP的服务端,
             * 因此我们被允许设置socket的参数选项比如tcpNoDelay和keepAlive。
             * 请参考ChannelOption和详细的ChannelConfig实现的接口文档以此可以对ChannelOptions的有一个大概的认识。
             */
            b = b.option(ChannelOption.SO_BACKLOG, 128);
            /***
             * option()是提供给NioServerSocketChannel用来接收进来的连接。
             * childOption()是提供给由父管道ServerChannel接收到的连接,
             * 在这个例子中也是NioServerSocketChannel。
             */
            b = b.childOption(ChannelOption.SO_KEEPALIVE, true);
            /***
             * 绑定端口并启动去接收进来的连接
             */
            ChannelFuture f = b.bind(port).sync();
            /**
             * 这里会一直等待,直到socket被关闭
             */
            f.channel().closeFuture().sync();
        } finally {
            /***
             * 关闭
             */
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    //将规则跑起来
    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new DiscardServer(port).run();
        System.out.println("server:run()");
    }
}

接着我们需要自己写一个 handler

package cn.bywind;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;

/**
 * 服务端处理通道.这里只是打印一下请求的内容,并不对请求进行任何的响应 DiscardServerHandler 继承自
 * ChannelHandlerAdapter, 这个类实现了ChannelHandler接口, ChannelHandler提供了许多事件处理的接口方法,
 * 然后你可以覆盖这些方法。 现在仅仅只需要继承ChannelHandlerAdapter类而不是你自己去实现接口方法。
 *
 */
public class DiscardServerHandler extends ChannelHandlerAdapter {
    /**
     * 这里我们覆盖了chanelRead()事件处理方法。 每当从客户端收到新的数据时, 这个方法会在收到消息时被调用,
     * 这个例子中,收到的消息的类型是ByteBuf
     * 
     * @param ctx
     *            通道处理的上下文信息
     * @param msg
     *            接收的消息
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {

        try {
            ByteBuf in = (ByteBuf) msg;
            // 打印客户端输入,传输过来的的字符
            System.out.print(in.toString(CharsetUtil.UTF_8));
        } finally {
            /**
             * ByteBuf是一个引用计数对象,这个对象必须显示地调用release()方法来释放。
             * 请记住处理器的职责是释放所有传递到处理器的引用计数对象。
             */
            // 抛弃收到的数据
            ReferenceCountUtil.release(msg);
        }

    }

    /***
     * 这个方法会在发生异常时触发
     * 
     * @param ctx
     * @param cause
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        /**
         * exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,即当 Netty 由于 IO
         * 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来 并且把关联的 channel
         * 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不 同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
         */
        // 出现异常就关闭
        cause.printStackTrace();
        ctx.close();
    }

}

接下来 我们运行 server端 启动它的 main方法
我们打开 CMD telnet localhost 8080
然后我们后续的任何输入
都会被 server 接收到

imagepng

我们打开CMD
然后 输入

telnet 127.0.0.1 8080

然后就可以输入字符
而这个时候就会实时的在服务端 显示你输入的字符
比如我们 输入 java
你可以看到 server的控制台 就会 出现了

imagepng

总结

netty 作为高性能的 网络通信框架

拥有良好的设计模式

高效的读写性能

通过对netty的学习和实践

我们知道 dubbo的 底层实现就是基于netty

也更加清楚的明白 远程过程调用 RPC 基于 代理 + 网络通信

是如何实现的

本次课的演示代码我上传到了我的

github : https://github.com/ibywind/dubbo-learn

猜你喜欢

转载自blog.csdn.net/qq_31922571/article/details/84891132