Netty介绍及其工作原理

资料总结来自MIC老师,仅供学习使用。

为什么选择Netty

Netty其实就是一个高性能NIO框架,所以它是基于NIO基础上的封装,本质上是提供高性能网络IO通信的功能。由于前面的课程中我们已经详细的对网络通信做了分析,因此在学习Netty时,学习起来应该是更轻松的。

Netty提供了上述三种Reactor模型的支持,我们可以通过Netty封装好的API来快速完成不同Reactor模型的开发,这也是为什么大家都选择Netty的原因之一,除此之外,Netty相比于NIO原生API,它有以下特点:

  • 提供了高效的I/O模型、线程模型和时间处理机制

  • 提供了非常简单易用的API,相比NIO来说,针对基础的Channel、Selector、Sockets、Buffffers等api提供了更高层次的封装,屏蔽了NIO的复杂性

  • 对数据协议和序列化提供了很好的支持

  • 稳定性,Netty修复了JDK NIO较多的问题,比如select空转导致的cpu消耗100%、TCP断线重连、keep-alive检测等问题。

  • 可扩展性在同类型的框架中都是做的非常好的,比如一个是可定制化的线程模型,用户可以在启动参数中选择Reactor模型、 可扩展的事件驱动模型,将业务和框架的关注点分离。

  • 性能层面的优化,作为网络通信框架,需要处理大量的网络请求,必然就面临网络对象需要创建和销毁的问题,这种对JVM的GC来说不是很友好,为了降低JVM垃圾回收的压力,引入了两种优化机制

  • 对象池复用,

  • 零拷贝技术

Netty**的生态介绍**

首先,我们需要去了解Netty到底提供了哪些功能,如图2-1所示,表示Netty生态中提供的功能说明。

后续内容中会逐步的分析这些功能。

Netty介绍及其工作原理_第1张图片

Netty**的基本使用**

需要说明一下,我们讲解的Netty版本是4.x版本,之前有一段时间netty发布了一个5.x版本,但是被官方舍弃了,原因是:使用**ForkJoinPool增加了复杂性,并且没有显示出明显的性能优势。**同时保持所有的分支同步是相当多的工作,没有必要。

添加**jar包依赖** :使用**4.1.66版本**

        
            io.netty
            netty-all
        

创建**Netty Server服务**

大部分场景中,我们使用的主从多线程Reactor模型,Boss线程是住Reactor,Worker是从Reactor。他们分别使用不同的NioEventLoopGroup

主Reactor负责处理Accept,然后把Channel注册到从Reactor,从Reactor主要负责Channel生命周期内的所有I/O事件。

