某学姐

Android Female Developer,Technology Fan,Reader。

Java学习笔记——Netty

2018-05-08 | Comments

1、整体框架
(1)Server工作原理
(2)Client工作原理
2、基本流程
(1)Server绑定端口
(2)Client建立连接
(3)Client/Server读数据
(4)Client/Server写数据
3、重要元素
(1)NioSocketChannel/NioServerSocketChannel
(2)NioEventLoopGroup
(3)NioEventLoop
(4)Pipeline
(5)ChannelHandlerContext
(6)ByteBuf
4、总结
(1)链式结构
(2)缓存结构
5、参考文档

1、整体框架

先看下Client/Server调用的代码:

(1)Server工作原理

(2)Client工作原理

2、基本流程

整个流程无非就4步:
(1)Server绑定端口以及监听新连接
(2)Client发起建立连接
(3)连接成功后,C/S读数据
(4)连接成功后,C/S写数据

看过源码后就会发现,Netty最终走的是JAVA NIO那一套,只不过在NIO基础上还做了很多额外的工作,简化了NIO使用上的繁琐。

由于耗时任务都已经放到NioEventLoop线程里执行了,所以Bootstrap和ServerBootstrap是可以在主线程调用的。

(1)Server绑定端口

(2)Client建立连接

(3)Client/Server读数据

(4)Client/Server写数据

3、重要元素

(1)NioSocketChannel/NioServerSocketChannel

NioSocketChannel/NioServerSocketChannel可以理解成java中的Socket/ServerSocket。既可以输入又可以输出,可以是设置成阻塞和非阻塞。

一个NioSocketChannel对应一个连接,一个NioSocketChannel/NioServerSocketChannel对应一个Pipeline。

(2)NioEventLoopGroup

NioEventLoopGroup相当于是一个线程池,维护一个NioEventLoop[]数组。

NioEventLoopGroup可以设置线程个数,线程默认个数是NettyRuntime.availableProcessors() * 2。

对于Server来说,parent Group一般只有1个线程,child Group有NettyRuntime.availableProcessors() * 2个线程。
对于Client来说,一般也是一个线程。

每次有新Channel注册时,NioEventLoopGroup会按顺序选择一个NioEventLoop来处理这个Channel。对于服务端,NioServerSocketChannel是扔给parent Group 里唯一的线程处理的,而新接入的NioSocketChannel则是由child Group按顺序选择的NioEventLoop来处理。客户端类似。

(3)NioEventLoop

NioEventLoop相当于一个线程,一个NioEventLoop包含一个Selector。

NioEventLoop是用来执行任务的。主要包括3种类型的任务:定时任务、普通任务、IO任务。

在需要执行任务的时候,会启动NioEventLoop线程,NioEventLoop的run()实现如下:

// NioEventLoop
protected void run() {
    while(true) {
        switch(hasTasks() ? selector.selectNow() : -1) {
            case CONTINUE: //-2
                continue;
            case SELECT: //-1
                select(wakenUp.getAndSet(false));
                if(this.wakenUp.get()) {
                    selector.wakeup();
                }
            default:
                processSelectedKeys();
                runAllTasks();
                break;
        }
    }
}

1.select():
轮询是否有到点的定时任务普通任务IO任务可以执行。如果有任一种任务可执行就会跳出循环。如果任何任务都没有,则会调用selector.select(timeoutMillis)等待可执行的IO任务。

定时任务:PriorityQueue scheduledTaskQueue,按timeout时间排序
普通任务:LinkedBlockingQueue taskQueue
IO任务:SelectionKey

2.processSelectedKeys():
处理IO任务OP_CONNECT/OP_ACCEPT/OP_READ/OP_WRITE。

