Netty底层与Java NIO对应关系

在讲Netty 客户端程序时候提到指定NioSocketChannel用于创建客户端NIO套接字通道的实例,下面我们来看NioSocketChannel是如何创建一个Java NIO里面的SocketChannel 的。首先我们来看NioSocketChannel的构造函数:

    public NioSocketChannel() {
        this(DEFAULT_SELECTOR_PROVIDER);
    }

其中 DEFAULT_SELECTOR_PROVIDER 定义如下:

private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = 
SelectorProvider.provider();

然后继续看

    //这里的 provider 为 DEFAULT_SELECTOR_PROVIDER
    public NioSocketChannel(SelectorProvider provider) {
        this(newSocket(provider));
    }

其中 newSocket 代码如下:

    private static SocketChannel newSocket(SelectorProvider provider) {
        try {
            return provider.openSocketChannel();
        } catch (IOException e) {
        throw new ChannelException("Failed to open a socket.", e);
        } 
    }

所以 NioSocketChannel 内部是管理一个客户端的SocketChannel 的,这个 SocketChannel 就是讲 Java NIO时候的 SocketChannel,也就是创建 NioSocketChannel 实例对象的时候相当于执行了 Java NIO 中:

SocketChannel socketChannel = SocketChannel.open();

另外在 NioSocketChannel 的父类 AbstractNioChannel 的构造函数里面默认会记录队 op_read 事件感兴趣,这个后面当链接完成后会使用到:

    protected AbstractNioByteChannel(Channel parent,SelectableChannel ch) {
        super(parent, ch, SelectionKey.OP_READ);
    }

另外在 NioSocketChannel 的父类 AbstractNioChannel 的构造函数里面设置了该套接字为非阻塞的。

    protected AbstractNioChannel(Channel parent, SelectableChannel ch,int readInterestOp) {
        super(parent);
        this.ch = ch;
        this.readInterestOp = readInterestOp;
        try {
            ch.configureBlocking(false);
        } catch (IOException e) {
        ...
        }
    }

下面我们看Netty里面是哪里创建的NioSocketChannel实例,哪里注册到选择器的。下面我们看下 Bootstrap connect 操作代码:

    public ChannelFuture connect(InetAddress inetHost, int inetPort) {
        return connect(new InetSocketAddress(inetHost, inetPort));
    }

类似 Java NIO 传递了一个 InetSocketAddress 对象用来记录服务端 ip 和端口:

    public ChannelFuture connect(SocketAddress remoteAddress) {
        ...
        return doResolveAndConnect(remoteAddress, config.localAddress());
    }

下面我们看下 doResolveAndConnect 的代码:

    private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    //(1)
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
        if (regFuture.isDone()) {
            if (!regFuture.isSuccess()) {
            return regFuture;
            }
    //(2)
    return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
        } 
    ...
    }

首先我们来看代码(1initAndRegister

    final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
        //(1.1)创建channel
        channel = channelFactory.newChannel();
        //(1.2)初始化channel
        init(channel);
        } catch (Throwable t) {
        ...
        }
        //(1.3)注册channel到eventLoopGroup(selector)上
        ChannelFuture regFuture = config().group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
            channel.close();
            } else {
            channel.unsafe().closeForcibly();
            } 
        } 
    return regFuture;
    }

其中(1.1)作用就是创建一个 NioSocketChannel 的实例,代码(1.2)是具体设置内部套接字的选项的。代码(1.3)则是具体注册客户端套接字到选择器的,其首先会调用 NioEventLoop register 方法,最后调用NioSocketChannelUnsafe 的 register 方法:

    public final void register(EventLoop eventLoop, final ChannelPromise promise) {
        ...
        AbstractChannel.this.eventLoop = eventLoop;
        if (eventLoop.inEventLoop()) {
            register0(promise);
        } else {//初次启动进入的是else
        try {
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
        ...
    } } }

其中 register0 内部调用 doRegister,其代码如下:

    protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
        try {
            //注册客户端 socket 到当前eventloop 的 selector 上
        selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
        return;
        } catch (CancelledKeyException e) {
    ...
    } } }