public class NettyBasicServerExample {
​
    /**
     * 开发一个主从多reactor多线程模型的服务
     * @param args
     */
    public static void main(String[] args) {
        //主线程主
        EventLoopGroup bossGroup=new NioEventLoopGroup(1);
        //表示工作线程组(register)
        EventLoopGroup workGroup=new NioEventLoopGroup(4);
        //构建Netty Server的API
        ServerBootstrap bootstrap=new ServerBootstrap();
        //Bootstrap
        bootstrap.group(bossGroup,workGroup)
                //指定epoll模型
                .channel(NioServerSocketChannel.class)
                //具体的工作处理类,负责处理相关SocketChannel的IO就绪事件
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        //心跳的hander
                        //编解码
                        //协议处理
                        //消息处理
                        ch.pipeline()
                                .addLast("h1",new NormalMessageHandler())
                        .addLast("h2",new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                System.out.println("收到消息------------第二处理器");
                            }
                        }); //处理IO事件
                    }
                });
        try {
            ChannelFuture channelFuture=bootstrap.bind(8080).sync(); //同步阻塞等到客户端连接
            System.out.println("Netty Server Started Success:listener port:8080");
            channelFuture.channel().closeFuture().sync();//同步等到服务端监听端口关闭
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //释放资源
            workGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
​

上述代码说明如下:

  • EventLoopGroup,定义线程组,相当于我们之前在写NIO代码时定义的线程。这里定义了两个线程组分别是boss线程和worker线程,boss线程负责接收连接,worker线程负责处理IO事件。boss线程一般设置一个线程,设置多个也只会用到一个,而且多个目前没有应用场景。而worker线程通常要根据服务器调优,如果不写默认就是cpu的两倍。

  • ServerBootstrap,服务端要启动,需要创建ServerBootStrap,在这里面netty把nio的模板式的代码都给封装好了。

  • ChannelOption.SO_BACKLOG

设置**Channel类型**

NIO模型是Netty中最成熟也是被广泛引用的模型,因此在使用Netty的时候,我们会采用NioServerSocketChannel作为Channel类型。

bootstrap.channel(NioServerSocketChannel.class);

除了NioServerSocketChannel以外,还提供了

  • EpollServerSocketChannel,epoll模型只有在linux kernel 2.6以上才能支持,在windows和mac都是不支持的,如果设置Epoll在window环境下运行会报错。

  • OioServerSocketChannel,用于服务端阻塞地接收TCP连接

  • KQueueServerSocketChannel,kqueue模型,是Unix中比较高效的IO复用技术,常见的IO复用技术有select, poll, epoll以及kqueue等等。其中epoll为Linux独占,而kqueue则在许多UNIX系统上存在。

注册**ChannelHandler**

在Netty中可以通过ChannelPipeline注册多个ChannelHandler,该handler就是给到worker线程执行的处理器,当IO事件就绪时,会根据这里配置的Handler进行调用。

这里可以注册多个ChannelHandler,每个ChannelHandler各司其职,比如做编码和解码的handler,心跳机制的handler,消息处理的handler等。这样可以实现代码的最大化复用。

                 //具体的工作处理类,负责处理相关SocketChannel的IO就绪事件
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        //心跳的hander
                        //编解码
                        //协议处理
                        //消息处理
                        ch.pipeline()
                                .addLast("h1",new NormalMessageHandler())
                                .addLast("h2",new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                System.out.println("收到消息------------第二处理器");
                            }
                        }); //处理IO事件
                    }
                });

ServerBootstrap中的childHandler方法需要注册一个ChannelHandler,这里配置了一个ChannelInitializer的实现类,通过实例化ChannelInitializer来配置初始化Channel。

当收到IO事件后,这个数据会在这多个handler中进行传播。上述代码中配置了一个NormalMessageHandler,用来接收客户端消息并输出。

绑定端口

完成Netty的基本配置后,通过bind()方法真正触发启动,而sync()方法会阻塞,直到整个启动过程完成。

ChannelFuture channelFuture=bootstrap.bind(port).sync();

NormalMessageHandler

ServerHandler继承了ChannelInboundHandlerAdapter,这是netty中的一个事件处理器,netty中的处理器分为Inbound(进站)和Outbound(出站)处理器,后面会详细介绍。

public class NormalMessageHandler extends ChannelInboundHandlerAdapter {
​
    //channelReadComplete方法表示消息读完了的处理,writeAndFlush方法表示写入并发送消息
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //这里的逻辑就是所有的消息读取完毕了,在统一写回到客户端。
        // Unpooled.EMPTY_BUFFER表 示空消息,addListener(ChannelFutureListener.CLOSE)表示写完后,就关闭连接
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }
​
    //channelRead方法表示读到消息以后如何处理,这里我们把消息打印出来
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf) msg;
        byte[] req = new byte[in.readableBytes()];
        in.readBytes(req);//把数据读到byte数组中
        System.out.println("服务端收到的数据:" + new String(req, "utf-8"));
        //写回数据
        ByteBuf resp = Unpooled.copiedBuffer(("receive message:" + new String(req, "utf-8") + "").getBytes());
​
        ctx.write(resp);
        //ctx.write表示把消息再发送回客户端,但是仅仅是写到缓冲区,没有发送,flush才会真正写 到网络上去
        super.channelRead(ctx, msg);
    }
​
    //exceptionCaught方法就是发生异常的处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
    }
}

通过上述代码发现,我们只需要通过极少的代码就完成了NIO服务端的开发,相比传统的NIO原生类库的服务端,代码量大大减少,开发难度也大幅度降低

