首页 文章 精选 留言 我的

精选列表

搜索[编译原理],共10000篇文章
优秀的个人博客,低调大师

Netty Reactor模式实现原理详解

在前面的文章中(Reactor模型详解),我们讲解了Reactor模式的各种演变形式,本文主要讲解的则是Netty是如何实现Reactor模式的。这里关于Netty实现的Reactor模式,需要说明的是,其实现的模式如下图所示: 对于Netty使用的Reactor模式,其主要特点如下: 使用一个线程作为mainReactor,专门用于监听客户端的连接事件,当获取到事件之后就将该事件交由Acceptor处理,以获取客户端连接; 在mainReactor获取客户端Channel之后,将其交由subReactor进行处理,这里的subReactor是一个线程池。 subReactor会完成诸如客户端Channel注册,数据读取,业务计算,以及数据写入的工作。这里需要注意的是,对于特定的客户端Channel而言,这一系列处理都是与subReactor中某个特定的线程进行绑定的。 1. 用法示例 关于Netty的用法,这里我们还是使用TimeServer展示其最简单的一个使用示例: public class TimeServer { public void bind(int port) throws InterruptedException { EventLoopGroup bossGroup = new NioEventLoopGroup(); // 声明一个bossGroup作为mainReactor EventLoopGroup workerGroup = new NioEventLoopGroup();// 声明一个workerGroup作为subReactor try { ServerBootstrap bootstrap = new ServerBootstrap(); // 创建服务端启动类 bootstrap.group(bossGroup, workerGroup) // 指定使用的bossGroup和workerGroup .channel(NioServerSocketChannel.class) // 指定使用的Channel连接方式,这里是nio .option(ChannelOption.SO_BACKLOG, 1024)// 设置TCP属性 .handler(new LoggingHandler(LogLevel.INFO)) // 添加公共handler .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeServerHandler()); // 在pipeline中添加自定义handler } }); ChannelFuture future = bootstrap.bind(port).sync(); // 绑定端口并且启动 future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws InterruptedException { new TimeServer().bind(8080); } } 上面示例中,需要说明的有如下几点: 示例首先声明了一个bossGroup和一个workerGroup,这两个group其实就是两个线程池,其每个线程都是使用一个NioEventLoop进行维护的。也就是说每个NioEventLoopGroup其实是由一系列的NioEventLoop组成的,而NioEventLoop的数量默认为“CPU数目 * 2”; bossGroup对应于上面Reactor模式中的mainReactor。但是这里需要理解的是,mainReactor只使用了一个线程进行客户端连接的获取,而这里bossGroup实际上是一个线程池。这主要和Netty启动线程的方式有关,NioEventLoop虽然维护了一个线程,但是其只有在第一次使用的时候才会尝试启动线程。也就是说对于mainReactor,虽然其使用的是一个NioEventLoopGroup,但实际上只有一个ServerSocketChannel与其某个NioEventLoop进行了绑定,而也只有该线程启动了,对于其他的NioEventLoop虽然进行了声明,但是并未启动; 示例中bootstrap.bind(port).sync();其实是整个程序的启动部分,这里的bind()方法会返回一个ChannelFuture对象,该对象类似于Java中的Future对象,会等待线程中任务执行完成才会获取到结果,否则就会进行阻塞性等待。这里调用的sync()方法就是这里的阻塞性方法,也就是说这一行代码的作用主要就是,等待bossGroup和workerGroup绑定指定的端口,完成一些初始化动作,然后主线程才会从sync()方法的等待中唤醒,从而执行后面的代码; future.channel().closeFuture().sync();的主要作用是等待服务器发出关闭命令,其实就是前一行代码将服务器成功绑定到某以端口之后,这里的closeFuture().sync()就会阻塞主线程,以等待服务器的bossGroup和workerGroup发出关闭命令。 2. NioEventLoopGroup线程池的构造 我们首先看一下线程池的构造,这里的构造过程其实比较简单,主要就是创建两个NioEventLoopGroup对象。我们跟踪其构造方法,最后进入了MultithreadEventExecutorGroup的构造方法中,如下是其初始化过程: protected MultithreadEventExecutorGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, Object... args) { if (executor == null) { // 这里如果用户没有设置Executor,则使用这里的ThreadPerTaskExecutor, // 该执行器会在每次接收到一个任务时都使用一个新的线程来执行任务 executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); } // 这里的children就是维护的线程池,nThreads的值就是"CPU数目 * 2" children = new EventExecutor[nThreads]; for (int i = 0; i < nThreads; i ++) { boolean success = false; // 实例化线程池的每个对象,对于NioEventLoopGroup,其返回的就是NioEventLoop对象 children[i] = newChild(executor, args); success = true; } // 这里的chooser其实是一个选择器,用于进行线程池线程的选择 chooser = chooserFactory.newChooser(children); // 为线程池创建一个只读的视图 Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length); Collections.addAll(childrenSet, children); readonlyChildren = Collections.unmodifiableSet(childrenSet); } 这里线程池的构造代码省略了其余的不相关的异常处理部分,而只展示主要的逻辑结构。在构造一个线程池的时候,首先会为每个线程创建一个执行器,该执行器会在每次任务到达时都使用一个新的线程处理当前任务,如下是ThreadPerTaskExecutor的execute()方法: @Override public void execute(Runnable command) { threadFactory.newThread(command).start(); } 在创建完执行器后,就会调用newChild()方法创建线程池中的线程维护对象。该方法是一个抽象方法,具体的实现在NioEventLoopGroup中,实际上就是实例化一个NioEventLoop对象,如下是newChild()方法的实现: @Override protected EventLoop newChild(Executor executor, Object... args) throws Exception { return new NioEventLoop(this, executor, (SelectorProvider) args[0], ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); } 可以看到,这里的NioEventLoop不仅维护了前面实例化的Executor对象,还维护了包括SelectorProvider,SelectStrategyFactory和RejectedExecutionHandler等对象。这里需要说明的是,对于每个EventLoop,Netty都会为其创建一个Selector对象,用于监听Channel的事件,这里的SelectorProvider就是用于创建Selector的。 在NioEventLoop的创建过程中,后面又创建一个了一个EventExecutorChooser类型的chooser对象,该对象的主要作用就是一个EventLoop选择器。关于chooser对象的源码,其实其比较简单,就是通过依次轮换的方式不断的在NioEventLoopGroup中选择NioEventLoop来处理新的channel。如下是chooserFactory.newChooser()的源码: @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; } // 如果当前线程池的数量是2的指数,则使用当前Chooser,该Chooser能够优化选择过程为一个&操作, // 这相对于取余操作会有很大的性能提升 private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser { private final AtomicInteger idx = new AtomicInteger(); private final EventExecutor[] executors; @Override public EventExecutor next() { return executors[idx.getAndIncrement() & executors.length - 1]; } } // 如果线程池的数量不是2的指数,则使用取余的方式进行Chooser的获取 private static final class GenericEventExecutorChooser implements EventExecutorChooser { private final AtomicInteger idx = new AtomicInteger(); private final EventExecutor[] executors; @Override public EventExecutor next() { return executors[Math.abs(idx.getAndIncrement() % executors.length)]; } } 上面的代码中,可以看到,对于Chooser的获取,这里主要就是通过一个idx变量,不断递增,然后在EventLoopGroup中获取指定索引的EventLoop。只不过其会根据线程池是否为2的指数次方来使用不同的计算策略。 这里我们有必要从整体上对线程池的构造过程进行一下说明:可以看到在线程池的构造过程中,Netty创建了一个Executor对象,nThreads个EventLoop对象和一个EventExecutorChooser对象。读者应该注意到了,每一个EventLoop对象在构造的时候都是使用的同一个Executor对象。其实本质上讲,EventLoop实现了Runnable接口,而在run()方法中,其会在一个死循环中不断轮询所注册的事件(获取客户端连接或者读写事件)和执行提交给当前EventLoop的相关任务。这里的Executor其实执行的就是当前EventLoop所代表的任务。前面我们讲到了Executor会在每次新任务到达时新建一个线程来执行任务,而实际上这里的任务就是EventLoop,并且EventLoop在执行每个任务时,除非系统退出,是不会停止其轮询状态的,也就是说Executor创建的线程数最终与EventLoop的数目是一致的。关于EventExecutorChooser,它的与EventLoop的关系在于,每当一个新的channel到达时,chooser就会按照索引递增顺序在NioEventLoopGroup中获取一个EventLoop与当前的Channel进行绑定,而后面所有与该Channel有关的事件都是由该EventLoop的run()方法进行轮询时执行的。 3. NioServerSocketChannel的创建 在前面的示例程序中创建了一个ServerBootstrap的实例,然后调用其gourp(),channel(),option()和childHandler()方法为其设置了相关的属性,这几个方法的调用只是简单的赋值操作,程序的主入口在于ServerBootstrap.bind()方法中,如下是该方法的主要实现: private ChannelFuture doBind(final SocketAddress localAddress) { // 这里initAndRegister()方法的主要作用就是创建一个ServerSocketChannel对象,然后对其进行初始化, // 初始化的内容主要是将ServerBootstrap中的设置的TCP选项和一些属性设置到ServerSocketChannel中; // 接着将用于接收客户端请求的ServerBootstrapAcceptor添加到ServerSocketChannel所维护的 // pipeline中;最后通过ServerSocketChannel所绑定的EventLoop对其进行注册。 final ChannelFuture regFuture = initAndRegister(); final Channel channel = regFuture.channel(); if (regFuture.cause() != null) { return regFuture; } // 走到这里,如果注册已经完成,则调用doBind0()方法将ServerSocketChannel绑定到对应的端口上, // 如果没有注册完成,则添加一个注册完成事件,在事件完成后调用doBind0()方法进行端口绑定。 // 需要注意的是,这里虽然看起来doBind0()方法可能是由当前主线程执行,也可能是由注册完成时所在的 // EventLoop的线程执行,但实际上不是的,doBind0()内部会判断当前是否是在对应的EventLoop中, // 如果在,则直接调用,不在则将绑定定位当做一个任务添加到EventLoop的执行队列中 if (regFuture.isDone()) { ChannelPromise promise = channel.newPromise(); doBind0(regFuture, channel, localAddress, promise); return promise; } else { final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); regFuture.addListener(new ChannelFutureListener() { // 添加注册完成监听事件 @Override public void operationComplete(ChannelFuture future) throws Exception { Throwable cause = future.cause(); if (cause != null) { promise.setFailure(cause); } else { promise.registered(); doBind0(regFuture, channel, localAddress, promise); } } }); return promise; } } 在doBind()方法中,首先会进行ServerSocketChannel的创建,然后会对其进行初始化和注册,这里的注册实际上就是指将当前ServerSocketChannel注册到其所对应的EventLoop所维护的Selector上。在注册完成之后,就会通过doBind0()方法将ServerSocketChannel绑定到目标端口上。可以看出,这里的doBind()方法实际上完成了ServerSocketChannel从初始化到注册,再到绑定,以及事件监听的整个过程。这里我们首先看看其是如何创建,初始化和注册的,如下是initAndRegister()方法的主要逻辑的源码: final ChannelFuture initAndRegister() { Channel channel = channelFactory.newChannel(); init(channel); ChannelFuture regFuture = config().group().register(channel); return regFuture; } 在initAndRegister()方法中,其主流程非常的清晰,首先通过channelFactory创建一个Channel对象,这里的channelFactory是ReflectiveChannelFactory,而创建的Channel对应的就是我们在示例中通过ServerBootstrap.channel()方法添加的Channel类型: public B channel(Class<? extends C> channelClass) { if (channelClass == null) { throw new NullPointerException("channelClass"); } return channelFactory(new ReflectiveChannelFactory<C>(channelClass)); } 这里的ReflectiveChannelFactory所实现的newChannel()方法其实就是通过反射创建一个ServerSocketChannel对象。在得到Channel对象之后,就会调用init()方法对其进行初始化,如下是初始化方法的主要逻辑: @Override void init(Channel channel) throws Exception { ChannelPipeline p = channel.pipeline(); 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()方法中,我们省略了对childOptions和childAttrs属性进行设置的代码,设置过程其实主要就是从ServerBootstrap中转移到这两个属性中,它们最终会在创建客户端Channel的时候将其应用到客户端Channel上。这里我们还是主要看ServerSocketChannel所对应的pipeline的变化过程。上面代码中,可以看到,主要是往pipeline中添加了一个ChannelInitializer对象,该对象的主要作用其实是在注册Channel注册完成之后通过ChannelPipeline.channelRegistered()事件触发时调用其initChannel()方法(也就是这里实现的方法)来完成初始化的。也就是说这里init()方法本质上只是往ServerSocketChannel所对应的pipeline中添加了ChannelInitializer对象,但是其并未执行任何动作。此时我们有必要对pipeline所对应的状态进行一个展示,其现在主要有三个节点: HeadContext-->ChannelInitializer-->TailContext 在初始化完成之后,我们接下来看ServerSocketChannel是如何注册的。注册主要是通过config().group().register(channel);方法调用进行的。对于NioServerSocketChannel,这里的register()最终是调用的NioMessageUnsafe.register()方法。如下是该方法的源码: @Override public final void register(EventLoop eventLoop, final ChannelPromise promise) { if (eventLoop.inEventLoop()) { register0(promise); // 进行注册 } else { eventLoop.execute(new Runnable() { @Override public void run() { register0(promise); // 交由对应的EventLoop进行注册 } }); } } private void register0(ChannelPromise promise) { boolean firstRegistration = neverRegistered; // 标志是否是第一次注册 doRegister(); // 调用Java的ServerSocketChannel进行注册 neverRegistered = false; registered = true; pipeline.invokeHandlerAddedIfNeeded(); // 触发handlerAdded()事件 safeSetSuccess(promise); pipeline.fireChannelRegistered(); // 触发注册完成事件 if (isActive()) { if (firstRegistration) { pipeline.fireChannelActive(); // 注册完成后触发channelActive()事件 } else if (config().isAutoRead()) { beginRead(); } } } 这里register()方法的主要过程其实就是判断当前执行是否在EventLoop中,如果不在,则交由EventLoop执行,最后的注册过程在register0()方法中,在该方法中,首先会调用doRegister()方法进行Java的ServerSocketChannel注册,如下是注册代码: selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); 可以看到,这里才是真正的将创建的Java的ServerSocketChannel进行注册(前面我们所说的Channel其实都是Netty封装的NioServerSocketChannel的执行过程,最终其注册过程是委托到了Java的ServerSocketChannel的注册)。注意,这里注册时监听的事件是0,表示当前只是注册,而不监听任何事件。在注册完成后,调用了pipeline.fireChannelRegistered()方法,我们前面讲到,在pipeline中不仅有头结点和尾节点,还有初始化时添加的ChannelInitializer对象。因而这里就会触发其register()事件,最终就会调用前面实现的initChannel()方法。翻看前面的代码可以看到,这里首先会将config.handler()添加到pipeline中,这个handler()其实就是最开始我们的示例程序中通过ServerBootstrap.handler()方法添加的LoggingHandler,然后创建了一个任务,交由EventLoop执行,该任务中会添加一个ServerBootstrapAcceptor的handler到pipeline中。当initChannel()方法执行完成之后,pipeline的节点结构变成了下面这种情况: HeadContext-->ChannelInitializer-->LoggingHandler-->TailContext 这里要注意的一点是,ChannelInitializer的channelRegister()事件触发时,在调用initChannel()方法之后,会将当前handler,也就是当前ChannelInitializer本身从pipeline中移除。因而当pipeline.fireChannelRegistered();调用完成时,整个pipeline的状态如下: HeadContext-->LoggingHandler-->TailContext 这里需要说明的是,此时我们并没有将ServerBootstrapAcceptor展示在这里的pipeline中。这是因为前面只是将其当做一个任务添加到当前EventLoop的任务队列中的。而当前的register0()本身也是EventLoop需要执行的一个任务,因而该任务的执行是要晚于当前注册的任务的。也就是说如果在注册完成,并且执行了该任务之后,pipeline中的节点状态如下: HeadContext-->LoggingHandler-->ServerBootstrapAcceptor-->TailContext 这里我们其实已经可以看到前面Reactor模式中的Acceptor了,也就是这里的ServerBootstrapAcceptor。它的主要作用也确实时不断监听客户端的连接事件,然后接收客户端连接,并且交由workerGroup进行处理。 我们继续看register0()的注册流程,在触发完注册事件之后,其会判断当前Channel是否处于活跃状态,然后调用pipeline.fireChannelActive();触发channelActive()事件。这里的判断主要目的是为了保证channelActive()事件在Channel解注册之后又进行注册,此时会触发该事件。而正常情况下,channelActive()事件是会在端口绑定完成后触发。 这里其实我们的服务端初始化和注册流程基本已经完成。在整个initAndRegister()方法的执行过程中,主要完成了NioServerSocketChannel的创建,注册,然后主要触发register()事件,并且在事件触发过程中完成了pipeline节点的维护。下面我们来看看NioServerSocketChannel是如何进行事件绑定的,如下前面doBind0()跟踪主要逻辑之后,最终在NioMessageUnsafe中的主要逻辑: @Override public final void bind(final SocketAddress localAddress, final ChannelPromise promise) { boolean wasActive = isActive(); doBind(localAddress); // 进行端口绑定 if (!wasActive && isActive()) { invokeLater(new Runnable() { @Override public void run() { pipeline.fireChannelActive(); } }); } } 这里首先会获取当前Channel的状态,然后调用doBind()方法进行端口绑定,该方法中主要就是进行调用Java的ServerSocketChannel进行绑定,如下是该方法的源码: @Override protected void doBind(SocketAddress localAddress) throws Exception { if (PlatformDependent.javaVersion() >= 7) { javaChannel().bind(localAddress, config.getBacklog()); } else { javaChannel().socket().bind(localAddress, config.getBacklog()); } } 在绑定完成之后,就会判断绑定之前当前Channel是非active的,然后绑定完成后当前Channel是active的,此时,就会触发channelActive()事件,表示当前Channel已经处于活跃状态了。这里需要重点指出的是,上面pipeline中的HeadContext在channelActive()中完成了一定的事件处理。如下是其实现源码: @Override public void channelActive(ChannelHandlerContext ctx) { ctx.fireChannelActive(); // 将channelActive()事件往下传递 readIfIsAutoRead(); // 注册读取事件 } 可以看到,HeadContext首先会将channelActive()事件往下传递。当监听动作完成之后,就会判断当前是否为autoRead,如果是,则触发pipeline的read()事件(这里默认都是autoRead的)。对于read()事件,其实就是通过HeadContext为当前Channel注册其感兴趣的事件,如下是其实现源码: @Override public void read(ChannelHandlerContext ctx) { unsafe.beginRead(); } // NioMessageUnSafe中 @Override public final void beginRead() { if (!isActive()) { return; } doBeginRead(); } @Override protected void doBeginRead() throws Exception { final int interestOps = selectionKey.interestOps(); if ((interestOps & readInterestOp) == 0) { selectionKey.interestOps(interestOps | readInterestOp); } } 可以看到,这里最终就是通过NioMessageUnsafe类进行Channel事件的注册的。这里doBeginRead()方法本质上就是判断当前selectionKey中是否包含有所感兴趣的目标事件,如果不包含,则对其进行设置。这里的目标事件保存在readInterestOp属性中。而该属性则是在创建NioServerSocketChannel和NioSocketChannel对象的不同而传入不同的值。对于NioServerSocketChannel,其传入的是SelectionKey.OP_ACCEPT,而NioSocketChannel传入的则是SelectionKey.OP_READ。具体的代码如下所示: public NioServerSocketChannel(ServerSocketChannel channel) { super(null, channel, SelectionKey.OP_ACCEPT); } protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) { super(parent, ch, SelectionKey.OP_READ); } 如此,NioServerSocketChannel的创建,注册,绑定,以及对监听事件的设置都已经完成。可以看到,虽然只是简简单单的几行代码,但实际上Netty在进行服务端创建时做了非常多的工作。下面我们继续看NioServerSocketChannel是如何接收客户端连接,并且进行处理的。 4. NioSocketChannel连接事件的处理 对于NioSocketChannel连接事件的处理主要是通过NioServerSocketChannel对SelectionKey.OP_ACCEPT事件的监听来进行的。前面我们讲到,每个Channel都会绑定一个NioEventLoop,本质上其是一个Runnable对象,也就是说NioServerSocketChannel绑定的NioEventLoop是通过其run()方法进行事件轮询的。如下是该方法的源码: @Override protected void run() { for (;;) { switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) { case SelectStrategy.CONTINUE: continue; case SelectStrategy.BUSY_WAIT: case SelectStrategy.SELECT: select(wakenUp.getAndSet(false)); // 获取客户端事件 default: } final long ioStartTime = System.nanoTime(); try { processSelectedKeys(); // 处理客户端事件 } finally { final long ioTime = System.nanoTime() - ioStartTime; runAllTasks(ioTime * (100 - ioRatio) / ioRatio); // 执行提交的任务 } } } 在run()方法中,可以看到,其通过一个无限循环,首先不断地监听客户端连接事件,如果有相应的事件,则调用processSelectedKeys()方法处理事件,在处理完事件之后,还会调用runAllTasks()执行所有注册到当前NioEventLoop中的任务,这里的任务其实就是我们前面讲到的诸如注册,绑定,添加handler等任务。这里我们主要看看processSelectedKeys()方法是如何处理客户端事件的,如下是其最终的调用方法: // processSelectedKeys() -> processSelectedKeysOptimized() -> processSelectedKey() private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); int readyOps = k.readyOps(); // 获取当前感兴趣的事件 if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { // 如果是OP_READ或OP_ACCEPT则进行处理 unsafe.read(); } } 这里处理方式其实主要就是首先获取当前Channel监听的事件,如果是OP_READ或OP_ACCEPT,则调用unsafe.read()方法处理。对于NioServerSocketChannel而言,其最终调用的是NioMessageUnsafe.read()方法: @Override public void read() { do { int localRead = doReadMessages(readBuf); if (localRead == 0) { break; } if (localRead < 0) { closed = true; break; } allocHandle.incMessagesRead(localRead); } while (allocHandle.continueReading()); int size = readBuf.size(); for (int i = 0; i < size; i ++) { pipeline.fireChannelRead(readBuf.get(i)); } } 上面代码中,readBuf是一个List<Object>类型的对象。在doReadMessages()方法中,主要就是调用Java的ServerSocketChannel获取客户端连接: @Override protected int doReadMessages(List<Object> buf) throws Exception { SocketChannel ch = SocketUtils.accept(javaChannel()); // 获取客户端连接 if (ch != null) { buf.add(new NioSocketChannel(this, ch)); // 封装为一个NioSocketChannel return 1; } return 0; } 在获取到客户端连接之后,将其存入readBuf中。最后对获取到的每个readBuf对象调用pipeline.fireChannelRead(readBuf.get(i));方法,从而触发其channelRead()事件。前面我们讲到,对于NioServerSocketChannel,其对应的pipeline如下: HeadContext-->LoggingHandler-->ServerBootstrapAcceptor-->TailContext 因而这里最终会触发到ServerBootstrapAcceptor的channelRead()事件,这里我们来看其是如何处理该事件的: @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { final Channel child = (Channel) msg; // 这里获取到的是一个Channel对象 child.pipeline().addLast(childHandler); // 添加handler childGroup.register(child).addListener(new ChannelFutureListener() { // 执行注册 @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { forceClose(child, future.cause()); } } }); } 在channelRead()方法中,这里首先是对传入的msg进行强转,得到一个Channel对象。然后将childHandler添加到pipeline中。这里的childHandler就是最开始我们的示例中通过ServerBootstrap.childHandler()方法设置的handler。我们已经知道,ChannelInitializer会在注册流程中通过触发channelRegister()事件从而得到触发,触发完成之后还会将当前handler从pipeline中移除。上面代码中,接下来就是进行NioSocketChannel的注册,这里的注册逻辑与NioServerSocketChannel的注册逻辑是完全一致的,只是在注册过程中有两点需要说明: NioServerSocketChannel在注册完成之后当前Channel还不是active状态的,因而没有出发channelActive()事件,而是在端口绑定完成之后再触发;而NioSocketChannel在注册完成之后,当前Channel是处于活跃状态的,因而其在注册完成之后会立即触发channelActive()事件,最终完成read()事件的触发,也就是为当前Channel注册感兴趣的事件; 对于感兴趣的事件,前面已经讲到,在构造NioSocketChannel的时候,会传入一个SelectionKey.OP_READ保存起来,如此对于NioSocketChannel而言,其监听的事件就是SelectionKey.OP_READ。 通过触发register()事件,最终就会实现客户端NioSocketChannel的注册以及相应事件的触发。对于客户端NioSocketChannel的数据读取,由于其也是通过绑定的NioEventLoop的不断轮询来实现的,其过程与上面讲的NioServerSocketChannel的轮询代码一致,只是需要注意的是客户端Channel使用的unsafe对象是NioSocketChannelUnsafe,在调用read()方法时需要查阅其是如何读取SocketChannel的数据的,这里不再赘述。 5. 小结 本文从宏观层面上对Netty是如何实现Reactor模式进行了讲解。通过上面的讲解,我们基本上可以将Netty中各个角色与本文一开始的图形进行一一对应: mainReactor对应于这里的NioEventLoopGroup中的一个NioEventLoop,通过该EventLoop的不断轮询来实现客户端连接事件的监听; Acceptor对应于ServerBootstrapAcceptor,在NioEventLoop监听到客户端事件之后,其就会触发ServerBootstrapAcceptor的channelRead()事件,从而获取客户端连接,并且交由workerGroup中的某个NioEventLoop处理; subReactor对应于NioEventLoopGroup,其中包含的每一个NioEventLoop都会与一个客户端Channel对应,并且处理其事件; handler就对应了最后的对于Channel的处理过程,包括read,decode,compute,encode,write。

优秀的个人博客,低调大师

Java 源码解析实战 - ThreadLocal 原理

说起CS游戏,应该是每个中二少年的年少回忆了.游戏开始时,每个人能够领到一把枪,枪把上有三个数字:子弹数、杀敌数、自己的命数,为其设置的初始值分别为1500、0、10. 设战场上的每个人都是一个线程,那么这三个初始值写在哪里呢?如果每个线程都写死这三个值,万一将初始子弹数统一改成 1000发呢?如果共享,那么线程之间的并发修改会导致数据不准确.能不能构造这样一个对象,将这个对象设置为共享变量,统一设置初始值,但是每个线程对这个值的修改都是互相独立的.这个对象就是ThreadLocal 注意不能将其翻译为线程本地化或本地线程英语恰当的名称应该叫作:CopyValueIntoEveryThread 示例代码该示例中,无 set 操作,那么初始值又是如何进入每个线程成为独立拷贝的呢?首先,虽然ThreadLocal在定义时重写了initial

优秀的个人博客,低调大师

Android中Message对象复用原理

Android 中 Message的应用 Message 在Android中主要是在 消息循环机制 中使用,即配合 Handler,Looper和MessageQueue来进行线程切换,线程间传递数据;以及配合Handler在IPC中传递数据; 这里不对这些进行展开,它不是我们关注的重点. 我们在代码中,被建议(网上或者前辈或看注释)用以下的方式来使用 Message,并且被告知,这样会提高性能. Message msg = Message.obtain(); /** * Return a new Message instance from the global pool. Allows us to * avoid allocating new objects in many cases. */ public static Message obtain(); 嗯! 根据官方的文档,这样确实能够提高性能,将避免在大多数情况下创建新的对象! 下面我们来看它是如何实现高效的. Message对象复用的实现 Message能提高效率的原因是,将使用完的Message清除附带的数据后,添加到复用池中,当我们需要使用它时,直接在复用池中取出对象使用,而不需要重新new创建对象. 而复用池本质上就是一个单向链表,为了更好的理解,Message复用池的实现,我们先来看下,简单的单向链表实现. public class Linked<T> { // 链表的头节点 private Node<T> head; // 链表的长度 private int length; // 数据插入头部 public void insertHead(T data) { Node<T> n = new Node<>(); n.data = data; // 将所添加的节点的下一个节点指向头结点 n.next = head; // 将头结点指向党员元素 head = n; // 链表长度加1 length++; } // 移除头部,并取出数据 public T removeHead() { // 拿到原头结点 Node<T> n = head; // 将头结点设置为下一个节点 head = n.next; // 原头节点的下一个节点置空 n.next = null; // 链表长度减1 length--; // 返回原头结点数据 return n.data; } public int getLength() { return length; } // 节点类 static class Node<T> { T data; // 节点存放的元素 Node<T> next; // 下一个节点的引用 } } 上面实现了单向链表的插入头部,和移除头部功能.相信大家,对这种单向链表的实现,都十分熟悉,这里不再详细讲解,如果看不懂,请自行复习数据结构相关知识点. 下面我们带着上面的知识,来看Message中源码的实现. // 只显示我们需要关注的代码 public final class Message implements Parcelable { ... // 下一个节点的引用 Message next; // 这里起到类锁的功能,相当于 Message.class private static final Object sPoolSync = new Object(); // 可以类比为 头结点 private static Message sPool; // 链表的长度 private static int sPoolSize = 0; // 链表的最大长度 private static final int MAX_POOL_SIZE = 50; // 获取Message对象,类比单向链表的removeHead操作 public static Message obtain() { // 同步锁, 这里相当于锁住 Message.class, 起到类锁的作用, 每一个Message实例都是同一把锁 synchronized (sPoolSync) { // 链表中有可复用的Message,直接拿到头结点,然后将头结点指向下一个元素 if (sPool != null) { Message m = sPool; sPool = m.next; m.next = null; m.flags = 0; // clear in-use flag sPoolSize--; return m; } } // 链表中,没有可复用的message,直接new return new Message(); } // 类比单向链表的insertHead操作 void recycleUnchecked() { // 清空数据操作 obj = null; ... // 同步锁 synchronized (sPoolSync) { // 如果链表长度小于50,将当前结点,加入链表头部 if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } } ... } 从上面的分析,我们来梳理一下整体的流程思想. Message 使用一个静态变量,来起到头结点的作用. 静态变量属于类变量,内存中只会存在一份引用,正好能起到 头结点的作用 Message 将自身作为Node节点,存储下一个节点的引用,和自身数据 obtain()方法相当于链表中移除首元素,并返回该元素的操作 从复用池中获取Message避免了 new创建的消耗. recycleUnchecked()方法相当于链表中将节点加入到头部的操作 添加到复用池之前,会先将Message中的数据清空. Message添加了线程安全的操作 Message复用池最多保存50个使用完待复用的Message 50个可能是考虑到内存开销和时间开销的平衡, 不是将数据无限制的添加到复用池. Message将自身作为节点, 使用一个静态变量作为头结点,让Message自身形成一个链表,而链表中存的是 已经清空数据的Message对象, 以这种方式起到复用的效果! 疑问 从代码中可以看出, Message中,将 private static final Object sPoolSync = new Object();作为锁标志,来起到类锁的作用. 它能起到类锁的作用是因为,static修饰的变量在类加载的初始化阶段就将被创建,final使得引用不可改变,从而达到 内存独一份的效果,进而起到和类锁同样的作用. 这里有个疑问, 为何不直接使用类锁来加锁呢? 使用上诉方式,反而需要new一个Object对象,不是增加开销吗? // why not ??? synchronized (Message.class) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } 有答案的同学, 欢迎在评论区中指出! 引用 Android Message 源码

优秀的个人博客,低调大师

RocketMQ原理解析-producer(转)

1,启动流程 Producer如何感知要发送消息的broker(brokerAddrTable中的值是怎么获得的)? producer本地集合中没有,会根据指定topic到namesrv获取TopicPublishInfo,并放入本地集合。 定时从namesrv更新topic路由信息。 Producer与broker间的心跳 Producer定时发送心跳,将producer信息(其实就是procduer的group)定时发送到broker(brokerAddrTable集合中列出的)。 master接收消息,slave拷贝master Producer发送消息,只发送到broker_master机器,通过broker的主从复制机制拷贝到broker_slave。 2,如何发送消息 producer轮询某topic下的所有queue的方式 实现发送方的负载均衡。 Topic下的所有队列如何理解? Broker Topic queue 注册namesrv队列(Topic_A) broker1 Topic_A queue0 , queue1 broker1_queue0 ,broker1_queue1 broker2 Topic_A queue0, queue2, queue3 broker2_queue0,broker2_queue1,broker2_queue2 broker3 Topic_A queue0 broker3_queue0 Producer如何实现轮询队列? Producer从namesrv获得topic_A路由信息TopicPublishInfo。 //Topic_A的所有的队列 private List<MessageQueue> messageQueueList //自增整型 private volatile ThreadLocalIndex sendWhichQueue /** *选择一个发送队列 *lastBrokerName不为空,代表上次选择的queue失败,本次避开同一个queue */ public MessageQueue selectOneMessageQueue(final String lastBrokerName){ //计算队列下标 //sendWhichQueue.getAndIncrement()每次调用+1 Math.abs(sendWhichQueue.getAndIncrement()) % this.messageQueueList.size() } Producer发消息系统重试? //消息发送失败重试次数默认为2 private int retryTimesWhenSendFailed = 2; //发送消息超时时间 private int sendMsgTimeout = 3000; 发送失败,换个队列继续发送所需条件: 1. 重试次数不到retryTimesWhenSendFailed (默认2次) 2. 发送此条消息花费时间还没有到sendMsgTimeout (默认3000毫秒) 3,如何发送顺序消息 4,发送分布式事物消息 5,消息在broker落地之普通消息 6,消息在broker落地之事物消息

优秀的个人博客,低调大师

gcForest算法原理及Python实现

1.背景介绍 从目前来看深度学习大多建立在多层的神经网络基础上,也即一些参数化的多层可微的非线性模块,这样就可以通过后向传播去训练,Zhi-Hua Zhou和Ji Feng在Deep Forest论文中基于不可微的模块建立深度模块,这就是gcForest。 传统的深度学习有一定的弊端: ●超参数个数较多,训练前需要大量初始化,主要靠经验调整,使得DNN更像一门艺术而非科学; ●DNN的训练要求大量的训练数据,数据的标注成本太高; ●DNN是一个黑盒子,训练的结果很难去解释描述,并且学习行为很难用理论去分析解释; ●DNN在训练之前就得确定具体的网络结构,模型的复杂度也是提前设定好的,虽然有一些dropout等的技术。 但是有一点是我们相信的,在处理更复杂的学习问题时,算法的学习模块应该要变的更深,现在的深层网络结构依旧依

优秀的个人博客,低调大师

Drill storage plugin实现原理分析

Drill Storage Plugin介绍 Drill是一个交互式SQL查询引擎,官方默认支持的数据源有hive、hbase、kafka、kudu、mongo、opentsdb、jdbc等,其中jdbc storage plugin可以覆盖所有支持jdbc协议的数据源,如:mysql、oracle等关系型数据库。所有数据源的接入都是通过drill的storage plugin实现的,理论上Drill通过storage plugin机制可以支持对任何数据源进行异构查询。 Drill作为一个SQL查询引擎,它跟传统数据库有着很多相似之处,主要包括SQL Parser、SQL Validator、Query Optimizer、Data Flow Operators等几部分。如下图所示,SQL Parser阶段会把SQL语句解析为SQL查

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册