前言/回顾

在之前的博客中,虽然有讲过NioServerSocketChannel,但这个channel其实就如当时博客的标题所说,是个服务端channel。那么既然有服务端channel,就必定会有客户端channel,否则服务端的存在就没有意义了。而这节就简单梳理一下他们的区别,如有缺漏,以后遇到会补上。

关于客户端channel,在前面的博客中并没有记录,在后面写Netty处理新连接的时候就会遇到,这里先记录下它们的核心区别,方便之后混乱时回查。

这里再提前说下,服务端channel和客户端channel的核心区别就在于它们各自的config类和unsafe类的实现,下面就会讲到。

Netty Version:4.1.6


类的关系图

在开始之前,我们不妨先看看它们类图之间的关系(下面类图经过一定简化):

channelunsafe的关系图.png

  • NioServerSocketChannel即服务端channel,NioSocketChannel即客户端channel。
  • 其它东西下面再解释。

从上面图中,就不难看出,它们除了本身的一些属性、方法等不是很重要的东西不同,它们的config、unsafe不同才是重点。下面我们从上至下开始分析。

AbstractChannel

在进入这个类之前,应该不难回忆起,我在前面在【服务端启动Netty时做了哪些事】这个章节的博客中,其实多多少少都遇到过。

下面截取代码中一些关键的属性:

    // 一些关键的属性,其余代码略
    private final Channel parent;
    private final ChannelId id;
    private final Unsafe unsafe;
    private final DefaultChannelPipeline pipeline;
    private volatile EventLoop eventLoop;
  • 前四个属性,在创建服务端channel的那篇博客中就完成创建了,只是当时仅仅是对服务端channel,后来学习到客户端channel时,发现也是复用同一个构造函数。
  • 关于EventLoop,在讲服务端注册selector的博客中就完成绑定了,后面讲客户端注册事件也会复用其中一段逻辑。
  • 另外,前面讲过的channel/selector的注册、绑定、初始化等核心方法其实都出自于或经过这个类。
  • 如果是服务端channel,这里的parent其实是null,因为它就表示parentChannel(客户端的parent就是服务端,服务端即null)。

AbstractNioChannel

同样截取一些关键的参数:

    private final SelectableChannel ch;
    protected final int readInterestOp;
    volatile SelectionKey selectionKey;
  • SelectableChannel其实就是客户端channel。
  • readInterestOp在创建服务端channel中就设置了OP_ACCEPT,而创建客户端channel则是OP_READ。
  • 关于SelectionKey,我在[processSelectedKeys]的博客中提过它的数据结构,在注册selector中的doRegister方法完成构造初始化。
  • 里面的doRegister方法是前面讲过的核心方法之一,负责绑定selector、channel、事件。它还有一个doBeginRead方法则是负责事件传播,在服务端启动时[绑定端口]后负责accept事件的传播,而在后面即将要讲的新连接接入则是负责传播read事件。

AbstractNioMessageChannel

这是服务端channel(NioServerSocketChannel)的抽象实现。

下面来看看其中一些方法:
io.netty.channel.nio.AbstractNioMessageChannel#newUnsafe

    @Override
    protected AbstractNioUnsafe newUnsafe() {
        return new NioMessageUnsafe();
    }
  • 这个方法返回的是NioMessageUnsafe,NioMessageUnsafe是AbstractNioMessageChannel的一个内部类。

我们去看下NioMessageUnsafe:
基本属性

private final List<Object> readBuf = new ArrayList<Object>();
  • 这个属性用于临时保存读取到的新连接(后面的博客会讲)。

另外,还需要关注它的read方法中的其中一段:

int localRead = doReadMessages(readBuf);
  • 这段代码其实就是在accept,也就是读取新连接。

下面就会拿AbstractNioByteChannel与上面做对比。


AbstractNioByteChannel

首先找到newUnsafe方法:
io.netty.channel.nio.AbstractNioByteChannel#newUnsafe

    @Override
    protected AbstractNioUnsafe newUnsafe() {
        return new NioByteUnsafe();
    }
  • 它与AbstractNioMessageChannel不一样,返回的是NioByteUnsafe,NioByteUnsafe是AbstractNioMessageChannel的内部类。