Netty**和NIO的api对应**

TransportChannel ----对应NIO中的channel

EventLoop---- 对应于NIO中的while循环

EventLoopGroup: 多个EventLoop,就是事件循环

ChannelHandler和ChannelPipeline---对应于NIO中的客户逻辑实现

handleRead/handleWrite(interceptor pattern)

ByteBuf---- 对应于NIO 中的ByteBuffffer

Bootstrap 和 ServerBootstrap ---对应NIO中的Selector、ServerSocketChannel等的创建、配置、启动等

Netty**的整体工作机制**

Netty的整体工作机制如下,整体设计就是前面我们讲过的多线程Reactor模型,分离请求监听和请求处理,通过多线程分别执行具体的handler。

Netty介绍及其工作原理_第2张图片 

网络通信层

网络通信层主要的职责是执行网络的IO操作,它支持多种网络通信协议和I/O模型的链接操作。当网络数据读取到内核缓冲区后,会触发读写事件,这些事件在分发给时间调度器来进行处理。

在Netty中,网络通信的核心组件以下三个组件

  • Bootstrap, 客户端启动api,用来链接远程netty server,只绑定一个EventLoopGroup

  • ServerBootStrap,服务端监听api,用来监听指定端口,会绑定两个EventLoopGroup,bootstrap组件可以非常方便快捷的启动Netty应用程序

  • Channel,Channel是网络通信的载体,Netty自己实现的Channel是以JDK NIO channel为基础,提供了更高层次的抽象,同时也屏蔽了底层Socket的复杂性,为Channel提供了更加强大的功能。

如图2-3所示,表示的是Channel的常用实现实现类关系图,AbstractChannel是整个Channel实现的基类,派生出了AbstractNioChannel(非阻塞io)、AbstractOioChannel(阻塞io),每个子类代表了不同的I/O模型和协议类型。Netty介绍及其工作原理_第3张图片

随着连接和数据的变化,Channel也会存在多种状态,比如连接建立、连接注册、连接读写、连接销毁。随着状态的变化,Channel也会处于不同的生命周期,每种状态会绑定一个相应的事件回调。以下是常见的时间回调方法。

  • channelRegistered, channel创建后被注册到EventLoop上

  • channelUnregistered,channel创建后未注册或者从EventLoop取消注册

  • channelActive,channel处于就绪状态,可以被读写

  • channelInactive,Channel处于非就绪状态

  • channelRead,Channel可以从源端读取数据

  • channelReadComplete,Channel读取数据完成

简单总结一下,Bootstrap和ServerBootStrap分别负责客户端和服务端的启动,Channel是网络通信的载体,它提供了与底层Socket交互的能力。

而当Channel生命周期中的事件变化,就需要触发进一步处理,这个处理是由Netty的事件调度器来完成。

事件调度器

事件调度器是通过Reactor线程模型对各类事件进行聚合处理,通过Selector主循环线程集成多种事件(I/O时间、信号时间),当这些事件被触发后,具体针对该事件的处理需要给到服务编排层中相关的Handler来处理。

事件调度器核心组件:

  • EventLoopGroup。相当于线程池

  • EventLoop。相当于线程池中的线程

EventLoopGroup本质上是一个线程池,主要负责接收I/O请求,并分配线程执行处理请求。为了更好的理解EventLoopGroup、EventLoop、Channel之间的关系,我们来看图2-4所示的流程。

Netty介绍及其工作原理_第4张图片

从图中可知

  • 一个EventLoopGroup可以包含多个EventLoop,EventLoop用来处理Channel生命周期内所有的I/O事件,比如accept、connect、read、write等

  • EventLoop同一时间会与一个线程绑定,每个EventLoop负责处理多个Channel

  • 每新建一个Channel,EventLoopGroup会选择一个EventLoop进行绑定,该Channel在生命周期内可以对EventLoop进行多次绑定和解绑。

图2-5表示的是EventLoopGroup的类关系图,可以看出Netty提供了EventLoopGroup的多种实现,如NioEventLoop、EpollEventLoop、NioEventLoopGroup等。

