Netty入门及源码分析

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.

以上是官网对Netty的介绍,Netty是基础Java Nio封装的网络编程框架,基于TCP或UDP四层协议进行流式网络编程。所以下面先从网络以及NIO进行介绍。

基础篇

网络协议

按OSI网络协议分层为七层,下一层都是对上一层数据进行封装,TCP/IP是在四层,通过IP进行数据路由,在四层通过IP+端口的方式确定一个应用,当网络数据到达时,找到对应的端口后将数据传输给应用进行处理。

基于netty的iot 开源项目 netty源码剖析与应用_基于netty的iot 开源项目

NIO是基于socket连接的,Netty可以在四层协议基础上实现上层应用层的自定义报文协议,目前基于Netty的应用很多,如Dubbo,protobuf等,更多的可以参考https://netty.io/wiki/related-projects.html

Java网络编辑发展

BIO

Java从1.0开始就提供了BIO,BIO即Blocking IO,是一种阻塞的基于流处理的IO。下面是代码示例:

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10000);
        while (true) {
            Socket accept = serverSocket.accept();
            int available = accept.getInputStream().available();
            byte[] buffer = new byte[1024];
            int read = accept.getInputStream().read(buffer);
            System.out.println(read);
            System.out.println(new String(buffer,0,read, "utf-8"));
            accept.getOutputStream().write("receive".getBytes());
            accept.getOutputStream().flush();
        }
    }
}

//client
public class Client {
    public static void main(String[] args) throws IOException, InterruptedException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("localhost", 10000));
        socket.getOutputStream().write("hello bio".getBytes());
        socket.getOutputStream().flush();
        Thread.sleep(1000);
    }
}

BIO读写阻塞可使用多线程进行优化,但整体效率不高,无法支持大量的客户端连接。

NIO

由于Netty是基于NIO的封装,所以了解NIO对掌握Netty非常关键。

Java从1.4开始提供了NIO网络编程,NIO(Non Blocking IO)是同步非阻塞IO,在java.nio包下,主要是Buffer,channel,selector几个类。

Channel

基于netty的iot 开源项目 netty源码剖析与应用_ci_02

NIO将网络抽象为通道channel,Channel分为服务端和客户端,服务端channel处理客户端的连接,都支持阻塞或非阻塞的。上图为类关系图。

Selector

selector是处理多通道的单线程选择器,通道通过向selector注册感兴趣的事件,当有事件发生时,selector会返回事件SelectionKey,SelectionKey里面包含有通道等相关的信息,可以理解为一个本次事件相关的上下文context,可以本次事件的Channel进行相关的读写操作。

SelectionKey有四个感兴趣的事件:

  • OP_ACCEPT
  • OP_CONNECT
  • OP_READ
  • OP_WRITE

基于netty的iot 开源项目 netty源码剖析与应用_Netty_03

Buffer

Buffer是进行网络数据读写的缓冲区,NIO有基于Buffer的各种类型实现。

基于netty的iot 开源项目 netty源码剖析与应用_ci_04

一般程序中使用ByteBuffer对网络数据进行读写缓存,由于网络传输采用大端,所以ByteBuffer默认采用大端数据传输方式。

大端:(Big-Endian):就是把数值的高位字节放在内存的低位地址上,把数值的低位字节放在内存的高位地址上。

小端:(Little-Endian):就是把数值的高位字节放在高位的地址上,低位字节放在低位地址上。

ByteBuffer类有几个关键的位置指针,分别是mark,position,limit和capacity。参考下图

基于netty的iot 开源项目 netty源码剖析与应用_NIO_05

  • position当前指针位置,
  • limit当前操作限制大小,
  • capacity当前ByteBuffer大小
  • mark是对当前指针的一个位置标记

ByteBuffer中除一些基本方法之外还有几个有用的方法,理解这此方法对于几个指针的操作更全面。

  • clear()方法将ByteBuff置于初始化状态,即position为0,limit=capacity,mark=-1
  • rewind()将ByteBuff的起始位置置0,即position=0,mark=-1,一般是在需要从0的位置开始写入时
  • flip()先限制读长度(limit=position)然后再执行同rewind()操作,用于将写转为读状态,限制读取长度
  • mark()标识当前position位置,即mark=position
  • reset()和mark()需要成对出现,reset是重置为上一个remark状态,即position=mark。reset时会判断mark状态,如果前面从未进行过mark操作会抛出错误