再找到NioByteUnsafe的read方法看到如下一段:
io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read

allocHandle.lastBytesRead(doReadBytes(byteBuf));
  • 这段代码说明这个read方法主要是在做数据流的读写,而不是AbstractNioMessageChannel的accept连接。

这里小结一下:

  • 服务端channel和客户端channel的区别之一其实在于它们处理的东西不一样,服务端channel主要是处理新连接(accept),客户端channel则主要是处理数据读写(ByteBuf)。对应各自的读写方法的实现(子类覆写的方法不再赘述,大致逻辑按照上面的思路想就对了)。

下面再来看看它们配置类的一些差别。

NioServerSocketChannelConfig

其实这个类在创建服务端channel是时也遇到过,但是当时并没有多说什么,现在一起来回忆一下几段代码:
io.netty.channel.socket.nio.NioServerSocketChannel#NioServerSocketChannel(java.nio.channels.ServerSocketChannel)

    public NioServerSocketChannel(ServerSocketChannel channel) {
        super(null, channel, SelectionKey.OP_ACCEPT);
        config = new NioServerSocketChannelConfig(this, javaChannel().socket());
    }

继续追进构造函数,直到看见如下代码:
io.netty.channel.socket.DefaultServerSocketChannelConfig#DefaultServerSocketChannelConfig

    public DefaultServerSocketChannelConfig(ServerSocketChannel channel, ServerSocket javaSocket) {
        super(channel);
        if (javaSocket == null) {
            throw new NullPointerException("javaSocket");
        }
        this.javaSocket = javaSocket;
    }
  • 之前的博客也说了上面这段代码其实就是在初始化负责初始化tcp连接的一些配置。但这只是服务端,到客户端配置就有点不一样了,下面就会看到。

不要忘了之前博客的Server.java设置了TCP_NODELAY。


NioSocketChannelConfig

客户端channel设置config的地方也与服务端channel相似,我们不妨在NioSocketChannel中找到如下方法:
io.netty.channel.socket.nio.NioSocketChannel#NioSocketChannel(io.netty.channel.Channel, java.nio.channels.SocketChannel)

    public NioSocketChannel(Channel parent, SocketChannel socket) {
        super(parent, socket);
        config = new NioSocketChannelConfig(this, socket.socket());
    }
  • 这里的配置类就是NioSocketChannelConfig。

然后我们继续跟进NioSocketChannelConfig的构造方法,最终来到如下代码:
io.netty.channel.socket.DefaultSocketChannelConfig#DefaultSocketChannelConfig

    public DefaultSocketChannelConfig(SocketChannel channel, Socket javaSocket) {
        super(channel);
        if (javaSocket == null) {
            throw new NullPointerException("javaSocket");
        }
        this.javaSocket = javaSocket;

        // Enable TCP_NODELAY by default if possible.
        if (PlatformDependent.canEnableTcpNoDelayByDefault()) {
            try {
                setTcpNoDelay(true);
            } catch (Exception e) {
                // Ignore.
            }
        }
    }
  • 可以看见,这个config就比服务端的config稍微多设置了一点东西,如setTcpNoDelay(true);
  • setTcpNoDelay(true);其实就是关闭Nagle算法。
  • 关闭Nagle算法的目的在于,让小数据包能更快的发送出去,从而降低与客户端数据传输的延迟。

以上暂时总结这么多,等后面博客讲netty处理新连接时,以上的知识就会用到。



小结

  • 服务端channel和客户端channel的区别之一其实在于它们处理的东西不一样,服务端channel主要是处理新连接(accept),客户端channel则主要是处理数据读写(ByteBuf)。对应各自的读写方法的实现(子类覆写的方法不再赘述,大致逻辑按照上面的思路想就对了)。
  • 服务端的config和客户端的config区别主要在于:客户端除了设置连接的基本属性之外,还会关闭tcp的Nagle算法,目的是让小数据包更快传输。
  • 另外,如果你大概查看过unsafe的方法,就能明白:unsafe就是读写的核心之一。