从图中可以看到,EventLoop是EventLoopGroup的子接口,我们可以把EventLoop等价于EventLoopGroup,前提是EventLoopGroup中只包含一个EventLoop。

Netty介绍及其工作原理_第5张图片

EventLoopGroup是Netty的核心处理引擎,它和前面我们讲解的Reactor线程模型有什么关系呢?其实,我们可以简单的把EventLoopGroup当成是Netty中Reactor线程模型的具体实现,我们可以通过配置不同的EventLoopGroup使得Netty支持多种不同的Reactor模型。

  • 单线程模型,EventLoopGroup只包含一个EventLoop,Boss和Worker使用同一个EventLoopGroup。

  • 多线程模型:EventLoopGroup包含多个EventLoop,Boss和Worker使用同一个EventLoopGroup。

  • 主从多线程模型:EventLoopGroup包含多个EventLoop,Boss是主Reactor,Worker是从Reactor模型。他们分别使用不同的EventLoopGroup,主Reactor负责新的网络连接Channel的创建(也就是连接的事件),主Reactor收到客户端的连接后,交给从Reactor来处理。

服务编排层

服务编排层的职责是负责组装各类的服务,简单来说,就是I/O事件触发后,需要有一个Handler来处理,所以服务编排层可以通过一个Handler处理链来实现网络事件的动态编排和有序的传播。

它包含三个组件

  • ChannelPipeline,它采用了双向链表将多个Channelhandler链接在一起,当I/O事件触发时,ChannelPipeline会依次调用组装好的多个ChannelHandler,实现对Channel的数据处理。

    ChannelPipeline是线程安全的,因为每个新的Channel都会绑定一个新的ChannelPipeline。一个ChannelPipeline关联一个EventLoop,而一个EventLoop只会绑定一个线程,如图2-6所示,表示ChannelPIpeline结构图。Netty介绍及其工作原理_第6张图片

     

从图中可以看出,ChannelPipeline中包含入站ChannelInBoundHandler和出站ChannelOutboundHandler,前者是接收数据,后者是写出数据,其实就是InputStream和OutputStream,为了更好的理解,我们来看图2-7。

  • ChannelHandler, 针对IO数据的处理器,数据接收后,通过指定的Handler进行处理。

  • ChannelHandlerContext,ChannelHandlerContext用来保存ChannelHandler的上下文信息,也就是说,当事件被触发后,多个handler之间的数据,是通过ChannelHandlerContext来进行传递的。ChannelHandler和ChannelHandlerContext之间的关系,如图2-8所示。Netty介绍及其工作原理_第7张图片

    每个ChannelHandler都对应一个自己的ChannelHandlerContext,它保留了ChannelHandler所需要的上下文信息,多个ChannelHandler之间的数据传递,是通过ChannelHandlerContext来实现的。Netty介绍及其工作原理_第8张图片

     

以上就是Netty中核心的组件的特性和工作机制的介绍,后续的内容中还会详细的分析这几个组件。可以看出,Netty的架构分层设计是非常合理的,它屏蔽了底层NIO以及框架层的实现细节,对于业务开发者来说,只需要关心业务逻辑的编排和实现即可。

组件关系及原理总结

如图2-9所示,表示Netty中关键的组件协调原理,具体的工作机制描述如下。

  • 服务单启动初始化Boss和Worker线程组,Boss线程组负责监听网络连接事件,当有新的连接建立时,Boss线程会把该连接Channel注册绑定到Worker线程

  • Worker线程组会分配一个EventLoop负责处理该Channel的读写事件,每个EventLoop相当于一个线程。通过Selector进行事件循环监听。

  • 当客户端发起I/O事件时,服务端的EventLoop讲就绪的Channel分发给Pipeline,进行数据的处理

  • 数据传输到ChannelPipeline后,从第一个ChannelInBoundHandler进行处理,按照pipeline链逐个进行传递

  • 服务端处理完成后要把数据写回到客户端,这个写回的数据会在ChannelOutboundHandler组成的链中传播,最后到达客户端。

Netty介绍及其工作原理_第9张图片

 

每天努力一点,每天都在进步

你可能感兴趣的:(网络IO)