Nio每种类型有基于堆内存和直接内存的实现,以ByteBuffer为例,可以通过allocate和allocateDirect方法生成基于堆或直接内存的buffer。

直接内存后台使用的是byte数组,直接内存使用unsafe分配堆外内存并存储分配的地址,并初始化一个Cleaner用于回收。具体代码如下:

//堆外内存分配
public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
    DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
          //调用unsafe进行堆外内存分配
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
      //用于内存回收
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
//底层使用malloc进行内存分配
void* x = os::malloc(sz, mtInternal);

//===============================
public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

    HeapByteBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new byte[cap], 0);
        /*
        hb = new byte[cap];
        offset = 0;
        */
    }
示例代码

NIO支持非阻塞,可以不使用selector,而自己去实现轮询功能

  1. 不使用Selector的非阻塞服务端
public class NonBlockNioServer {
    private static List<SocketChannel> list = new ArrayList<>();

    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(35155));

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            if (socketChannel != null) {
                list.add(socketChannel);
                socketChannel.configureBlocking(false);
            }
            Iterator<SocketChannel> iterator = list.iterator();
            while (iterator.hasNext()) {
                SocketChannel socket = iterator.next();
                //重复利用
                buffer.clear();
                try {
                    int read = socket.read(buffer);
                    if (read > 0) {
                        System.out.println("========" + read);
                        //根据读取的大小取数据
                        System.out.println(new String(buffer.array(),0,read));
                        //将从零开始写入
                        buffer.rewind();
                        String s = "lalala";
                        buffer.put(s.getBytes());
                        //发送前需要先flip(),限制真实发送的数据
                        buffer.flip();
                        socket.write(buffer);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    iterator.remove();
                }
            }
        }

    }
}
  1. 使用selector的服务端
