Netty之揭开BootStrap 的神秘面纱
客户端BootStrap:
Bootstrap 是Netty 提供的一个便利的工厂类, 我们可以通过它来完成Netty 的客户端或服务器端的Netty 初始化。下面我先来看一个例子, 从客户端和服务器端分别分析一下Netty 的程序是如何启动的。首先,让我们从客户端的代码片段开始:
- EventLoopGroup group = new NioEventLoopGroup();
- try {
- Bootstrap bootstrap = new Bootstrap();
- bootstrap.group(group).channel(NioSocketChannel.class)
- .option(ChannelOption.TCP_NODELAY, true)
- .handler(new ChannelInitializer<SocketChannel>() {
- @Override
- protected void initChannel(SocketChannel ch) throws Exception {
- ChannelPipeline pipeline = ch.pipeline();
- pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
- pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
- pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
- pipeline.addLast("handler", new MyClient());
- }
- });
- ChannelFuture f = bootstrap.connect("127.0.0.1", 6666).sync();
- f.channel().closeFuture().sync();
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- group.shutdownGracefully();
}
1.EventLoop 的初始化:
最开始的 Client 用户代码中,我们在一开始就实例化了一个NioEventLoopGroup 对象,因此我们就从它的构造器中追踪一下EventLoop 的初始化过程。首先来看一下NioEventLoopGroup 的类继承层次:
NioEventLoop 有几个重载的构造器,不过内容都没有太大的区别,从new NioEventLoopGroup()开始,其中确定了线程数,selectorProvider ,选择器策略工厂DefaultSelectStrategyFactory.INSTANCE 最终都是调用的父类MultithreadEventLoopGroup的构造器:
- protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
- super(nThreads == ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
- }
其中有个有意思的地方是,如果我们传入的线程数nThreads 是0,那么Netty 会为我们设置默认的线程数DEFAULT_EVENT_LOOP_THREADS,而这个默认的线程数是怎么确定的呢?其实很简单,在静态代码块中,会首先确定DEFAULT_EVENT_LOOP_THREADS 的值:
- static {
- DEFAULT_EVENT_LOOP_THREADS = Math.max(, SystemPropertyUtil.getInt(
- "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * ));
- }
Netty 首先会从系统属性中获取"io.netty.eventLoopThreads"的值,如果我们没有设置的话,那么就返回默认值:即处理器核心数* 2。回到MultithreadEventLoopGroup 构造器中会继续调用父类MultithreadEventExecutorGroup 的构造器:
- protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
- this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
- }
其中 DefaultEventExecutorChooserFactory 就是事件执行器选择策略的创建工厂,继续调用重载方法:
- protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
- EventExecutorChooserFactory chooserFactory, Object... args) {
- 。。。。。。
- children = new EventExecutor[nThreads];
- for (int i = ; i < nThreads; i ++) {
- 。。。。。。
- children[i] = newChild(executor, args);
}- 。。。。。。
- chooser = chooserFactory.newChooser(children);
- //去掉以下代码
- }
这里其实就是创建了一个大小为 nThreads 的事件执行器数组,第二步是初始化该数组,紧接着通过选择策略工厂 EventExecutorChooserFactory 创建选择策略,首先看一下他的初始化,调用newChhild()方法初始化children 数组。根据上面的代码,我们也能知道:MultithreadEventExecutorGroup 内部维护了一个EventExecutor 数组,而Netty 的EventLoopGroup 的实现机制其实就建立在MultithreadEventExecutorGroup 之上。每当Netty 需要一个EventLoop 时,会调用next()方法获取一个可用的EventLoop。newChild()方法是一个抽象方法,它的任务是实例化EventLoop 对象。我们跟踪一下它的代码。可以发现。这个方法在NioEventLoopGroup 类中有实现,其内容很简单:
- protected EventLoop newChild(Executor executor, Object... args) throws Exception {
- EventLoopTaskQueueFactory queueFactory = args.length == ? (EventLoopTaskQueueFactory) args[] : null;
- return new NioEventLoop(this, executor, (SelectorProvider) args[],
- ((SelectStrategyFactory) args[]).newSelectStrategy(), (RejectedExecutionHandler) args[], queueFactory);
- }
在此构造方法中调用其父类 SingleThreadEventLoop 的构造方法,再调用 SingleThreadEventExecutor的构造方法。数组初始化结束,紧接着是初始化选择器,我们可以继续跟踪到newChooser 方法里面看看其实现逻辑,具体代码如下:
- public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory {
- public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory();
- private DefaultEventExecutorChooserFactory() { }
- @SuppressWarnings("unchecked")
- @Override
- public EventExecutorChooser newChooser(EventExecutor[] executors) {
//判断是否二次幂- if (isPowerOfTwo(executors.length)) {
- return new PowerOfTwoEventExecutorChooser(executors);
- } else {
- return new GenericEventExecutorChooser(executors);
- }
- }
- private static boolean isPowerOfTwo(int val) {
- return (val & -val) == val;
- }
- private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
- private final AtomicInteger idx = new AtomicInteger();
- private final EventExecutor[] executors;
- PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
- this.executors = executors;
- }
- @Override
- public EventExecutor next() {
// 索引自增 & 执行器数组长度-1- return executors[idx.getAndIncrement() & executors.length - ];
- }
- }
- private static final class GenericEventExecutorChooser implements EventExecutorChooser {
- private final AtomicInteger idx = new AtomicInteger();
- private final EventExecutor[] executors;
- GenericEventExecutorChooser(EventExecutor[] executors) {
- this.executors = executors;
- }
- @Override
- public EventExecutor next() {
// 索引自增 % 执行器数组长度 后取绝对值- return executors[Math.abs(idx.getAndIncrement() % executors.length)];
- }
- }
- }
上面的代码逻辑主要表达的意思是:即如果nThreads 是2 的幂,则使用PowerOfTwoEventExecutorChooser,否则使用GenericEventExecutorChooser。这两个Chooser 都重写next()方法。next()方法的主要功能就是将数组索引循环位移,如下图所示:
当索引移动最后一个位置时,再调用next()方法就会将索引位置重新指向0。
这个运算逻辑其实很简单,就是每次让索引自增后和数组长度取模:idx.getAndIncrement() % executors.length。但是就连一个非常简单的数组索引运算,Netty 都帮我们做了优化。因为在计算机底层,&与比%运算效率更高。好了,分析到这里我们应该已经非常清楚MultithreadEventExecutorGroup 中的处理逻辑,最后总结一下整个EventLoopGroup 的初始化过程:
- EventLoopGroup(其实是MultithreadEventExecutorGroup)内部维护一个类型为EventExecutor children 数组,是大小为nThreads的SingleThreadEventExecutor,这样就构成了一个线程池。
- 如果我们在实例化NioEventLoopGroup 时,如果指定线程池大小,则nThreads 就是指定的值,反之是处理器核心数* 2。
- MultithreadEventExecutorGroup 中会调用newChild()抽象方法来初始化children 数组。
- 抽象方法newChild()是在NioEventLoopGroup 中实现的,它返回一个NioEventLoop 实例。
- NioEventLoop 属性赋值:
- provider:在NioEventLoopGroup 构造器中通过SelectorProvider.provider()获取一个SelectorProvider。
- selector:在NioEventLoop 构造器中通过调用通过provider.openSelector()方法获取一个selector 对象。
- 根据nThreads 的大小,创建不同的Chooser,即如果nThreads 是2 的幂,则使用PowerOfTwoEventExecutorChooser,反之使用GenericEventExecutorChooser。不论使用哪个Chooser,它们的功能都是一样的,即从children 数组中选出一个合适的EventExecutor 实例。
2.Channel 简介:
在Netty 中,Channel 是一个Socket 的抽象,它为用户提供了关于Socket 状态(是否是连接还是断开)以及对Socket的读写等操作。每当Netty 建立了一个连接后, 都创建一个对应的Channel 实例。除了TCP 协议以外,Netty 还支持很多其他的连接协议, 并且每种协议还有NIO(非阻塞IO)和OIO(Old-IO, 即传统的阻塞IO)版本的区别。不同协议不同的阻塞类型的连接都有不同的Channel 类型与之对应下面是一些常用的Channel类型:
- NioSocketChannel :异步非阻塞的客户端TCP Socket 连接。
- NioServerSocketChannel :异步非阻塞的服务器端TCP Socket 连接
- NioDatagramChannel :异步非阻塞的UDP 连接。
- NioSctpChannel :异步的客户端Sctp(Stream Control Transmission Protocol,流控制传输协议)连接。
- NioSctpServerChannel :异步的Sctp 服务器端连接。
- OioSocketChannel :同步阻塞的客户端TCP Socket 连接。
- OioServerSocketChannel :同步阻塞的服务器端TCP Socket 连接。
- OioDatagramChannel :同步阻塞的UDP 连接。
- OioSctpChannel :同步的Sctp 服务器端连接。
- OioSctpServerChannel: 同步的客户端TCP Socket 连接。
下面我们来看一下Channel 的总体类图:
NioSocketChannel 的创建:
从上面的客户端代码虽然简单, 但是却展示了Netty 客户端初始化时所需的所有内容:
- EventLoopGroup:不论是服务器端还是客户端, 都必须指定EventLoopGroup。在这个例子中, 指定了NioEventLoopGroup, 表示一个NIO 的EventLoopGroup。
- ChannelType: 指定Channel 的类型。因为是客户端,因此使用了NioSocketChannel。
- Handler: 设置处理数据的Handler。
下面我们继续深入代码,看一下客户端通过Bootstrap 启动后,都做了哪些工作?我们看一下NioSocketChannel 的类层次结构如下:
回到我们在客户端连接代码的初始化Bootstrap 中调用了一个channel()方法,传入的参数是NioSocketChannel.class,在这个方法中其实就是初始化了一个ReflectiveChannelFactory 的对象:
- public B channel(Class<? extends C> channelClass) {
- return channelFactory(new ReflectiveChannelFactory<C>(
- ObjectUtil.checkNotNull(channelClass, "channelClass")
- ));
- }
而ReflectiveChannelFactory 实现了ChannelFactory 接口, 它提供了唯一的方法, 即newChannel()方法,ChannelFactory, 顾名思义, 就是创建Channel 的工厂类。进入到ReflectiveChannelFactory 的newChannel()方法中,我们看到其实现代码如下:
- public T newChannel() {
- return constructor.newInstance();
- }
根据上面代码的提示,我们就可以得出:
- Bootstrap 中的ChannelFactory 实现类是ReflectiveChannelFactory。
- 通过channel()方法创建的Channel 具体类型是NioSocketChannel。
Channel 的实例化过程其实就是调用ChannelFactory 的newChannel()方法,而实例化的Channel 具体类型又是和初始化Bootstrap 时传入的channel()方法的参数相关。因此对于客户端的Bootstrap 而言,创建的Channel 实例就是NioSocketChannel。
客户端Channel 的初始化:
前面我们已经知道了如何设置一个Channel 的类型,并且了解到Channel 是通过ChannelFactory 的newChannel()方法来实例化的, 那么ChannelFactory 的newChannel()方法在哪里调用呢?继续跟踪, 我们发现其调用链如下:
在AbstractBootstrap 的initAndRegister()中调用了ChannelFactory()的newChannel()来创建一个NioSocketChannel的实例,其源码如下:
- final ChannelFuture initAndRegister() {
- channel = channelFactory.newChannel();
- init(channel);
- ChannelFuture regFuture = config().group().register(channel);
}
在newChannel()方法中,利用反射机制调用类对象的newInstance()方法来创建一个新的Channel 实例,相当于调用NioSocketChannel 的默认构造器。NioSocketChannel 的默认构造器代码如下:
- public NioSocketChannel() {
- this(DEFAULT_SELECTOR_PROVIDER);
- }
这里的代码比较关键,我们看到,在这个构造器中会调用newSocket()来打开一个新的Java NIO 的SocketChannel:
- private static SocketChannel newSocket(SelectorProvider provider) {
return provider.openSocketChannel();- }
接着会调用父类, 即AbstractNioByteChannel 的构造器,并传入参数parent 为null, ch 为刚才调用newSocket()创建的Java NIO 的SocketChannel 对象, 因此新创建的NioSocketChannel 对象中parent 暂时是null。并且这里会传入一个感兴趣事件是读事件:
- protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
- super(parent, ch, SelectionKey.OP_READ);
- }
然后继续调用父类AbstractChannel 的构造器:
- protected AbstractChannel(Channel parent) {
- this.parent = parent;
- id = newId();
- unsafe = newUnsafe();
- pipeline = newChannelPipeline();
- }
至此, NioSocketChannel 就初始化就完成了, 我们可以稍微总结一下NioSocketChannel 初始化所做的工作内容:
- 调用NioSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER)打开一个新的Java NIOSocketChannel。
- AbstractChannel(Channel parent)中需要初始化的属性:
- id:每个Channel 都拥有一个唯一的id。
- parent:属性置为null。
- unsafe:通过newUnsafe()实例化一个unsafe 对象,它的类型是AbstractNioByteChannel.NioByteUnsafe 内部类。
- pipeline:是new DefaultChannelPipeline(this)新创建的实例。
- AbstractNioChannel 中的属性:
- ch:赋值为Java SocketChannel,即NioSocketChannel 的newSocket()方法返回的Java NIO SocketChannel。
- readInterestOp:赋值为SelectionKey.OP_READ
- ch:被配置为非阻塞,即调用ch.configureBlocking(false)。
- NioSocketChannel 中的属性:
- config = new NioSocketChannelConfig(this, socket.socket())
Unsafe 字段的初始化:
在实例化NioSocketChannel 的过程中,会在父类AbstractChannel 的构造方法中调用newUnsafe()来获取一个unsafe 实例。那么unsafe 是怎么初始化的呢? 它的作用是什么?其实unsafe 特别关键,它封装了对Java 底层Socket 的操作,因此实际上是沟通Netty 上层和Java 底层的重要的桥梁。那么我们下面看一下Unsafe 接口所提供的方法吧:
- 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();
- }
从源码中可以看出, 这些方法其实都是对应到相关的Java 底层的Socket 的操作。继续回到AbstractChannel 的构造方法中,在这里调用了newUnsafe()获取一个新的unsafe 对象,而newUnsafe()方法在NioSocketChannel 中被重写了。来看代码:
- protected AbstractNioUnsafe newUnsafe() {
- return new NioSocketChannelUnsafe();
- }
NioSocketChannel 的newUnsafe()方法会返回一个NioSocketChannelUnsafe 实例。从这里我们就可以确定了,在实例化的NioSocketChannel 中的unsafe 字段,其实是一个NioSocketChannelUnsafe 的实例。
Pipeline 的初始化:
上面我们分析了NioSocketChannel 的大体初始化过程, 但是我们漏掉了一个关键的部分,即ChannelPipeline 的初始化。在Pipeline 的注释说明中写到“Each channel has its own pipeline and it is created automatically when a newchannel is created.”,我们知道,在实例化一个Channel 时,必然都要实例化一个ChannelPipeline。而我们确实在AbstractChannel 的构造器看到了pipeline 字段被初始化为DefaultChannelPipeline 的实例。那么我们就来看一下,:DefaultChannelPipeline 构造器做了哪些工作。
- 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 的构造器需要传入一个channel,而这个channel 其实就是我们实例化的NioSocketChannel,DefaultChannelPipeline 会将这个NioSocketChannel 对象保存在channel 字段中。DefaultChannelPipeline 中还有两个特殊的字段,即head 和tail,这两个字段是双向链表的头和尾。其实在DefaultChannelPipeline 中,维护了一个以AbstractChannelHandlerContext 为节点元素的双向链表,这个链表是Netty 实现Pipeline 机制的关键。关于DefaultChannelPipeline 中的双向链表以及它所起的作用,我们暂不深入,后续再做深入分析。先看看HeadContext跟TailContext的类继承层次结构如下所示:
再来看看二者的构造器:
- HeadContext(DefaultChannelPipeline pipeline) {
- super(pipeline, (EventExecutor)null, DefaultChannelPipeline.HEAD_NAME, false, true);
- this.unsafe = pipeline.channel().unsafe();
- this.setAddComplete();
- }
- TailContext(DefaultChannelPipeline pipeline) {
- super(pipeline, (EventExecutor)null, DefaultChannelPipeline.TAIL_NAME, true, false);
- this.setAddComplete();
- }
我们可以看到,链表中head 实现了ChannelOutboundHandler跟ChannelInboundHandler,它调用了父类AbstractChannelHandlerContext 的构造器,并传入参数inbound = false,outbound = true。而而tail 则是一个ChannelInboundHandler。TailContext 的构造器与HeadContext 的相反,它调用了父类AbstractChannelHandlerContext 的构造器,并传入参数inbound = true,outbound = false。即header 是一个OutBoundHandler,而tail 是一个InBoundHandler。
Channel 注册到Selector:
在前面的分析中,我们提到Channel 会在Bootstrap 的initAndRegister()中进行初始化,但是这个方法还会将初始化好的Channe 注册到NioEventLoop 的selector 中。接下来我们来分析一下Channel 注册的过程。再回顾一下AbstractBootstrap 的initAndRegister()方法:
- final ChannelFuture initAndRegister() {
- channel = channelFactory.newChannel();
- init(channel);
- ChannelFuture regFuture = config().group().register(channel);
- }
当Channel 初始化后,紧接着会调用group().register()方法来向selector 注册Channel。我们继续跟踪的话,会发现其调用链如下:
通过跟踪调用链, 最终我们发现是调用到了unsafe 的register 方法,那么接下来我们就仔细看一下AbstractChannel$AbstractUnsafe.register()方法中到底做了什么?
- public final void register(EventLoop eventLoop, final ChannelPromise promise) {
- AbstractChannel.this.eventLoop = eventLoop;
- register0(promise);
- }
首先,将eventLoop 赋值给Channel 的eventLoop 属性,而我们知道这个eventLoop 对象其实是MultithreadEventLoopGroup 的next()方法获取的,根据我们前面的分析,我们可以确定next()方法返回的eventLoop对象是NioEventLoop 实例。register()方法接着调用了register0()方法,继而调用 doRegister() 方法:
- protected void doRegister() throws Exception {
- boolean selected = false;
- for (;;) {//javaChannel()是channel初始化的时候设置的值
- selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), , this);
- return;
- }
- }
看到javaChannel()这个方法在前面我们已经知道了,它返回的是一个Java NIO 的SocketChannel 对象,这里我们将这个SocketChannel 注册到与eventLoop 关联的selector 上了。我们总结一下Channel 的注册过程:
- 首先在AbstractBootstrap 的initAndRegister()方法中, 通过group().register(channel),调用MultithreadEventLoopGroup 的register()方法。
- 在MultithreadEventLoopGroup 的register()中,调用next()方法获取一个可用的SingleThreadEventLoop, 然后调用它的register()方法。
- 在SingleThreadEventLoop 的register()方法中,调用channel.unsafe().register(this, promise)方法来获取channel 的unsafe()底层操作对象,然后调用unsafe 的register()。
- 在AbstractUnsafe 的register()方法中, 调用register0()方法注册Channel 对象。
- 在AbstractUnsafe 的register0()方法中,调用AbstractNioChannel 的doRegister()方法。
- AbstractNioChannel 的doRegister()方法通过javaChannel().register(eventLoop().selector, 0, this)将Channel对应的Java NIO 的SocketChannel 注册到一个eventLoop 的selector 中,并且将当前Channel 作为attachment 与SocketChannel 关联。
总的来说,Channel 注册过程所做的工作就是将Channel 与对应的EventLoop 关联,因此这也体现了,在Netty中,每个Channel 都会关联一个特定的EventLoop,并且这个Channel 中的所有IO 操作都是在这个EventLoop 中执行的;当关联好Channel 和EventLoop 后,会继续调用底层Java NIO 的SocketChannel 对象的register()方法,将底层Java NIO 的SocketChannel 注册到指定的selector 中。通过这两步,就完成了Netty 对Channel 的注册过程。
Handler 的添加过程:
Netty 有一个强大和灵活之处就是基于Pipeline 的自定义handler 机制。基于此,我们可以像添加插件一样自由组合各种各样的handler 来完成业务逻辑。例如我们需要处理HTTP 数据,那么就可以在pipeline 前添加一个针对HTTP编、解码的Handler,然后接着添加我们自己的业务逻辑的handler,这样网络上的数据流就向通过一个管道一样, 从不同的handler 中流过并进行编、解码,最终在到达我们自定义的handler 中。说到这里,有些小伙伴肯定会好奇,既然这个pipeline 机制是这么的强大,那么它是怎么实现的呢? 我们从简单的入手,先体验一下自定义的handler 是如何以及何时添加到ChannelPipeline中的。后续我回深入分析首先我们看一下如下的用户代码片段:
- .handler(new ChannelInitializer<SocketChannel>() {
- @Override
- protected void initChannel(SocketChannel ch) throws Exception {
- ChannelPipeline pipeline = ch.pipeline();
- pipeline.addLast("frameEncoder", new LengthFieldPrepender());
- pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
- pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
- pipeline.addLast("handler", new MyClient());
- }
- })
这个代码片段就是实现了handler 的添加功能。我们看到,Bootstrap 的handler()方法接收一个ChannelHandler,而我们传的参数是一个派生于抽象类ChannelInitializer 的匿名类,它当然也实现了ChannelHandler 接口。我们来看一下,ChannelInitializer 类内到底有什么玄机:
- public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter {
- protected abstract void initChannel(C ch) throws Exception;
- @Override
- @SuppressWarnings("unchecked")
- public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
- if (initChannel(ctx)) {
- ctx.pipeline().fireChannelRegistered();
- removeState(ctx);
- } else {
- ctx.fireChannelRegistered();
- }
- }private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
- if (initMap.add(ctx)) { // Guard against re-entrance.
- try {
- initChannel((C) ctx.channel());
- } catch (Throwable cause) {
- exceptionCaught(ctx, cause);
- } finally {
- ChannelPipeline pipeline = ctx.pipeline();
- if (pipeline.context(this) != null) {
- pipeline.remove(this);
- }
- }
- return true;
- }
- return false;
- }
- }
ChannelInitializer 是一个抽象类,它有一个抽象的方法initChannel(),我们看到的匿名类正是实现了这个方法,并在这个方法中添加的自定义的handler 的。那么initChannel()是哪里被调用的呢?其实是在ChannelInitializer 的channelRegistered()方法中。接下来关注一下channelRegistered()方法。从上面的源码中,我们可以看到,在channelRegistered()方法中,会调用this.init(channel);方法,将自定义的handler 添加到ChannelPipeline 中,然后调用ctx.pipeline().remove(this)方法将自己从ChannelPipeline 中删除。上面的分析过程,如下图片所示:一开始,ChannelPipeline 中只有三个handler,分别是:head、tail 和我们添加的ChannelInitializer。
接着initChannel()方法调用后,添加了自定义的handler:
最后将ChannelInitializer 删除:
分析到这里,我们已经简单了解了自定义的handler 是如何添加到ChannelPipeline 中的,后续我们再进行深入的探讨。
客户端发起连接请求:
经过上面的各种分析后,我们大致已经了解Netty 客户端初始化时,所做的工作,那么接下来我们就直奔主题,分析一下客户端是如何发起TCP 连接的?首先,客户端通过调用Bootstrap 的connect()方法进行连接。在connect()方法中,会进行一些参数检查后,最终调用的是doConnect()方法,其代码实现如下:
- private static void doConnect(
- final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {
- final Channel channel = connectPromise.channel();
- channel.eventLoop().execute(new Runnable() {
- @Override
- public void run() {
- if (localAddress == null) {
- channel.connect(remoteAddress, connectPromise);
- } else {
- channel.connect(remoteAddress, localAddress, connectPromise);
- }
- connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
- }
- });
- }
在doConnect()方法中,eventLoop 线程中调用Channel 的connect()方法,而这个Channel 的具体类型实际就是NioSocketChannel,前面已经分析过了。继续跟踪到channel.connect()方法中,我们发现它调用的是DefaultChannelPipeline 的connect()方法,pipeline 的connect()方法代码如下:
- public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
- return tail.connect(remoteAddress, promise);
- }
tail 我们已经分析过, 是一个TailContext 的实例,而TailContext 又是AbstractChannelHandlerContext 的子类,并且没有实现connect()方法,因此这里调用的其实是AbstractChannelHandlerContext 的connect()方法,我们看一下这个方法的实现:
- public ChannelFuture connect(
- final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
- 。。。。。
- final AbstractChannelHandlerContext next = findContextOutbound(MASK_CONNECT);
- EventExecutor executor = next.executor();
- if (executor.inEventLoop()) {
- next.invokeConnect(remoteAddress, localAddress, promise);
- } else {
- safeExecute(executor, new Runnable() {
- @Override
- public void run() {
- next.invokeConnect(remoteAddress, localAddress, promise);
- }
- }, promise, null);
- }
- return promise;
- }
上面的代码中有一个关键的地方,即final AbstractChannelHandlerContext next = findContextOutbound(),这里调用findContextOutbound()方法,从DefaultChannelPipeline 内的双向链表的tail 开始,不断向前找到第一个outbound为true 的AbstractChannelHandlerContext,然后调用它的invokeConnect()方法,其代码如下:
- private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
- if (invokeHandler()) {
- try {
- ((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
- } catch (Throwable t) {
- notifyOutboundHandlerException(t, promise);
- }
- } else {
- connect(remoteAddress, localAddress, promise);
- }
- }
前面我们有提到,在DefaultChannelPipeline 的构造器中,实例化了两个对象:head 和tail,并形成了双向链表的头和尾。head 是HeadContext 的实例,它实现了ChannelOutboundHandler 接口,并且它的outbound 设置为true。因此在findContextOutbound()方法中,找到的AbstractChannelHandlerContext 对象其实就是head。进而在invokeConnect()方法中, 我们向上转换为ChannelOutboundHandler 就是没问题的了。而又因为HeadContext 重写了connect()方法,因此实际上调用的是HeadContext 的connect()方法。我们接着跟踪到HeadContext 的connect()方法,看其代码如下:
- public void connect(
- ChannelHandlerContext ctx,
- SocketAddress remoteAddress, SocketAddress localAddress,
- ChannelPromise promise) {
- unsafe.connect(remoteAddress, localAddress, promise);
- }
这个connect()方法很简单,只是调用了unsafe 的connect()方法。回顾一下HeadContext 的构造器, 我们发现这个unsafe 其实就是pipeline.channel().unsafe()返回的Channel 的unsafe 字段。到这里为止,我们应该已经知道, 其实是AbstractNioByteChannel.NioByteUnsafe 内部类兜了一大圈。最后,我们找到创建Socket 连接的关键代码继续跟踪,其实调用的就是AbstractNioUnsafe 的connect()方法:
- public final void connect(
- final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {if (connectPromise != null) {
- // Already a connect in process.
- throw new ConnectionPendingException();
- }
- boolean wasActive = isActive();
- if (doConnect(remoteAddress, localAddress)) {
- fulfillConnectPromise(promise, wasActive);
- } else {
- 。。。
- }
- }
在这个connect()方法中,又调用了doConnect()方法。注意:这个方法并不是AbstractNioUnsafe 的方法,而是AbstractNioChannel 的抽象方法。doConnect()方法是在NioSocketChannel 中实现的,因此进入到NioSocketChannel的doConnect()方法中:
- protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
- boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
- }
我们终于看到的最关键的部分了,首先是获取Java NIO 的SocketChannel,获取NioSocketChannel 的newSocket()返回的SocketChannel 对象;然后调用SocketChannel 的connect()方法完成JavaNIO 底层的Socket 的连接。最后总结一下,客户端BootStrap 发起连接请求的流程可以用如下时序图直观地展示:
服务端ServerBootStrap:
在分析客户端的代码时,我们已经对Bootstrap 启动Netty 有了一个大致的认识,那么接下来分析服务器端时,就会相对简单一些了。首先还是来看一下服务器端的启动代码:
- EventLoopGroup bossGroup = new NioEventLoopGroup();
- EventLoopGroup workerGroup = new NioEventLoopGroup();
- try {
- ServerBootstrap b = new ServerBootstrap();
- b.group(bossGroup, workerGroup)
- .channel(NioServerSocketChannel.class)
- .childHandler(new ChannelInitializer<SocketChannel>() {
- @Override
- protected void initChannel(SocketChannel ch) throws Exception {
- ChannelPipeline pipeline = ch.pipeline();
- //自定义协议解码器
- /** 入参有5个,分别解释如下
- (1) maxFrameLength - 发送的数据包最大长度;
- (2) lengthFieldOffset - 长度域偏移量,指的是长度域位于整个数据包字节数组中的下标;
- (3) lengthFieldLength - 长度域的自己的字节数长度。
- (4) lengthAdjustment – 长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,
- 还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。
- (5) initialBytesToStrip – 丢弃的起始字节数。丢弃处于有效数据前面的字节数量。比如前面有4个节点的长度域,则它的值为4。
- */
- pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, , , , ));
- //自定义协议编码器
- pipeline.addLast(new LengthFieldPrepender());
- //对象参数类型编码器
- pipeline.addLast("encoder",new ObjectEncoder());
- //对象参数类型解码器
- pipeline.addLast("decoder",new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
- pipeline.addLast(new RegistryHandler());
- }
- })
- .option(ChannelOption.SO_BACKLOG, )
- .childOption(ChannelOption.SO_KEEPALIVE, true);
- ChannelFuture future = b.bind(port).sync();
- System.out.println("Wuzz RPC Registry start listen at " + port );
- future.channel().closeFuture().sync();
- } catch (Exception e) {
- bossGroup.shutdownGracefully();
- workerGroup.shutdownGracefully();
- }
服务端基本写法和客户端的代码相比,没有很大的差别,基本上也是进行了如下几个部分的初始化:
- EventLoopGroup:不论是服务器端还是客户端,都必须指定EventLoopGroup。在上面的代码中,指定了NioEventLoopGroup,表示一个NIO 的EventLoopGroup,不过服务器端需要指定两个EventLoopGroup,一个是bossGroup,用于处理客户端的连接请求;另一个是workerGroup,用于处理与各个客户端连接的IO 操作。
- ChannelType: 指定Channel 的类型。因为是服务器端,因此使用了NioServerSocketChannel。
- Handler:设置数据处理器。
bossGroup 与workerGroup:
在客户端的时候,我们初始化了一个EventLoopGroup 对象,而在服务端的初始化时,我们设置了两个EventLoopGroup,一个是bossGroup,另一个是workerGroup。那么这两个EventLoopGroup 都是干什么用的呢? 接下来我们详细探究一下。其实,bossGroup 只用于服务端的accept,也就是用于处理客户端新连接接入请求。我们可以把Netty 比作一个餐馆,bossGroup 就像一个大堂经理,当客户来到餐馆吃时,大堂经理就会引导顾客就坐,为顾客端茶送水等。而workerGroup 就是实际上干活的厨师,它们负责客户端连接通道的IO 操作:当大堂经历接待顾客后,顾客可以稍做休息, 而此时后厨里的厨师们(workerGroup)就开始忙碌地准备饭菜了。关于bossGroup 与workerGroup 的关系,我们可以用如下图来展示:
首先,服务端的bossGroup 不断地监听是否有客户端的连接,当发现有一个新的客户端连接到来时,bossGroup 就会为此连接初始化各项资源,然后从workerGroup 中选出一个EventLoop 绑定到此客户端连接中。那么接下来的服务器与客户端的交互过程就全部在此分配的EventLoop 中完成。口说无凭,我们还是以源码说话吧。首先在ServerBootstrap 初始化时,调用了b.group(bossGroup, workerGroup)设置了两个EventLoopGroup,我们跟踪进去以后会看到:
- public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
- super.group(parentGroup);this.childGroup = childGroup;
- return this;
- }
显然,这个方法初始化了两个字段,一个是group = parentGroup。它是在super.group(parentGroup)中完成初始化的,另一个是childGroup = childGroup。接着从应用程序的启动代码来看调用了b.bind()方法来监听一个本地端口。bind()方法会触发如下调用链:AbstractBootstrap.bind() -> AbstractBootstrap.doBind() -> AbstractBootstrap.initAndRegister()源码看到到这里为止,我们发现AbstractBootstrap 的initAndRegister()方法已经是我们的老朋友了,我们在分析客户端程序时和它打过很多交道,现在再来回顾一下这个方法吧:
- final ChannelFuture initAndRegister() {
- channel = channelFactory.newChannel();
- init(channel);
- ChannelFuture regFuture = config().group().register(channel);
- }
这里group()方法返回的是上面我们提到的bossGroup,而这里的channel 其实就是NioServerSocketChannel 的实例,因此我们可以猜测group().register(channel)将bossGroup 和NioServerSocketChannel 应该就关联起来了。那么workerGroup 具体是在哪里与NioServerSocketChannel 关联的呢?我们继续往下看init(channel)方法:
- void init(Channel channel) throws Exception {
- ChannelPipeline p = channel.pipeline();
- final EventLoopGroup currentChildGroup = childGroup;
- final ChannelHandler currentChildHandler = childHandler;
- // .......
- p.addLast(new ChannelInitializer<Channel>() {
- @Override
- public void initChannel(final Channel ch) throws Exception {
- final ChannelPipeline pipeline = ch.pipeline();
- ChannelHandler handler = config.handler();
- if (handler != null) {
- pipeline.addLast(handler);
- }
- ch.eventLoop().execute(new Runnable() {
- @Override
- public void run() {
- pipeline.addLast(new ServerBootstrapAcceptor(
- ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
- }
- });
- }
- });
- }
实际上init()方法在ServerBootstrap 中被重写了,从上面的代码片段中我们看到,它为pipeline 中添加了一个ChannelInitializer,而这个ChannelInitializer 中添加了一个非常关键的ServerBootstrapAcceptor 的handler。关于handler 的添加与初始化的过程,我们留到之再详细分析。现在,我们来关注一下ServerBootstrapAcceptor类。在ServerBootstrapAcceptor 中重写了channelRead()方法,其主要代码如下:
- public void channelRead(ChannelHandlerContext ctx, Object msg) {
- final Channel child = (Channel) msg;
- child.pipeline().addLast(childHandler);
- setChannelOptions(child, childOptions, logger);
- for (Entry<AttributeKey<?>, Object> e: childAttrs) {
- child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
- }
- try {
- childGroup.register(child).addListener(new ChannelFutureListener() {
- @Override
- public void operationComplete(ChannelFuture future) throws Exception {
- if (!future.isSuccess()) {
- forceClose(child, future.cause());
- }
- }
- });
- } catch (Throwable t) {
- forceClose(child, t);
- }
- }
ServerBootstrapAcceptor 中的childGroup 是构造此对象是传入的currentChildGroup,也就是workerGroup 对象。而这里的Channel 是NioSocketChannel 的实例,因此这里的childGroup 的register()方法就是将workerGroup 中的某个EventLoop 和NioSocketChannel 关联上了。既然如此,那么现在的问题是ServerBootstrapAcceptor 的channelRead()方法是在哪里被调用的呢? 其实当一个client 连接到server 时,Java 底层NIO 的ServerSocketChannel就会有一个SelectionKey.OP_ACCEPT 的事件就绪,接着就会调用到NioServerSocketChannel 的doReadMessages()方法:
- protected int doReadMessages(List<Object> buf) throws Exception {
- SocketChannel ch = SocketUtils.accept(javaChannel());
- }
在doReadMessages()方法中,通过调用javaChannel().accept()方法获取到客户端新连接的SocketChannel 对象,紧接着就实例化一个NioSocketChannel,并且传入NioServerSocketChannel 对象(即this)。由此可知,我们创建的这个NioSocketChannel 的父类Channel 就是NioServerSocketChannel 实例。接下来就经由Netty 的ChannelPipeline 机制,将读取事件逐级发送到各个handler 中,于是就会触发前面我们提到的ServerBootstrapAcceptor 的channelRead()方法。
服务端Selector 事件轮询:
再回到服务端ServerBootStrap 的启动代码,是从bind()方法开始的。ServerBootStrapt 的bind()方法实际上就是其父类AbstractBootstrap 的bind()方法,来看代码:
- private static void doBind0(
- final ChannelFuture regFuture, final Channel channel,
- final SocketAddress localAddress, final ChannelPromise promise) {
- channel.eventLoop().execute(new Runnable() {
- @Override
- public void run() {
- if (regFuture.isSuccess()) {
- channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
- } else {
- promise.setFailure(regFuture.cause());
- }
- }
- });
- }
在doBind0()方法中,调用的是EventLoop 的execute()方法,我们继续跟进去:
- public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
} else {
boolean inEventLoop = this.inEventLoop();
if (inEventLoop) {
this.addTask(task);
} else {
this.startThread();
this.addTask(task);
// .......
}
}
在execute()主要就是创建线程,将线程添加到EventLoop 的无锁化串行任务队列。我们重点关注startThread()方法,继续看源代码:
- private void startThread() {
- doStartThread();
- }
- private void doStartThread() {
executor.execute(new Runnable() {
.....
SingleThreadEventExecutor.this.run();
.......
});
}
我们发现startThread()最终调用的是SingleThreadEventExecutor.this.run()方法,这个this 就是NioEventLoop 对象:
- protected void run() {
- for (;;) {
- try {
- try {
- switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
- case SelectStrategy.CONTINUE:
- continue;
- case SelectStrategy.BUSY_WAIT:
- case SelectStrategy.SELECT:
- select(wakenUp.getAndSet(false));
- if (wakenUp.get()) {
- selector.wakeup();
- }
- default:
- }
- cancelledKeys = ;
- needsToSelectAgain = false;
- final int ioRatio = this.ioRatio;
- if (ioRatio == ) {
- try {
- processSelectedKeys();
- } finally {
- runAllTasks();
- }
......- }
- }
终于看到似曾相识的代码。上面代码主要就是用一个死循环,在不断地轮询SelectionKey.select()方法,主要用来解决JDK 空轮训Bug,而processSelectedKeys()就是针对不同的轮询事件进行处理。如果客户端有数据写入,最终也会调用AbstractNioMessageChannel 的doReadMessages()方法。那么我们先来看一下是哪里调用的,通过追踪processSelectedKeys()方法,最后会调用到NioEventLoop的processSelectedKey 方法:
- private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
- final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
- if (!k.isValid()) {
- final EventLoop eventLoop;
- eventLoop = ch.eventLoop();
- int readyOps = k.readyOps();
- if ((readyOps & SelectionKey.OP_CONNECT) != ) {
- int ops = k.interestOps();
- ops &= ~SelectionKey.OP_CONNECT;
- k.interestOps(ops);
- unsafe.finishConnect();
- }
- if ((readyOps & SelectionKey.OP_WRITE) != ) {
- ch.unsafe().forceFlush();
- }
- if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != || readyOps == ) {
- unsafe.read();
- }
- } catch (CancelledKeyException ignored) {
- unsafe.close(unsafe.voidPromise());
- }
- }
这里可以看到熟悉的代码,当有链接进来的时候,便会走 unsafe.read():这个里面就调用了doReadMessages
- Connect, 即连接事件(TCP 连接), 对应于SelectionKey.OP_CONNECT.int值为16.
- Accept, 即确认事件, 对应于SelectionKey.OP_ACCEPT.int值为8.
- Read, 即读事件, 对应于SelectionKey.OP_READ, 表示 buffer 可读.int值为1
- Write, 即写事件, 对应于SelectionKey.OP_WRITE, 表示 buffer 可写.int值为4
- public void read() {
- int localRead = doReadMessages(readBuf);
- }
总结一下:
- Netty 中Selector 事件轮询是从EventLoop 的execute()方法开始的。
- 在EventLoop 的execute()方法中,会为每一个任务创建一个独立的线程,并保存到无锁化串行任务队列。
- 线程任务队列的每个任务实际调用的是NioEventLoop 的run()方法。
- 在run 方法中调用processSelectedKeys()处理轮询事件。
Netty 解决JDK 空轮训Bug:
各位应该早有耳闻臭名昭著的Java NIO epoll 的bug,它会导致Selector 空轮询,最终导致CPU 100%。官方声称在JDK1.6 版本的update18 修复了该问题,但是直到JDK1.7 版本该问题仍旧存在,只不过该BUG 发生概率降低了一些而已,它并没有被根本解决。出现此Bug 是因为当Selector 的轮询结果为空,也没有wakeup 或新消息处理,则发生空轮询,CPU 使用率达到100%。我们来看下这个问题在issue 中的原始描述:
- This is an issue with poll (and epoll) on Linux. If a file descriptor for a connected socket is polled with a request
- event mask of , and if the connection is abruptly terminated (RST) then the poll wakes up with the POLLHUP (and
- maybe POLLERR) bit set in the returned event set. The implication of this behaviour is that Selector will wakeup and
- as the interest set for the SocketChannel is it means there aren't any selected events and the select method
- returns .
具体解释为:在部分Linux 的2.6 的kernel 中,poll 和epoll 对于突然中断的连接socket 会对返回的eventSet 事件集合置为POLLHUP,也可能是POLLERR,eventSet 事件集合发生了变化,这就可能导致Selector 会被唤醒。这是与操作系统机制有关系的,JDK 虽然仅仅是一个兼容各个操作系统平台的软件,但很遗憾在JDK5 和JDK6 最初的版本中(严格意义上来将,JDK 部分版本都是),这个问题并没有解决,而将这个帽子抛给了操作系统方,这也就是这个bug 最终一直到2013 年才最终修复的原因。在Netty 中最终的解决办法是:创建一个新的Selector,将可用事件重新注册到新的Selector 中来终止空轮训。前面我们有提到select()方法解决了JDK 空轮训的Bug,它到底是如何解决的呢?下面我们来一探究竟,进入select()方法的源码:
- private void select(boolean oldWakenUp) throws IOException {
- Selector selector = this.selector;
- try {
- int selectCnt = ;
- long currentTimeNanos = System.nanoTime();
- long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
- for (;;) {
// .......
int selectedKeys = selector.select(timeoutMillis);- selectCnt ++;
- if (selectedKeys != || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
- break;
- }
- if (Thread.interrupted()) {
- //.....
- selectCnt = ;
- break;
- }
- long time = System.nanoTime();
- if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
- // timeoutMillis elapsed without anything selected.
- selectCnt = ;
- } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > &&
- selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
- selector = selectRebuildSelector(selectCnt);
- selectCnt = ;
- break;
- }
- currentTimeNanos = time;
- }
- }
从上面的代码中可以看出,Selector 每一次轮询都计数selectCnt++,开始轮询会计时赋值给timeoutMillis,轮询完成会计时赋值给time,这两个时间差会有一个时间差,而这个时间差就是每次轮询所消耗的时间。从上面的的逻辑看出,如果每次轮询消耗的时间为0,且重复次数超过512 次,则调用rebuildSelector()方法,即重构Selector。我们跟进到源码中就会发现:
- private void rebuildSelector0() {
- final Selector oldSelector = selector;
- final SelectorTuple newSelectorTuple;
- newSelectorTuple = openSelector();
- // Register all channels to the new Selector.
- int nChannels = ;
- for (SelectionKey key: oldSelector.keys()) {
- Object a = key.attachment();
- try {
- if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
- continue;
- }
- int interestOps = key.interestOps();
- key.cancel();
- SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
- }
在rebuildSelector()方法中,主要做了三件事情:
- 创建一个新的Selector。
- 将原来Selector 中注册的事件全部取消。
- 将可用事件重新注册到新的Selector 中,并激活。
NioServerSocktChannel 的创建:
我们在分析客户端Channel 初始化过程时已经提到,Channel 是对Java 底层Socket 连接的抽象,并且知道了客户端Channel 的具体类型是NioSocketChannel,那么,自然的服务端的Channel 类型就是NioServerSocketChannel 了。通过前面的分析, 我们已经知道了,在客户端中,Channel 类型的指定是在初始化时,通过Bootstrap 的channel()方法设置的,服务端也是同样的方式。再看服务端代码,我们调用了ServerBootstarap 的channel(NioServerSocketChannel.class)方法,传的参数是NioServerSocketChannel.class 对象。如此,按照客户端代码同样的流程,我们可以确定NioServerSocketChannel 的实例化也是通过ReflectiveChannelFactory 工厂类来完成的,而ReflectiveChannelFactory 中的clazz 字段被赋值为NioServerSocketChannel.class,因此当调ReflectiveChannelFactory 的newChannel()方法,就能获取到一个NioServerSocketChannel 的实例。
最后我们也来总结一下:
- ServerBootstrap 中的ChannelFactory 的实现类是ReflectiveChannelFactory 类。
- 创建的Channel 具体类型是NioServerSocketChannel。
Channel 的实例化过程:
其实就是调用ChannelFactory 的newChannel()方法,而实例化的Channel 具体类型就是初始化ServerBootstrap 时传给channel()方法的实参。因此,上面代码案例中的服务端ServerBootstrap, 创建的Channel实例就是NioServerSocketChannel 的实例。
服务端Channel 的初始化:
我们来分析NioServerSocketChannel 的实例化过程,先看一下NioServerSocketChannel 的类层次结构图:
首先,我们来跟踪一下NioServerSocketChannel 的默认构造,和NioSocketChannel 类似,构造器都是调用newSocket()来打开一个Java 的NIO Socket。不过需要注意的是, 客户端的newSocket()方法调用的是openSocketChannel(),而服务端的newSocket()调用的是openServerSocketChannel()。顾名思义,一个是客户端的Java SocketChannel,一个是服务器端的Java ServerSocketChannel,来看代码:
- private static ServerSocketChannel newSocket(SelectorProvider provider) {
- return provider.openServerSocketChannel();
- }
接下来会调用重载构造方法:
- public NioServerSocketChannel(ServerSocketChannel channel) {
- super(null, channel, SelectionKey.OP_ACCEPT);
- config = new NioServerSocketChannelConfig(this, javaChannel().socket());
- }
这个构造方法中,调用父类构造方法时传入的参数是SelectionKey.OP_ACCEPT。作为对比,我们回顾一下,在客户端的Channel 初始化时,传入的参数是SelectionKey.OP_READ。在服务启动后需要监听客户端的连接请求,因此在这里我们设置SelectionKey.OP_ACCEPT,也就是通知selector 我们对客户端的连接请求感兴趣。接着和客户端对比分析一下,会逐级地调用父类的构造器NioServerSocketChannel -> AbstractNioMessageChannel-> AbstractNioChannel -> AbstractChannel。同样的,在AbstractChannel 中实例化一个unsafe 和pipeline:
- protected AbstractChannel(Channel parent) {
- this.parent = parent;
- id = newId();
- unsafe = newUnsafe();
- pipeline = newChannelPipeline();
- }
不过,在这里需要注意的是,客户端的unsafe 是AbstractNioByteChannel#NioByteUnsafe 的实例,而服务端的unsafe是AbstractNioMessageChannel.AbstractNioUnsafe 的实例。因为AbstractNioMessageChannel 重写了newUnsafe()方法,其源代码如下:
- protected AbstractNioUnsafe newUnsafe() {
- return new NioMessageUnsafe();
- }
最后总结一下, 在NioServerSocketChannel 实例化过程中的执行逻辑:
- 调用NioServerSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER)方法打开一个新的Java NIOServerSocketChannel
- AbstractChannel 初始化被赋值是属性:parent:设置为nullunsafe:通过newUnsafe()实例化一个unsafe 对象, 类型是AbstractNioMessageChannel#AbstractNioUnsafepipeline:创建实例DefaultChannelPipeline 实例
- AbstractNioChannel 中被赋值的属性:ch:赋值为Java NIO 的ServerSocketChannel,调用NioServerSocketChannel 的newSocket()方法获取。readInterestOp:默认赋值为SelectionKey.OP_ACCEPT。ch 设置为非阻塞,调用ch.configureBlocking(false)方法。
- NioServerSocketChannel 中被赋值的属性:config = new NioServerSocketChannelConfig(this, javaChannel().socket())
ChannelPipeline 初始化:
服务端ChannelPipeline 的初始化和客户端一致,因此就不再单独分析了。
服务端Channel 注册到Selector:
服务端Channel 的注册过程和客户端一致,也不再单独分析了。
Handler 的添加过程:
服务端handler 的添加过程和客户端的有点区别,跟EventLoopGroup 一样服务端的handler 也有两个:一个是通过handler()方法设置的handler,另一个是通过childHandler()方法设置的childHandler。通过前面的bossGroup 和workerGroup 的分析,其实我们在这里可以大胆地猜测:handler 与accept 过程有关。即handler 负责处理客户端新连接接入的请求;而childHandler 就是负责和客户端连接的IO 交互。那么实际上是不是这样的呢?我们继续用代码来证明。在前面章节我们已经了解ServerBootstrap 重写了init()方法,在这个方法中也添加了handler:
- void init(Channel channel) throws Exception {
- ChannelPipeline p = channel.pipeline();
- final EventLoopGroup currentChildGroup = childGroup;
- final ChannelHandler currentChildHandler = childHandler;
- // ......
- p.addLast(new ChannelInitializer<Channel>() {
- @Override
- public void initChannel(final Channel ch) throws Exception {
- final ChannelPipeline pipeline = ch.pipeline();
- ChannelHandler handler = config.handler();
- if (handler != null) {
- pipeline.addLast(handler);
- }
- ch.eventLoop().execute(new Runnable() {
- @Override
- public void run() {
- pipeline.addLast(new ServerBootstrapAcceptor(
- ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
- }
- });
- }
- });
- }
在上面代码的initChannel()方法中,首先通过handler()方法获取一个handler,如果获取的handler 不为空,则添加到pipeline 中。然后接着,添加了一个ServerBootstrapAcceptor 的实例。那么这里的handler()方法返回的是哪个对象呢? 其实它返回的是handler 字段,而这个字段就是我们在服务器端的启动代码中设置的:那么这个时候, pipeline 中的handler 情况如下:
根据我们原来客户端代码的分析来,我们指定channel 绑定到eventLoop(在这里是指NioServerSocketChannel 绑定到bossGroup)后,会在pipeline 中触发fireChannelRegistered 事件,接着就会触发对ChannelInitializer 的initChannel()方法的调用。因此在绑定完成后,此时的pipeline 的内如下:
在前面我们分析bossGroup 和workerGroup 时,已经知道了ServerBootstrapAcceptor 的channelRead()方法会为新建的Channel 设置handler 并注册到一个eventLoop 中,即:
- public void channelRead(ChannelHandlerContext ctx, Object msg) {
- final Channel child = (Channel) msg;
- child.pipeline().addLast(childHandler);
- setChannelOptions(child, childOptions, logger);
- childGroup.register(child).addListener(new ChannelFutureListener() {
- }
而这里的childHandler 就是我们在服务器端启动代码中设置的handler。后续的步骤我们基本上已经清楚了,当客户端连接Channel 注册后,就会触发ChannelInitializer 的initChannel()方法的调用。最后我们来总结一下服务端handler 与childHandler 的区别与联系:
- 在服务器NioServerSocketChannel 的pipeline 中添加的是handler 与ServerBootstrapAcceptor。
- 当有新的客户端连接请求时,调用ServerBootstrapAcceptor 的channelRead()方法创建此连接的NioSocketChannel 并添加childHandler 到NioSocketChannel 对应的pipeline 中,并将此channel 绑定到workerGroup 中的某个eventLoop 中。
- handler 是在accept 阶段起作用,它处理客户端的连接请求。
- childHandler 是在客户端连接建立以后起作用,它负责客户端连接的IO 交互。
最后来看一张图,加深理解。下图描述了服务端调用流程:
Netty之揭开BootStrap 的神秘面纱的更多相关文章
- Netty(六)揭开 BootStrap 的神秘面纱
6.1 客户端 BootStrap 6.1.1 Channel 简介 在 Netty 中,Channel 是一个 Socket 的抽象,它为用户提供了关于 Socket 状态(是否是连接还是断开)以及 ...
- ASP.NET 运行时详解 揭开请求过程神秘面纱
对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就 ...
- 带你揭开ATM的神秘面纱
相信大家都用过ATM取过money吧,但是有多少人真正是了解ATM的呢?相信除了ATM从业者外了解的人寥寥无几吧,鄙人作为一个从事ATM软件开发的伪专业人士就站在我的角度为大家揭开ATM的神秘面纱吧. ...
- 揭开Future的神秘面纱——结果获取
前言 在前面的两篇博文中,已经介绍利用FutureTask任务的执行流程,以及利用其实现的cancel方法取消任务的情况.本篇就来介绍下,线程任务的结果获取. 系列目录 揭开Future的神秘面纱—— ...
- 揭开Future的神秘面纱——任务执行
前言 此文承接之前的博文 解开Future的神秘面纱之取消任务 补充一些任务执行的一些细节,并从全局介绍程序的运行情况. 系列目录 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱— ...
- 揭开Future的神秘面纱——任务取消
系列目录: 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱——任务执行 揭开Future的神秘面纱——结果获取 使用案例 在之前写过的一篇随笔中已经提到了Future的应用场景和特 ...
- SparkSQL大数据实战:揭开Join的神秘面纱
本文来自 网易云社区 . Join操作是数据库和大数据计算中的高级特性,大多数场景都需要进行复杂的Join操作,本文从原理层面介绍了SparkSQL支持的常见Join算法及其适用场景. Join背景介 ...
- 揭开HTTPS的神秘面纱
摘自:https://www.cnblogs.com/hujingnb/p/11789728.html 揭开HTTPS的神秘面纱 在说HTTP前,一定要先介绍一下HTTP,这家伙应该不用过多说明了 ...
- 揭开Docker的神秘面纱
Docker 相信在飞速发展的今天已经越来越火,它已成为如今各大企业都争相使用的技术.那么Docker 是什么呢?为什么这么多人开始使用Docker? 本节课我们将一起解开Docker的神秘面纱. 本 ...
随机推荐
- Spark 计算人员二度关系
1.一度人脉:双方直接是好友 2.二度人脉:双方有一个以上共同的好友,这时朋友网可以计算出你们有几个共同的好友并且呈现数字给你.你们的关系是: 你->朋友->陌生人 3.三度人脉:即你朋友 ...
- PayPal支付对接
开发时间:2019-04-30 我的目标:在我们公司的海外网站上,接入PayPal支付,美国用户在线完成付款. 准备: (1)准备:公司注册信息(执照,注册号,法人等),法人信息(身份证,住址等) ( ...
- vue 组件间传值方式
/* 父组件给子组件传值 1.父组件调用子组件的时候 绑定动态属性 <v-header :title="title"></v-header> 2.在子组件里 ...
- k8s基本概念
1)Master模块简介: Master是Cluster的大脑,它的主要职责是调度,即决定将应用放在哪里运行.Master运行Linux操作系统,可以是物理机或者虚拟机.为了实现高可用,可以运 ...
- Oracle 数字转为字符串 to_char()
格式:TO_CHAR(number,'format_model') 9 -->Represents a number 0 --> Forces a zero to be displayed ...
- git 使用远程分支覆盖本地分支(重置本地分支)
1 丢弃本地变更 重置为远端分支内容 git reset --hard origin/branchName 如 git reset --hard origin/F_AssetItem
- bzoj1875 [SDOI2009]HH去散步 矩阵快速幂
题目传送门 https://lydsy.com/JudgeOnline/problem.php?id=1875 题解 如果没有这个"不能立刻沿着刚刚走来的路走回",那么这个题就是一 ...
- 51nod 1028 大数乘法 V2 【FFT模板题】
题目链接 模板题.. #include<bits/stdc++.h> using namespace std; typedef int LL; typedef double db; nam ...
- LeetCode--051--N皇后(java)-star
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击. 上图为 8 皇后问题的一种解法. 给定一个整数 n,返回所有不同的 n 皇后问题的解决方案. 每一种解 ...
- python-Exception异常使用
Exception #自定义异常类 ,MyInputExcp继承Exception异常 class MyInputExcp(Exception): def __init__(self, lenght, ...