前言/回顾
在之前的博客中,虽然有讲过NioServerSocketChannel,但这个channel其实就如当时博客的标题所说,是个服务端channel。那么既然有服务端channel,就必定会有客户端channel,否则服务端的存在就没有意义了。而这节就简单梳理一下他们的区别,如有缺漏,以后遇到会补上。
关于客户端channel,在前面的博客中并没有记录,在后面写Netty处理新连接的时候就会遇到,这里先记录下它们的核心区别,方便之后混乱时回查。
这里再提前说下,服务端channel和客户端channel的核心区别就在于它们各自的config类和unsafe类的实现,下面就会讲到。
Netty Version:4.1.6
类的关系图
在开始之前,我们不妨先看看它们类图之间的关系(下面类图经过一定简化):
- 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就是读写的核心之一。