public class NioServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        serverSocketChannel.bind(new InetSocketAddress(10200));
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        while (true) {
            selector.select();
            Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
            while (selectionKeyIterator.hasNext()) {
                SelectionKey key = selectionKeyIterator.next();
                selectionKeyIterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();
                    if (socketChannel != null) {
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }
                } else if (key.isReadable()) {
                    if (key.isValid()) {
                        try {
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            buffer.clear();
                            int length = socketChannel.read(buffer);
                            if (length > 0) {
                                String received = new String(buffer.array(), 0, length, "utf-8");
                                System.out.println(received);
                                buffer.rewind();
                                buffer.put(("received :" + received).getBytes());
                                buffer.flip();
                                socketChannel.write(buffer);
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
  1. NIO客户端
public class NioClient {
    private Selector selector;
    private volatile boolean flag = false;
    private void init() {
        try {
            selector = Selector.open();
            SocketChannel channel = SocketChannel.open();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_CONNECT);
            channel.connect(new InetSocketAddress("localhost", 10200));
            System.out.println("client connected");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private void listen() {
        while (true) {
            if (!flag) {
                try {
                    int length = selector.select(2000);
                    if (length > 0) {
                        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                        while (iterator.hasNext()) {
                            SelectionKey next = iterator.next();
                            iterator.remove();
                            handleKeys(next);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                System.out.println("11 ------ 客户端断开连接  并且 停止客户端程序");
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    try {
                        iterator.next().channel().close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                    }
                }
                System.exit(1);
            }
        }
    }

    private void handleKeys(SelectionKey key) {
        if (key.isValid()) {
            SocketChannel channel = (SocketChannel) key.channel();
            if (key.isConnectable()) {
                boolean isSuccess = false;
                try {
                    isSuccess = channel.finishConnect();
                    System.out.println("connect server success");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (isSuccess) {
                    String[] requestStrArray = new String[] { "getNow", "getServerName", "abcde" };
                    Random r = new Random();
                    String request = requestStrArray[r.nextInt(requestStrArray.length)];
                    System.out.println("6 ------ client  发送请求给  server: " + request);

                    try {
                        channel.register(this.selector, SelectionKey.OP_READ);
                    } catch (ClosedChannelException e) {
                        e.printStackTrace();
                    }
                    writeDataToServer(channel,request);
                }
            } else if (key.isReadable()) {
                ByteBuffer allocate = ByteBuffer.allocate(1024);
                try {
                    int length = channel.read(allocate);
                    String s = new String(allocate.array(), 0, length);
                    System.out.println("receive from server " + s);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (key!=null) {
                    key.cancel();
                    if (key.channel()!=null) {
                        try {
                            key.channel().close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    flag = true;
                }
            }
        }
    }
    private void writeDataToServer(SocketChannel clientChannel, String request) {
        ByteBuffer allocate = ByteBuffer.allocate(1024);
        allocate.put(request.getBytes());
        allocate.flip();
        try {
            clientChannel.write(allocate);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("client send server "+request);
    }

    public static void main(String[] args) {
        NioClient nioClient = new NioClient();
        nioClient.init();
        nioClient.listen();
    }
}
Nio底层调用

NIO是Non Blocking IO的缩写,服务器读写和端口监听都不阻塞,linux有NIO有三种模式,分别是select,poll和epoll三种模式,MAC是基于Kqueue实现。

基于netty的iot 开源项目 netty源码剖析与应用_NIO_06

select特点:
  • select模型只有一个select函数,每次在调用select函数 时,都需要把整个文件描述符集合从用户态拷贝到内核态,当文件描 述符很多时,开销会比较大。
  • 每次在调用select函数时,内核都需要遍历所有的文件描述 符,这个开销也很大,尤其是当很多文件描述符根本就无状态改变 时,也需要遍历,浪费性能。
  • select可支持的文件描述符有上限,可监控的文件描述符个 数取决于sizeOf(fd_set)的值。如果sizeOf(fd_set)=512,那么此服 务器最多支持512×8=4096个文件描述符。
epoll

NIO是对各种类型操作系统的基于事件驱动的方案的抽象封装,linux和solaris使用epoll,mac使用kqueue,都是基于底层的事件驱动机制,所以能做到非阻塞。epoll使用三个系统调用来实现,分别是epoll_create,epoll_ctl和epoll_wait;kqueue使用了两个系统调用,分别是kqueue()、kevent()。系统调用在linux或mac中可以通过man查看具体的说明。

基于netty的iot 开源项目 netty源码剖析与应用_NIO_07


基于netty的iot 开源项目 netty源码剖析与应用_ci_08

epoll的三个系统调用函数

  1. epoll_create建立一个epoll对象。
  2. epoll_ctl往epoll对象中增加/删除某一个流的某一个事件,设置你关心的事件。
  3. epoll_wait等待直到注册的事件发生

kqueu的两个系统调用函数

  1. kqueue() 生成一个内核事件队列,返回该队列的文件描述符。
  2. kevent() 提供向内核注册 / 反注册事件和返回就绪事件或错误事件。

nio通过DefaultSelectorProvider.create()创建SelectorImpl,方法会根据不同的操作系统去实现,这也是java能实现跨平台的一个示例。整个流程方法大致如下:selectProvider→openselect()方法创建selectorImpl→创建wrapper类,一般wrapper类实现与底层交互的操作,通过jni去执行jvm中c的操作,再通过c调用操作系统调用,见下图。

基于netty的iot 开源项目 netty源码剖析与应用_NIO_09

上面的系统调用与nio的几个阶段相对应,selector.open,socketChannel.reigster和selector.open。socketChannel.reigste主要是将文个描述符放入到数组中,而select.open执行了epoll_ctl和epoll_wait调用。

基于netty的iot 开源项目 netty源码剖析与应用_Netty_10

详细调用图如下:

基于netty的iot 开源项目 netty源码剖析与应用_NIO_11

Netty使用篇

NIO服务端开发步骤

编写一套NIO服务器需要完成的三步:

  1. 服务器启动与端口监听
  1. 创建多路复用器selector(Netty会开启额外线程去创建)
  2. 打开ServersocketChannel(Netty采用NioServerSocketChannel来包装)
  3. 把ServersocketChannel注册到selector(Netty在额外的NioEventLoop线程上执行,并返回ChannelPromise来异步通知是否注册成功)
  4. 绑定端口
  5. 设置监听OP_ACCEPT事件
  1. ServerSocketChannel处理新接入链接
  1. 循环遍历多路复用器准备就绪的SelectionKey
  2. 从SelectionKey中获取ServerSocketChannel
  3. 通过ServerSocketChannel的accept获取接入的客户端链接SocketChannel
  4. 把客户端链接注册到Selector上,并监听其OP_READER事件
  1. SocketChannel读/写数据
  1. 循环遍历多路复用器准备就绪的SelectionKey
  2. 从SelectionKey中获取SocketChannel
  3. 从SocketChannel中读取数据
  4. 解码、编码→业务逻辑处理
  5. SocketChannel write结果

Netty服务端开发步骤

Netty服务端程序实现步骤:

  1. 创建两个线程组,分别为Boss线程组和Worker线程组。Boss 线程专门用于接收来自客户端的连接;Worker线程用于处理已经被 Boss线程接收的连接。
  2. 运用服务启动辅助类ServerBootstrap创建一个对象,并配 置一系列启动参数,如参数ChannelOption .SO_RCVBUF和 ChannelOption .SO_SNDBUF分别对应接收缓冲区和发送缓冲区的大 小。
  3. 当Boss线程把接收到的连接注册到Worker线程中后,需要交 给连接初始化消息处理Handler链。由于不同的应用需要用到不同的 Handler链,所以Netty提供了ChannelInitializer接口,由用户实现 此接口,完成Handler链的初始化工作。
  4. 编写业务处理Handler链,并实现对接收客户端消息的处理 逻辑。
  5. 绑定端口。由于端口绑定需要由Boss线程完成,所以主线程 需要执行同步阻塞方法,等待Boss线程完成绑定操作。

Netty示例代码

服务端代码:

public class NettyServer {

    public void initServer() {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, worker).channel(NioServerSocketChannel.class);
            bootstrap.option(ChannelOption.SO_BACKLOG, 128).childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new StringEncoder());
                    ch.pipeline().addLast(new ServerChannelHandler());
                }
            });
            ChannelFuture future = bootstrap.bind(Constants.PORT).sync();
            ZookeeperServerWatch.getInstance().register();
            System.out.println("server start at 10111");
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
            boss.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new NettyServer().initServer();
    }
}


public class ServerChannelHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println(msg);
        ctx.channel().writeAndFlush("receive you");
    }
}

客户端代码:

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup work = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(work).channel(NioSocketChannel.class);
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(new StringDecoder());
                ch.pipeline().addLast(new StringEncoder());
                ch.pipeline().addLast(new ClientChannelHandler());
            }
        });
        ChannelFuture future = bootstrap.connect("localhost", 10111).sync();
        future.channel().writeAndFlush("hello world");
    }
}

public class ClientChannelHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println(msg);
    }
}