到这里代码(1)initAndRegister 的流程讲解完毕了,下面我们来看(2)doResolveAndConnect0的代码

    public final void connect(final SocketAddress remoteAddress, 
        final SocketAddress localAddress, final ChannelPromise promise) {
        ...
        try {
            ...
            boolean wasActive = isActive();
            if (doConnect(remoteAddress, localAddress)) {
                fulfillConnectPromise(promise, wasActive);
            } else {
                ...
            }
        } catch (Throwable t) {
            ...
        } }

其中 doConnect 代码如下:

    protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    ...
    boolean success = false;
    try {
        //2.1
        boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
        //2.2
        if (!connected) {
        selectionKey().interestOps(SelectionKey.OP_CONNECT);
        }
        success = true;
        return connected;
    } finally {
        if (!success) {
        doClose();
        } 
    } }

其中 2.1 具体调用客户端套接字的 connect 方法,等价于Java NIO里面的。代码2.2由于connect方法是异步的,所以类似 JavaNIO 调用 connect 方法进行判断,如果当前没有完成链接则设置对 op_connect 感兴趣。最后一个点就是何处进行的从选择器获取就绪的事件的,具体是在该客户端套接关联的 NioEventLoop 里面的做的,每个 NioEventLoop 里面有一个线程用来循环从选择器里面获 取就绪的事件,然后进行处理:

    protected void run() {
        for (;;) {
        try {
        ...
        select(wakenUp.getAndSet(false));
        ...
        processSelectedKeys();
        ...
        } catch (Throwable t) {
            handleLoopException(t);
        }
        ...
        } 
    }

其中 select 代码如下:

private void select(boolean oldWakenUp) throws IOException {
    Selector selector = this.selector;
    try {
    ...
    for (;;) {
    ...
    int selectedKeys = selector.select(timeoutMillis);
    selectCnt ++;
    ...
    } catch (CancelledKeyException e) {
    ...
    } 
}

可知会从选择器选取就绪的事件,其中 processSelectedKeys 代码如下:

private void processSelectedKeys() {
    ...
    processSelectedKeysPlain(selector.selectedKeys());
    ...
}

可知会获取已经就绪的事件集合,然后交给 processSelectedKeysPlain 处理,后者循环调用processSelectedKey具体处理每个事件,代码如下:

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    ...
    try {
    //(3)如果是 op_connect 事件
    int readyOps = k.readyOps();
    if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
    int ops = k.interestOps();
    ops &= ~SelectionKey.OP_CONNECT;
    k.interestOps(ops);
    //3.1
    unsafe.finishConnect();
    }
    //4如果是写事件
    if ((readyOps & SelectionKey.OP_WRITE) != 0) {
    ch.unsafe().forceFlush();
    }
    //5如果是读事件
    if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 ||                 readyOps == 0) {
    unsafe.read();
    }
    } catch (CancelledKeyException ignored) {
    unsafe.close(unsafe.voidPromise());
    } 
}

代码(3)如果当前事件 key op_connect 则去掉 op_connect,然后调用 NioSocketChannel的doFinishConnect:

protected void doFinishConnect() throws Exception {
    if (!javaChannel().finishConnect()) {
    throw new Error();
    }
}

可知是调用了客户端套接字的finishConnect方法,最后会调用 NioSocketChanneldoBeginRead方法设置对op_read事件感兴趣:

protected void doBeginRead() throws Exception {
    ...
    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    } 
}

这里 interestOps op_read,上面在讲解 NioSocketChannel 的构造函数时候提到过。代码(5)如果当前是 op_accept 事件说明是服务器监听套接字获取到了一个链接套接字,如果是 op_read,则说明可以读取客户端发来的数据了,如果是后者则会激活管线里面的所有 handler channelRead 方法,这里会激活我们自定义的 NettyClientHandler channelRead 读取客户端发来的数据,然后在向客户端写入数据。

总结:

本篇讲解了 Netty 客户端底层如何使用 Java NIO 进行实现的,可见与我们前面讲解的 Java NIO 设计的客户端代码步骤是一致的,只是 netty 对其进行了封装,方便了我们使用,了解了这些对深入研究 netty 源码提供了一个骨架指导。

你可能感兴趣的:(Netty)