// NioEventLoop
public void processSelectedKeys() {
    for(int i = 0; i < selectedKeys.size; ++i) {
        SelectionKey k = selectedKeys.keys[i];
        AbstractNioChannel ch = k.attachment();
        NioUnsafe unsafe = ch.unsafe();
        
        //处理连接事件
        int readyOps = k.readyOps();
        if ((readyOps & OP_CONNECT) != 0) {
            unsafe.finishConnect();
        }
        
        //处理写事件
        if((readyOps & SelectionKey.OP_WRITE) != 0) {
            unsafe.forceFlush();
        }

        //处理读事件或处理NioServerSocketChannel的ACCEPT事件
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            unsafe.read();
        }
    }
}

3.runAllTasks():
处理到点的定时任务和普通任务。
对于到点的定时任务,会添加到普通任务队列,并执行普通任务队列的任务。

IO任务执行时间比例:ioRatio = 50
其他任务执行时间比例:100 - ioRatio
如果ioRatio设置为0的话,则是顺序执行,先执行完IO任务再执行完其他任务。

(4)Pipeline

Pipeline相当于一个管道,维护一个ChannelHandlerContext的双向链表,头结点是HeadContext,尾结点是TailContext。

HeadContext,既是ChannelInBoundHandler也是ChannelOutBoundHandler,是outBound类型。
TailContext,是ChannelInBoundHandler,是inBound类型。

当进行读操作,即数据从外部读到自己这边的Channel时,数据会先传给Pipeline,然后从HeadContext开始依次传给inBound类型的ChannelInBoundHandler,直到TailContext结束。
当进行写操作,即数据从自己这边的Channel往外部写时,数据会先传给Pipeline,然后从TailContext开始依次传给outBound类型的ChannelOutBoundHandler,直到HeadContext结束。

异常事件是从HeadContext往TailContext传递。

对于Client端:
除HeadContext和TailContext外,Pipeline中的handler还包括Bootstrap.handler()中的handler。

对于Server端:
除HeadContext和TailContext外,NioServerSocketChannel对应的Pipeline中的handler包括ServerBootstrap.handler()中的handler和ServerBootstrapAcceptor。 接入新连接后,NioSocketChannel对应的Pipeline中的handler包括ServerBootstrap.childHandler()中的childHandler

如果使用了ChannelInitializer这个handler的话,由于在它的initChannel()回调方法中又remove了它自己,所以最终对应的handler是initChannel()里的handler。因此ChannelInitializer仅仅起到初始化作用,不参与实际数据处理。

PS: 数据传递过程是一个链式传递,因为之前稍微看过OkHttp源码,有点类似。

(5)ChannelHandlerContext

unsafe:
NioServerSocketChannel对应NioMessageUnsafe NioSocketChannel对应NioByteUnsafe

HeadContext的工作:
1、通过unsafe处理bind/connect/read/write/flush/deregister/disconnect/close等具体事件
2、链式传递exceptionCaught/channelRegistered/channelUnregistered/channelActive/channelInactive/channelRead/channelReadComplete等事件。

TailContext的工作:
负责userEventTriggered/exceptionCaught/channelRead等一些异常捕获或未处理消息的日志提醒和资源释放工作。

用户自定义的ChannelHandlerContext可以选择是否将消息继续往下传递,默认是传递的,如果不传递将ctx.fireChannelXXX();这行代码覆盖掉即可。比如ServerBootstrapAcceptor在channelRead的时候就没有继续往下传。

传递的参数可以是Channel,比如在Server端监听到OP_ACCEPT事件时,ServerSocketChannel.accept()会得到一个NioSocketChannel,这个连接会放到list,然后经由ServerSocketChannel对应的Pipeline的channelRead递归传给ServerBootstrapAcceptor,NioSocketChannel进行一些初始化处理后再交由child Group进行下一步操作。

(6)ByteBuf

ByteBuf是数据缓存。

4、总结

(1)链式结构

OkHttp网络框架和这个类似,可能有相互借鉴。

(2)缓存结构

可以对比下okio、nio中的Buffer

5、参考文档

(1)https://www.jianshu.com/u/4fdc8c2315e8
(2)https://www.jianshu.com/p/03bb8a945b37
(3)https://blog.csdn.net/zxhoo/article/category/1800249

本文原文发自 某学姐, 转载请保留出处, 谢谢.

Comments