源码分析篇

相关类介绍

ServerBootStrap/BootStrap

Bootstrap是一个Netty服务引导启动类,用于串联各种组件或配置。服务端使用ServerBootstrap,客户端使用Bootstrap,两个共同继承自AbstractBootstrap父类,两者区别是Bootstrap则只有一个NioEventLoopGroup,而ServerBootstrap有多一个NioEventLoopGroup作为childgroup进行初始化,这个childgroup则是服务端在收到服务端连接后的读写处理,而共同使用的这个EventLoopGroup是在AbstractBootstrap中定义的属性group。

基于netty的iot 开源项目 netty源码剖析与应用_NIO_12

NioSocketChannel/NioServerSocketChannel

NioSocketChannel/NioServerSocketChannel是对NioChannel的封装(见图中红框部分),这两个类是在Bootstrap中传入的class类,在初始化方法initAndRegister()中使用ReflectiveChannelFactory来反射实例化。

基于netty的iot 开源项目 netty源码剖析与应用_基于netty的iot 开源项目_13

NioEventLoopGroup/NioEventLoop

基于netty的iot 开源项目 netty源码剖析与应用_ci_14

NioEventLoopGroup是一个管理NioEventLoop生命周期的类,NioEventLoopGroup是可以对多个NioEventLoop进行管理的,在初始化的时候根据如果不传入线程默认使用当前机器CPU核数的2倍去初始化NioEventLoop,初始化同时会通过EventExecutorChooserFactory初始化一个EventExecutorChoosert选择器,选择器根据一定的算法来选择NioEventLoop。

NioEventLoop对NIO的Selector和SelectionKey进行了封装,并对Selector进行了优化,使用数组去替换了原生HashSet的selectedKeys属性。

