当我们看Netty源码分析的时候,经常会看到几个常见组件比如Channel、Pipeline、Unsafe、EventLoopGroup、EventLoop。每个组件都是必不可少,而且这种设计对网络通信和逻辑处理的解耦起到非常重要的作用,比如我们在应用层不用关系底层网络通信的实现逻辑,只需编写ChannelHandler针对不同的请求事件处理即可。当我们想把BIO切换到NIO只需要做小小的变动,即可完成切换。所以我们今天主要来分析下三个比较关键的组件Channel、Unsafe、Pipeline。它们的作用如下:
Channel:用于执行网络I/O操作【BIO、NIO等等】,它是一个顶层的接口,针对不同的I/O类型它有不同的实现类,下面做一个简单的展示
列举一些常见的Channel类型:
Unsafe:这个不是我们常说的Java自带的sun.misc.Unsafe;我觉得因为它是不能被用户代码调用,所以取名叫做Unsafe;它的作用主要是用于数据传输操作,比如发送消息、把Channel注册到EventLoop等等数据操作。Unsafe主要跟自己专属的Channel一起使用
下面是它相关的API,我们从中也可以简单的了解它的作用
interface Unsafe {
RecvByteBufAllocator.Handle recvBufAllocHandle();
SocketAddress localAddress();
SocketAddress remoteAddress();
void register(EventLoop eventLoop, ChannelPromise promise);
void bind(SocketAddress localAddress, ChannelPromise promise);
void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise);
void disconnect(ChannelPromise promise);
void close(ChannelPromise promise);
void closeForcibly();
void deregister(ChannelPromise promise);
void beginRead();
void write(Object msg, ChannelPromise promise);
void flush();
ChannelPromise voidPromise();
ChannelOutboundBuffer outboundBuffer();
}
从上面我们很容易了解到,针对不同的I/O类型它都有一种不同的实现,下面对几个常见的Unsafe做一个介绍
Pipeline:Pipeline中由ChannelHandler组成的一条双向链表,创建一个Pipeline默认有一个头(HeadContext)、尾(TailContext)节点。ChannelHandler它是Channel中的逻辑处理,我们使用比较多的就是ChannelHandler这一块。
我们从上面分析可知Unsafe、Pipeline都是跟Channel有关联的,那么我们就从Channel为出发点,而且从Channel的类图我们可以看到,AbstractChannel是最顶层的父类,那么我们就以AbstractChannel的源码开始分析。
AbstractChannel的API如下:
我们主要从构造器->bind()->connect()简单浏览下源码
首先我们来看AbstractChannel的构造器,它有2个构造器,不过都大同小异,一个需要子类传入ChannelId,一个是调用newId()自动生成
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
protected AbstractChannel(Channel parent, ChannelId id) {
this.parent = parent;
this.id = id;
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
这里主要由三个关键方法
protected ChannelId newId() {
return DefaultChannelId.newInstance();
}
public static DefaultChannelId newInstance() {
return new DefaultChannelId();
}
private DefaultChannelId() {
data = new byte[MACHINE_ID.length + PROCESS_ID_LEN + SEQUENCE_LEN + TIMESTAMP_LEN + RANDOM_LEN];
int i = 0;
// machineId
System.arraycopy(MACHINE_ID, 0, data, i, MACHINE_ID.length);
i += MACHINE_ID.length;
// processId
i = writeInt(i, PROCESS_ID);
// sequence
i = writeInt(i, nextSequence.getAndIncrement());
// timestamp (kind of)
i = writeLong(i, Long.reverse(System.nanoTime()) ^ System.currentTimeMillis());
// random
int random = PlatformDependent.threadLocalRandom().nextInt();
i = writeInt(i, random);
assert i == data.length;
hashCode = Arrays.hashCode(data);
}
ChannelId主要由机器Id、进程Id、数字、时间戳、随机数组成
newUnsafe()它是一个模板方法,那么就以NIO的Server端的NioServceSocketChannel为例,那么调用的则是AbstractNioMessageChannel的newUnsafe()
protected AbstractNioUnsafe newUnsafe() {
return new NioMessageUnsafe();
}
这里很简单,就创建了一个NioMessageUnsafe实例
newChannelPipeline()方法也很简单,就是创建了一个DefaultChannelPipeline实例,并且把Channel实例传给了DefaultChannelPipeline对象。
protected DefaultChannelPipeline newChannelPipeline() {
return new DefaultChannelPipeline(this);
}
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
/**
* 创建了一个尾节点、头节点
*/
tail = new TailContext(this);
head = new HeadContext(this);
/**
* 尾节点和头节点互相关联,形成一个双向链表
*/
head.next = tail;
tail.prev = head;
}
我们来看一下 经常使用的DefaultChannelPipeline的addLast()方法
public final ChannelPipeline addLast(ChannelHandler handler) {
return addLast(null, handler);
}
@Override
public final ChannelPipeline addLast(String name, ChannelHandler handler) {
return addLast(null, name, handler);
}
@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
/**
* 把ChannelHandler包装成一个AbstractChannelHandlerContext
*/
newCtx = newContext(group, filterName(name, handler), handler);
/**
* 把上面生成的Context加入链表中,即尾节点的前面
*/
addLast0(newCtx);
//下面省略一些其他代码,有兴趣的,可以去看下我前面讲的Netty4.x源码分析:服务端绑定端口有讲
return this;
}
private void addLast0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext prev = tail.prev;
newCtx.prev = prev;
newCtx.next = tail;
prev.next = newCtx;
tail.prev = newCtx;
}
AbstractChannel的构造方法,主要做了3件事情
这里我们也可以发现每个Channel都有对应的Unsafe实例、Pipeline实例。
我们还是以AbstractChannel的bind方法为入口
public ChannelFuture bind(SocketAddress localAddress) {
return pipeline.bind(localAddress);
}
这里调用了DefaultChannelPipeline的bind()
DefaultChannelPipeline
public final ChannelFuture bind(SocketAddress localAddress) {
return tail.bind(localAddress);
}
TailContext
public ChannelFuture bind(SocketAddress localAddress) {
return bind(localAddress, newPromise());
}
public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
if (localAddress == null) {
throw new NullPointerException("localAddress");
}
if (isNotValidPromise(promise, false)) {
// cancelled
return promise;
}
/**
* 从当前节点开始,找到Pipeline中前一个outbound节点
*/
final AbstractChannelHandlerContext next = findContextOutbound();
EventExecutor executor = next.executor();
/**
* 如果next节点是在当前线程执行的,那么直接同步执行next节点的bind方法
*/
if (executor.inEventLoop()) {
next.invokeBind(localAddress, promise);
} else {
/**
* 如果next节点指定了EventExecutor,那么就在指定的EventExecutor里异步调用next节点的bind方法
*/
safeExecute(executor, new Runnable() {
@Override
public void run() {
next.invokeBind(localAddress, promise);
}
}, promise, null);
}
return promise;
}
从上面我们知道,从尾节点TailContext开始往前找属于outbound的节点,然后调用该节点的bind方法。这时,我们先设想Pipeline中只有头结点HeadContext和尾节点TailContext,那么这时候就调用了HeadContext的bind方法。【这里解释一下HeadContext即是inbound又是outbound,可以去看下HeadContext的类结构就清楚了】
HeadContext
public void bind(
ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
throws Exception {
/**
* 这里就是调用了Unsafe的bind方法,上面我们说到,在创建Channel的时候,针对不同的I/O类型Channel都会创建一个特定的Unsafe实例。
* 那么我们还是以NIO为例,上面我们介绍过,创建NioServerSocketChannel的时候会创建NioMessageUnsafe实例。那么这里就是调用
* NioMessageUnsafe的bind方法,但是NioMessageUnsafe没有实现这个bind方法,所以就调用父类的AbstractUnsafe的bind方法
*/
unsafe.bind(localAddress, promise);
}
AbstractUnsafe
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
// 省略前面的
/**
* 我们只关注这个doBind方法,实现方法在Channel中。doBind它是一个模板方法,实现类在它的子类。
* 因为我们是NioServerSocketChannel,所以实现方法在NioServerSocketChannel类中,为什么要这样实现呢?其实很简单,
* 不同的I/O类型的绑定都是不一样的,而Channel是对应I/O操作相关的。所以自然而然真正的绑定端口肯定在对应的子类Channel中实
* 现
*/
doBind(localAddress);
// 省略后面的
}
NioServerSocketChannel
protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
javaChannel().bind(localAddress, config.getBacklog());
} else {
/**
* 这里就很简单了,调用了我们熟悉的JDK的API
*/
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}
Channel的connect就不讲了,跟bind逻辑差不多,可以自己试着去分析下。
1、Netty底层把I/O抽象成了Channel,对应不同I/O模型有对应的Channel实例与之对应。比如NIO的服务端就对应NioServerSocketChannel等等。这就当我们想改变I/O模型,只需要改变对应的Channel即可,非常之方便,并不需要关心底层的API。
2、创建Channel的时候会生成2个非常重要的组件Unsafe、Pipeline。Unsafe针对不同I/O模型也有对应的实现。而Pipeline不管使用哪种I/O模型都是一样的,都会创建一条DefaultChannelPipeline,它是一个双向链表,有一个头结点HeadContext、尾节点TailContext组成。
3、在Pipeline中执行业务逻辑的时候,是在单线程中执行的,所以是线程安全的。当我们在分析bind方法的时候,当取出下一个outbound的next节点时,首先判断它是否有指定的EventExecutor,如果没有,那么就同步执行,如果有,那么就异步在指定的EventExecutor内执行。这种设计减少了线程的上下文切换,并且是线程安全的,所以在Pipeline中不用考虑一些数据安全的问题,效率非常高。