//对nio中的selector方法进行包装,并进行部分优化
    private SelectorTuple openSelector() {
        final Selector unwrappedSelector;
        try {
            //创建selector,获取原生nio的selector
            unwrappedSelector = provider.openSelector();
        } catch (IOException e) {
            throw new ChannelException("failed to open a new selector", e);
        }
        //判断是否设置了优化开关,通过系统属性设置io.netty.noKeySetOptimization
        if (DISABLE_KEY_SET_OPTIMIZATION) {
            return new SelectorTuple(unwrappedSelector);
        }
        //反射加载SelectorImpl类,此类是前面创建原生Selector的父类,后面使用unsafe去替换 selectedKeys 和 publicSelectedKeys
        Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    return Class.forName(
                            "sun.nio.ch.SelectorImpl",
                            false,
                            PlatformDependent.getSystemClassLoader());
                } catch (Throwable cause) {
                    return cause;
                }
            }
        });
        //判断是否加载成功,未加载成功就使用原生的selector
        if (!(maybeSelectorImplClass instanceof Class) ||
            // ensure the current selector implementation is what we can instrument.
            !((Class<?>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) {
            if (maybeSelectorImplClass instanceof Throwable) {
                Throwable t = (Throwable) maybeSelectorImplClass;
                logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, t);
            }
            return new SelectorTuple(unwrappedSelector);
        }

        final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
        //此处是优化类,实现了Abstract接口,使用数组去替换前面的HashSet
        //SelectedSelectionKeySet具体做了什么优化呢?主要是数据结构 改变了,用数组替代了HashSet,重写了add()和iterator()方法,数组的遍历效率更高
        final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();

        //执行替换,如果正常会返回null,非正常会返回错误,后面去判断
        Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    //selectedKeys为就绪Key的集合,拥有所有操作事件准备就绪的选择 Key
                    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                    //publicSelectedKeys为外部访问就绪Key的集合代理,由 selectedKeys集合包装成不可修改的集合
                    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
                    //java9以上且有unsafe类就使用unsafe去替换,设置后就直接返回
                    if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) {
                        // Let us try to use sun.misc.Unsafe to replace the SelectionKeySet.
                        // This allows us to also do this in Java9+ without any extra flags.
                        long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField);
                        long publicSelectedKeysFieldOffset =
                                PlatformDependent.objectFieldOffset(publicSelectedKeysField);

                        if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) {
                            PlatformDependent.putObject(
                                    unwrappedSelector, selectedKeysFieldOffset, selectedKeySet);
                            PlatformDependent.putObject(
                                    unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet);
                            return null;
                        }
                        // We could not retrieve the offset, lets try reflection as last-resort.
                    }
                    //设置为可写
                    Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }
                    cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }
                    //使用默认的反射设置方法
                    selectedKeysField.set(unwrappedSelector, selectedKeySet);
                    publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
                    return null;
                } catch (NoSuchFieldException e) {
                    return e;
                } catch (IllegalAccessException e) {
                    return e;
                }
            }
        });

        //如果有异常就使用原生的
        if (maybeException instanceof Exception) {
            selectedKeys = null;
            Exception e = (Exception) maybeException;
            logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e);
            return new SelectorTuple(unwrappedSelector);
        }
        selectedKeys = selectedKeySet;
        logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
        return new SelectorTuple(unwrappedSelector,
                                 new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
    }

NioEventLoop中的run()方法是一个去轮询nio事件和处理taskQueue队列

//首先调用select(boolean oldWakenUp)方法轮询就绪的Channel;
    // 然后调用processSelectedKeys()方法处理I/O事件;
    // 最后运行 runAllTasks()方法处理任务队列
    @Override
    protected void run() {
        int selectCnt = 0;
        for (;;) {
            try {
                int strategy;
                try {
                    //根据是否有任务获取策略,如果有任务,返回 selectNow(),没任务返回SelectStrategy.SELECT
                    strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
                    switch (strategy) {
                    case SelectStrategy.CONTINUE:
                        continue;

                    case SelectStrategy.BUSY_WAIT:
                        // fall-through to SELECT since the busy-wait is not supported with NIO

                    case SelectStrategy.SELECT:
                        //查询下一个task的deadline时间
                        long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                        if (curDeadlineNanos == -1L) {
                            curDeadlineNanos = NONE; // nothing on the calendar
                        }
                        nextWakeupNanos.set(curDeadlineNanos);
                        try {
                            if (!hasTasks()) {
                                //调用nio的select方法,按超时时间返回
                                strategy = select(curDeadlineNanos);
                            }
                        } finally {
                            // This update is just to help block unnecessary selector wakeups
                            // so use of lazySet is ok (no race condition)
                            nextWakeupNanos.lazySet(AWAKE);
                        }
                        // fall through
                    default:
                    }
                } catch (IOException e) {
                    // If we receive an IOException here its because the Selector is messed up. Let's rebuild
                    // the selector and retry. https://github.com/netty/netty/issues/8566
                    rebuildSelector0();
                    selectCnt = 0;
                    handleLoopException(e);
                    continue;
                }

                selectCnt++;
                cancelledKeys = 0;
                needsToSelectAgain = false;
                //ioRatio(I/O事件与taskQueue运行的时间占比)
                final int ioRatio = this.ioRatio;
                boolean ranTasks;
                if (ioRatio == 100) {
                    try {
                        if (strategy > 0) {//判断selector.select是否有数据返回,如果有返回先执行selectedkeys
                            processSelectedKeys();
                        }
                    } finally {
                        // Ensure we always run tasks.
                        ranTasks = runAllTasks();
                    }
                } else if (strategy > 0) {
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        //processSelectedKey的运行时间
                        final long ioTime = System.nanoTime() - ioStartTime;
                        //ioRatio占比 越高,运行时间越少,目前未找到ioRatio设置地方,只有初始的50,所以一般只占一半的运行时间
                        ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                } else {
                    ranTasks = runAllTasks(0); // This will run the minimum number of tasks
                }

                if (ranTasks || strategy > 0) {
                    if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                                selectCnt - 1, selector);
                    }
                    selectCnt = 0;
                //此处是解决空轮询bug,如果大于阈值(默认152)就重新rebuildselector
                } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
                    selectCnt = 0;
                }
            } catch (CancelledKeyException e) {
                // Harmless exception - log anyway
                if (logger.isDebugEnabled()) {
                    logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                            selector, e);
                }
            } catch (Error e) {
                throw e;
            } catch (Throwable t) {
                handleLoopException(t);
            } finally {
                // Always handle shutdown even if the loop processing threw an exception.
                try {
                    if (isShuttingDown()) {
                        closeAll();
                        if (confirmShutdown()) {
                            return;
                        }
                    }
                } catch (Error e) {
                    throw e;
                } catch (Throwable t) {
                    handleLoopException(t);
                }
            }
        }
    }
ByteBuf

NIO中的ByteBuffer使用方式比较复杂,Netty提供的ByteBuf在使用上进行了简化,也有基于堆或直接内存的实现,同时进行了很多优化,Netty中的零拷贝就进基于直接内存缓存的实现,有基于Pooled和UnPooled的实现,Composite是对多个ByteBuf的组合。

基于netty的iot 开源项目 netty源码剖析与应用_ci_15

内存管理
RecvByteBufAllocator

内存预测器,根据当前对通道数据大小的读取在评估后面使用的内存大小。

基于netty的iot 开源项目 netty源码剖析与应用_Netty_16

RecvByteBufAllocator类在初始化的时候会初始化一个静态数组SIZE_TABLE,这个SIZE_TABLE在[16,512]区间每次增加16,直到512;而 从512起,每次翻一倍,直到int的最大值。

RecvByteBufAllocator通过Handle内部类进行各种操作,RecvByteBufAllocator负责创建这个类。Handle通过guess()方法估算需初始化的内存大小,完成ByteBufAllocator的内存分配,每次读写变动时会更新预测下一次的初始化大小,在初始化时默认初始化2048(256B)

SizeClass/Allocator/PoolChunk/PoolSubPage/PoolArena/PoolThreadCache

PooledByteBufAllocator这个是内存分配类,在这个类中会通过本地线程缓存类PoolThreadLocalCache去存储PoolThreadCache,当PoolThreadCache缓存中没有时根据内存类型(是直接内存还是堆内存)调用PoolArena去进行内存分配,PooledByteBufAllocator会根据CPU或内存大小比例去初始化几个PoolArena。

PoolThreadCache是针对不同类型的page缓存,分small和normal通过MemoryRegionCache类型进行缓存,在MemoryRegionCache中通过Entry队列存储。

基于netty的iot 开源项目 netty源码剖析与应用_ci_17

sizeClasses是PoolArena的父类,主要是初始化一个sizeClasses的数组,这是一个二维数组,主要是存放以下几个数据index, log2Group, log2Delta, nDelta, isMultiPageSize, isSubPage, log2DeltaLookup,同时会初始化这个表格表示的size对应表格索引的sizeIdx2sizeTab,多页的size对应属性pageIdx2sizeTab,以及lookup和size对应属性size2idxTab。初始化这个的目的是为了后面根据表格去分配内存。

这个内存分配原因是来自于jemalloc的论文,按大小分为small,large和huge三个区,在这个分配中,根据SizeClasses类中初始化的数组sizeClasses使用算法int size = (1 << log2Group) + (nDelta << log2Delta)来计算size值。

基于netty的iot 开源项目 netty源码剖析与应用_NIO_18

PoolArena是PooledByteBufAllocator调用的内存分配类,这个类会根据不同大小的内存需求去分配small, normal, 和huge内存,huge是直接内存分配,small和normal都会先从缓存PoolThreadCache中去获取,如果没有small分通过PoolSubPage去进行内存分配,如果不足会从PoolChunk的Page中分页一小块切分为小段进行分配,normal会通过PoolChunk进行内存分配。

这三个区Huge是通过直接内存进行分配的,其它两个区块在Netty中处理稍有不同(4.1.69版本)整个分配是通过PoolChunk,PoolSubPage来进行分配,资源的分配是通过PoolArena去分配的。

  • 小于4K的部分属于small区域,先会去缓存PoolThreadCache中去分配,未分配成功再分配PoolSubPage,调用PoolChunk去初始化subPage内存。
  • 大于4K,小于2M的通过PoolChunkList去分配,在不同使用比例的PoolChunkList中没有分配到使用空间会初始化PoolChunk放入到QInit队列中。
  • 大于2M的使用直接内存分配

基于netty的iot 开源项目 netty源码剖析与应用_ci_19

在PoolChunk中通过将内存区分为段进行管理,即page是最小的管理单元,一系统列page的集合称为run,一系统run的集合称为chunk。

在PoolChunk中通过runsAvailMap和runsAvail管理run,runAvail是一个PriorityQueue数组队列,runsAvailMap管理了所有的可使用的run的集合,一组集合的开始和结束runOffset会存储在runsAvailMap中,这两个存储的值为Handler,一个Handler即一个long类型的数组,共64位,通过不同的位表示不同的意义。

基于netty的iot 开源项目 netty源码剖析与应用_基于netty的iot 开源项目_20

整个Handler分前后两个部分,前32包含四个部分,如果是subPage后32位表示subpage的bitmap

  • 前32位
  • 前15位表示的整个page的位移
  • 中间的15位表示page的大小
  • 31位表示是否使用
  • 32位表示是否子而,对应sizeclass中的isSubPage属性
  • 后面的32位用于表示subpage的bitmap

基于netty的iot 开源项目 netty源码剖析与应用_NIO_21

PoolSubPage是small区块的内存封装,所有的PoolSubpage会存放在PoolChunk的subpages数组属性中,每个index对PoolChunk中的一块区块。在PoolSubPage会记录当前对应的会是一个链表结构,将PoolChunk中的一小块区块分为若干个小的段组成的subpage链表,每个段的大小根据需要的容器大小与sizeClass二级数组对应的大小关系确定;划分的小段与Handler中的后32位bitmap位进行映射,用0或1表示未分配与分配,如果已分配完所有的可用小段就从这个链表中删除。

PoolChunkList是不同占用比例PoolChunk的链表封装,所有对PoolChunkList的操作最后都是依赖底层的PoolChunk。在PoolArena中会根据不同的比例初始化6个PoolChunk,分别是qInit, q000, q025, q050, q075, q100,他们分别组成了一个循环的双向链表,但q000前面指向空而不是qInit,以避免qInit被回收。

基于netty的iot 开源项目 netty源码剖析与应用_NIO_22

内存管理相关类依赖图如下:

基于netty的iot 开源项目 netty源码剖析与应用_源码_23

调用关系

基于netty的iot 开源项目 netty源码剖析与应用_Netty_24

调用内存管理通过read()方法去调用内存管理工具的使用,有NioByteUnsafe和NioMessageUnsafe的实现,这两个实现分别对应的是boss线程和work线程的。两个处理方式大致相同

NioByteUnsafe:

基于netty的iot 开源项目 netty源码剖析与应用_Netty_25

NioMessageUnsafe:

基于netty的iot 开源项目 netty源码剖析与应用_NIO_26

ChannelPipeline/ChannelHandlerContext/ChannelHandler

ChannelPipeline相当于一个管道,在管道中将一个一个的ChannelHandler封装成ChannelHandlerContext,这些Context通过链表的形式从头到尾的连接起来,Head和Tail是两个特殊的Handler,在初始化的时候会初始化,后面添加的会添加到这两个的中间。ChannelHandlerContext会分为ChannelInboundInvoker, ChannelOutboundInvoker两种类型,是按输入和输出两个方向,输入是从head到tail,而输出是从tail到head顺序。

在Boss线程初始化时会初始化一个ServerBootstrapAcceptor类,这个类负责将boss线程中收到的连接注册到worker中,并将我们初始化时的childHandler初始化到worker的pipeline中

基于netty的iot 开源项目 netty源码剖析与应用_Netty_27

相关的类依赖图如下:

基于netty的iot 开源项目 netty源码剖析与应用_源码_28

调用流程中有大量的fireXXX方法就是,这些方法的调用是通过AbstractChannelHandlerContext类提供的invokeChannelXXX方法去链式调用后一个context方法。调用顺序梳理如下:

  1. 最初由pipeline发起,给定初始context为head,去调用invokeChannelxxx方法
  2. 由AbstractChannelHandlerContext获取handler,去调用handler的channelxxx方法,传入this类,即AbstractChannelHandlerContex
  3. handler的channelxxx方法执行完后回调AbstractChannelHandlerContext方法的fireChannelxxx方法
  4. fireChannelxxx方法通过调用findContextInbound/findContextOutBound方法获取下一个context
  5. 继续执行第一步的调用,只是给定的为下一个context

context提供了channelxxx两个相关的方法

  1. invokeChannelxxx方法,这个方法会调用传入context所包含handler的相关实现方法,这个方法一般是用抽象父类AbstractChannelContext的实现方法
  2. fireChannelxxx方法,这个方法会找到当前context的下一个context,然后再去调用第一个方法,即invokeChannelxxx方法

Netty读写流程

Netty是对NIO的一个封装,特别是在线程模型、内存管理等多方面进行了优化,下面从整个Netty的启动和读写流程进行介绍

Netty线程模型

从服务实现看似Netty与NIO没有关系,其实是对NIO的精巧封装,通过主线程与子线程去分别针对不同阶段的处理,创建主线程只负责监听并将连接交给子线程去处理,子线程可以通过多个线程(一个线程对应一个selector)的封装去处具体具体的读写操作,并通过管道去完成相应的操作,管道里可以定义很多Handler,我们可以自定义Handler去处理相应的业务,相关的线程模型如下:

基于netty的iot 开源项目 netty源码剖析与应用_ci_29

服务端启动流程

根据以上的线程模型,服务端在启动时会使用两个线程组,Boss线程组初始化一般使用一个线程,主要负责客户端的连接,连接成功后会将当前连接注册到worker线程组,worker线程线负责通道的读写。Boss和worker都是使用NioEventLoop,Boss一般使用一个NioEventLoop,worker线程组默认会按cpu核数的2倍初始化NioEventLoop。NioEventLoop主要就是Selector和TaskQueue的封装,在一个线程方法中主要执行IO的轮询和Task任务。

基于netty的iot 开源项目 netty源码剖析与应用_ci_30

Boss和worker线程组之所以能复用相同的流程而执行不同的任务,是在run方法中调用了不同的Unsafe处理方法,NioMessageUnsafe主要负责Worker线程的处理,NioByteUnsafe主要负责Boss线程的处理。

基于netty的iot 开源项目 netty源码剖析与应用_Netty_31

时间轮

时间轮是一个近似IO时间的定时任务调度,通过一个HashedWheelBucket去模拟一个有刻度的时间轮,通过head和tail两个HashedWheelTimeout类型的指针去标识头尾,HashedWheelBucket主要负责时间的任务添加与过期处理;HashedWheelTimeout类会存储这个刻度时的一个双向链表,这个类里会存储当前的过期时间和任务状态;Worker是一个线程执行类,会负责HashedWheelTimer里到期和超时线程队列的处理。

基于netty的iot 开源项目 netty源码剖析与应用_Netty_32