Netty学习笔记(三)——netty源码剖析
1、Netty启动源码剖析
启动类:
public class NettyNioServer { public static void main(String[] args) throws Exception { /** *创建两个线程组bossGroup和workGroup,bossGroup负责处理请求连接,workGroup负责数据的处理 *两个都是无线循环 *调用可构造方法,默认的字线程数NioEventLoopGroup是实际cpu核数*2 */ EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workGroup = new NioEventLoopGroup(); try{ //创建启动器 ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup,workGroup)//设置两个线程组 .channel(NioServerSocketChannel.class)//使用NioServerSocketChannel作为服务器的通道实现 .option(ChannelOption.SO_BACKLOG,128)//设置线程队列得到的连接数 .childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持活动的连接状态
.handler(new LoggingHandler(LogLevel.INFO)//加入日志 .childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道测试对象 //给pipeline设置处理器 @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new NettyHandelServer());//调用处理器 } }); //启动服务器并绑定端口,绑定端口并同步,创建一个ChannelFuture对象 ChannelFuture channelFuture = bootstrap.bind(7777).sync(); //加监听器 channelFuture.addListener((future)->{ if(channelFuture.isSuccess()){ System.out.println("服务器启动成功"); }else{ System.out.println("服务器启动失败"); } }); //对关闭通道进行监听 channelFuture.channel().closeFuture().sync(); }catch (Exception e){ e.printStackTrace(); }finally {
//关闭 bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } }
调用可构造方法,默认的字线程数NioEventLoopGroup是实际cpu核数*2
看源码:根据debug:我们默认的构造方法不传值,就是默认为0
/** * Create a new instance using the default number of threads, the default {@link ThreadFactory} and * the {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}. */ public NioEventLoopGroup() { this(0); }
我们继续看代码,可以看到这段代码:当nThreads == 0 时,会去获取DEFAULT_EVENT_LOOP_THREADS值,那么看看DEFAULT_EVENT_LOOP_THREADS的值
/** * @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, Object...) */ protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) { super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args); }
DEFAULT_EVENT_LOOP_THREADS的值等于取值中的较大数,NettyRuntime.availableProcessors()获取系统的核数
private static final int DEFAULT_EVENT_LOOP_THREADS; static { DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt( "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); if (logger.isDebugEnabled()) { logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS); } }
所以我这里workGroup的线程数是8。
接下来继续debug,可以来到MultithreadEventExecutorGroup方法,这里创建NioEventLoopGroup对象
/** * Create a new instance. * * @param nThreads the number of threads that will be used by this instance 使用的线程数,默认为core *2 * @param executor the Executor to use, or {@code null} if the default should be used. 执行器:如果传入null,则采用Netty默认的线程工厂和默认的执行器ThreadPerTaskExecutor * @param chooserFactory the {@link EventExecutorChooserFactory} to use. 单例new DefaultEventExecutorChooserFactory() * @param args arguments which will passed to each {@link #newChild(Executor, Object...)} call args在创建执行器的时候传入固定参数 */ protected MultithreadEventExecutorGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, Object... args) { if (nThreads <= 0) { throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads)); } if (executor == null) { executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); }
//private final EventExecutor[] children;
//创建执行器对象,workGroup的nthreads是8
children = new EventExecutor[nThreads];
//循环创建 for (int i = 0; i < nThreads; i ++) { boolean success = false; try {
//传入的执行器默认是ThreadPerTaskExecutor,进入newChild方法,会返回一个NioEventLoopGroup对象
//创建指定的线程数的执行器组 children[i] = newChild(executor, args); success = true; } catch (Exception e) { // TODO: Think about if this is a good exception type throw new IllegalStateException("failed to create a child event loop", e); } finally {
//创建失败,关闭 if (!success) { for (int j = 0; j < i; j ++) { children[j].shutdownGracefully(); } for (int j = 0; j < i; j ++) { EventExecutor e = children[j]; try { while (!e.isTerminated()) { e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); } } catch (InterruptedException interrupted) { // Let the caller handle the interruption. Thread.currentThread().interrupt(); break; } } } } } //创建选择器 chooser = chooserFactory.newChooser(children); final FutureListener<Object> terminationListener = new FutureListener<Object>() { @Override public void operationComplete(Future<Object> future) throws Exception {
//juc下的原子类方法,线程安全 if (terminatedChildren.incrementAndGet() == children.length) { terminationFuture.setSuccess(null); } } }; //给每一个执行器添加监听器 for (EventExecutor e: children) { e.terminationFuture().addListener(terminationListener); }
//将NioEvnetLoopGroup加入链表中 Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length); Collections.addAll(childrenSet, children); readonlyChildren = Collections.unmodifiableSet(childrenSet); }
children[i] = newChild(executor, args);这里返回一个NioEventLoopGroup对象:
@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]); }
接下来看bootstrap启动器:{@link Bootstrap} sub-class which allows easy bootstrap of {@link ServerChannel},用来启动ServerChannel
bootstrap.group()方法,传入两个NioEventGroup,父类交由本类的父类处理,子类该本类中处理
/** * Set the {@link EventLoopGroup} for the parent (acceptor) and the child (client). These * {@link EventLoopGroup}'s are used to handle all the events and IO for {@link ServerChannel} and * {@link Channel}'s. */ public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
//上级父类处理 super.group(parentGroup); if (childGroup == null) { throw new NullPointerException("childGroup"); } if (this.childGroup != null) { throw new IllegalStateException("childGroup set already"); }
//childGroup是EventLoopGroup是用volatile关键字修饰的对象
this.childGroup = childGroup; return this; }
channel()方法,这里返回一个ReflectiveChannelFactory,里面有一个Class对象 class io.netty.channel.socket.nio.NioServerSocketChannel,这时候还没有创建channel
在ReflectiveChannelFactory类中:有一个创建channel的方法,使用java反射进制,通过Class名调用newInstance()方法,那么NioserverSocket对象是什么时候创建的?
@Override public T newChannel() { try { return clazz.newInstance(); } catch (Throwable t) { throw new ChannelException("Unable to create Channel from class " + clazz, t); } }
根据后面debug可知,是在绑定端口的时候bootstrap.bind(7777).sync();
final ChannelFuture initAndRegister() { Channel channel = null; try {
//创建对象并初始化 channel = channelFactory.newChannel(); init(channel); } catch (Throwable t) { if (channel != null) { // channel can be null if newChannel crashed (eg SocketException("too many open files")) channel.unsafe().closeForcibly(); } // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t); } ChannelFuture regFuture = config().group().register(channel); if (regFuture.cause() != null) { if (channel.isRegistered()) { channel.close(); } else { channel.unsafe().closeForcibly(); } } // If we are here and the promise is not failed, it's one of the following cases: // 1) If we attempted registration from the event loop, the registration has been completed at this point. // i.e. It's safe to attempt bind() or connect() now because the channel has been registered. // 2) If we attempted registration from the other thread, the registration request has been successfully // added to the event loop's task queue for later execution. // i.e. It's safe to attempt bind() or connect() now: // because bind() or connect() will be executed *after* the scheduled registration task is executed // because register(), bind(), and connect() are all bound to the same thread. return regFuture; }
option()和childIOption()方法:
public <T> B option(ChannelOption<T> option, T value) { if (option == null) { throw new NullPointerException("option"); } if (value == null) { synchronized (options) { options.remove(option); } } else { synchronized (options) { options.put(option, value); } } return (B) this; }
这个group其实是bossGroup对象,也就是option()方法是给bossGroup设置,那么childGroup应该就是给workGroup设置了,我们debug一下:
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) { if (childOption == null) { throw new NullPointerException("childOption"); } if (value == null) { synchronized (childOptions) { childOptions.remove(childOption); } } else { synchronized (childOptions) { childOptions.put(childOption, value); } } return this; }
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> { private static final InternalLogger logger = InternalLoggerFactory.getInstance(ServerBootstrap.class); private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>(); private final Map<AttributeKey<?>, Object> childAttrs = new LinkedHashMap<AttributeKey<?>, Object>(); private final ServerBootstrapConfig config = new ServerBootstrapConfig(this); private volatile EventLoopGroup childGroup; private volatile ChannelHandler childHandler;
看这里的group的具体对象的确是workGroup对象。
看上面的ServerBootStrapConfig对象的内容:
handler()和childHandler()方法:从上面的 option()和childIOption()方法可以猜到,这两个方法分别作用于bossGroup和workGroup。
handler():
childHandler:
config:
serverBootStrap基本点:
1)链式调用: group方法,将boss和worker传入,boss赋值给parentGroup属性,worker 赋值给childGroup属性
2) channel 方法传入NioServerSocketChannel class对象。会根据这个class 创建channel 对象。
3) option方法传入TCP参数,放在一个LinkedHashMap中。
4) handler 方法传入一个个handler 中,这个hanlder只专属于ServerSocketChannel 而不是SocketChannel
5)childHandler 传入一个hanlder ,这个handler 将会在每个客户端连接的时候调用。供SocketChannel 使用。
bootstrap.bind(7777),绑定端口,这里我们看看这里面做了些什么?
首先验证,validate()和判空
/** * Create a new {@link Channel} and bind it. */ public ChannelFuture bind(SocketAddress localAddress) { validate(); if (localAddress == null) { throw new NullPointerException("localAddress"); } return doBind(localAddress); }
validate()方法也是验空:
public B validate() { if (group == null) { throw new IllegalStateException("group not set"); } if (channelFactory == null) { throw new IllegalStateException("channel or channelFactory not set"); } return (B) this; }
接着继续调用doBind()方法,这里面有几个方法得注意:
private ChannelFuture doBind(final SocketAddress localAddress) { final ChannelFuture regFuture = initAndRegister(); final Channel channel = regFuture.channel(); if (regFuture.cause() != null) { return regFuture; } if (regFuture.isDone()) { // At this point we know that the registration was complete and successful. ChannelPromise promise = channel.newPromise(); doBind0(regFuture, channel, localAddress, promise); return promise; } else { // Registration future is almost always fulfilled already, but just in case it's not. 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) { // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an // IllegalStateException once we try to access the EventLoop of the Channel. promise.setFailure(cause); } else { // Registration was successful, so set the correct executor to use. // See https://github.com/netty/netty/issues/2586 promise.registered(); doBind0(regFuture, channel, localAddress, promise); } } }); return promise; } }
先是调用initAndRegister(),这里用来创建NioServerSocketChannel对象并初始化,前面说过是通过channelFactory.newChannel(),通过java反射技术实现。
final ChannelFuture initAndRegister() { Channel channel = null; try { channel = channelFactory.newChannel(); init(channel); } catch (Throwable t) { if (channel != null) { // channel can be null if newChannel crashed (eg SocketException("too many open files")) channel.unsafe().closeForcibly(); } // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t); } ChannelFuture regFuture = config().group().register(channel); if (regFuture.cause() != null) { if (channel.isRegistered()) { channel.close(); } else { channel.unsafe().closeForcibly(); } } // If we are here and the promise is not failed, it's one of the following cases: // 1) If we attempted registration from the event loop, the registration has been completed at this point. // i.e. It's safe to attempt bind() or connect() now because the channel has been registered. // 2) If we attempted registration from the other thread, the registration request has been successfully // added to the event loop's task queue for later execution. // i.e. It's safe to attempt bind() or connect() now: // because bind() or connect() will be executed *after* the scheduled registration task is executed // because register(), bind(), and connect() are all bound to the same thread. return regFuture; }
init()方法:这里面来初始化NioServerSocketChannel
(1)设置NioServerSocketChannel的TCP属性。
(2)由于LinkedHashMap 是非线程安全的,使用同步进行处理。
(3)对NioServerSocketChannel的ChannelPipeline 添加Channellnitializer 处理器。
(4)可以看出,init 的方法的核心作用在和ChannelPipeline 相关。
(5)从NioServerSocketChannel的初始化过程中,我们知道,pipeline 是一个双向链表,并
且,他本身就初始化了head和tail, 这里调用了他的addLast 方法,也就是将整个handler 插入到tail 的
前面,因为tail 永远会在后面,需要做一些系统的固定工作。
@Override void init(Channel channel) throws Exception { final Map<ChannelOption<?>, Object> options = options0(); synchronized (options) { setChannelOptions(channel, options, logger); } final Map<AttributeKey<?>, Object> attrs = attrs0(); synchronized (attrs) { for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) { @SuppressWarnings("unchecked") AttributeKey<Object> key = (AttributeKey<Object>) e.getKey(); channel.attr(key).set(e.getValue()); } } ChannelPipeline p = channel.pipeline(); final EventLoopGroup currentChildGroup = childGroup; final ChannelHandler currentChildHandler = childHandler; final Entry<ChannelOption<?>, Object>[] currentChildOptions; final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
synchronized (childOptions) { currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size())); } synchronized (childAttrs) { currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size())); }
//添加handler 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)); } }); } }); }
addLast会追踪到这里:我们知道pipeline 是一个双向链表,并且初始化了head和tail,这里调用addLast()方法,是在tail前面插入。
而tail会在尾部做一些系统的固定操作
private void addLast0(AbstractChannelHandlerContext newCtx) { AbstractChannelHandlerContext prev = tail.prev; newCtx.prev = prev; newCtx.next = tail; prev.next = newCtx; tail.prev = newCtx; }
这里是将一个handler加入到链表尾部的前一个节点:
示意图:
在 doBind()中会去调用doBind0(regFuture, channel, localAddress, promise)方法,最终会调用NioServerSocketChannel中的doBind()方法
方法了。到此整个启动过程已经结束了,ok了
@Override protected void doBind(SocketAddress localAddress) throws Exception { if (PlatformDependent.javaVersion() >= 7) { javaChannel().bind(localAddress, config.getBacklog()); } else { javaChannel().socket().bind(localAddress, config.getBacklog()); } }
这是java nio中的底层方法,在nio中我们绑定网络端口是这样:
//绑定网络端口 serverSocketChannel.socket().bind(new InetSocketAddress(666));
在绑定网端口后,4.7回到bind方法(alt+v), 最后-一步: safeSetSuccess(promise), 告诉promise 任务成功了。其可以执行监听器的方法,
这时候启动已经成功,再继续debug,我们会进入NioEventLoop类的run()方法,这里面进行监听
@Override protected void run() { for (;;) { try { switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) { case SelectStrategy.CONTINUE: continue; case SelectStrategy.SELECT: select(wakenUp.getAndSet(false)); if (wakenUp.get()) { selector.wakeup(); } default: // fallthrough } cancelledKeys = 0; needsToSelectAgain = false; final int ioRatio = this.ioRatio; if (ioRatio == 100) { try {
processSelectedKeys(); } finally { // Ensure we always run tasks. runAllTasks(); } } else { final long ioStartTime = System.nanoTime(); try { processSelectedKeys(); } finally { // Ensure we always run tasks. final long ioTime = System.nanoTime() - ioStartTime; runAllTasks(ioTime * (100 - ioRatio) / ioRatio); } } } catch (Throwable t) { handleLoopException(t); } // Always handle shutdown even if the loop processing threw an exception. try { if (isShuttingDown()) { closeAll(); if (confirmShutdown()) { return; } } } catch (Throwable t) { handleLoopException(t); } } }
在上次讲得关于Netty的线程模式中:这里面有一个循环的执行过程图解,是和上面的代码对应的。
Netty 启动过程小结:
1) 创建2个EventLoopGroup 线程池数组。数组默认大小CPU核数*2, 方便chooser选择线程池时提高性能 2) BootStrap 将boss 设置为group 属性,将worker 设置为childer 属性 3)通过bind 方法启动,内部重要方法为initAndRegister 和dobind 方法 4) initAndRegister 方法会反射创建NioServerSocketChannel 及其相关的NIO的对象,pipeline ,unsafe, 同时也为pipeline 初始了head 节点和tail 节点。 5)在register0方法成功以后调用在dobind 方法中调用doBind0 方法,该方法会调用NioServerSocketChannel 的doBind 方法对JDK的channel 和端口进行绑定,完成Netty 服务器的所有启动,并在NioEventLoop中开始监听连接事件 NioEventLooph中有三个重要的方法:select、processSelectedKey和runAllTasks方法。
2、Netty接受请求过程源码剖析
在上面服务器启动的时候我们知道在NioEventLoop中,会有一个事件循环监听事件的发生,我们debug启动服务端,
用浏览器连接服务器,debug到NioEventLoop中的processSelectedKey方法,这里是来监听事件
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); if (!k.isValid()) { final EventLoop eventLoop; try { eventLoop = ch.eventLoop(); } catch (Throwable ignored) { // If the channel implementation throws an exception because there is no event loop, we ignore this // because we are only trying to determine if ch is registered to this event loop and thus has authority // to close ch. return; } // Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop // and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is // still healthy and should not be closed. // See https://github.com/netty/netty/issues/5125 if (eventLoop != this || eventLoop == null) { return; } // close the channel if the key is not valid anymore unsafe.close(unsafe.voidPromise()); return; } try { int readyOps = k.readyOps(); // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise // the NIO JDK channel implementation may throw a NotYetConnectedException. if ((readyOps & SelectionKey.OP_CONNECT) != 0) { // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking // See https://github.com/netty/netty/issues/924 int ops = k.interestOps(); ops &= ~SelectionKey.OP_CONNECT; k.interestOps(ops); unsafe.finishConnect(); } // Process OP_WRITE first as we may be able to write some queued buffers and so free memory. if ((readyOps & SelectionKey.OP_WRITE) != 0) { // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write ch.unsafe().forceFlush(); } // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead // to a spin loop if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { unsafe.read(); } } catch (CancelledKeyException ignored) { unsafe.close(unsafe.voidPromise()); } }
再看unsafe.read()方法
@Override public void read() { assert eventLoop().inEventLoop(); final ChannelConfig config = config(); final ChannelPipeline pipeline = pipeline(); final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle(); allocHandle.reset(config); boolean closed = false; Throwable exception = null; try { try { do { int localRead = doReadMessages(readBuf); if (localRead == 0) { break; } if (localRead < 0) { closed = true; break; } allocHandle.incMessagesRead(localRead); } while (allocHandle.continueReading()); } catch (Throwable t) { exception = t; } int size = readBuf.size(); for (int i = 0; i < size; i ++) { readPending = false; pipeline.fireChannelRead(readBuf.get(i)); } readBuf.clear(); allocHandle.readComplete(); pipeline.fireChannelReadComplete(); if (exception != null) { closed = closeOnReadError(exception); pipeline.fireExceptionCaught(exception); } if (closed) { inputShutdown = true; if (isOpen()) { close(voidPromise()); } } } finally { // Check if there is a readPending which was not processed yet. // This could be for two reasons: // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method // // See https://github.com/netty/netty/issues/2254 if (!readPending && !config.isAutoRead()) { removeReadOp(); } } } }
进入doReadMessages()方法
@Override protected int doReadMessages(List<Object> buf) throws Exception { SocketChannel ch = SocketUtils.accept(javaChannel()); try {
if (ch != null) { buf.add(new NioSocketChannel(this, ch)); return 1; } } catch (Throwable t) { logger.warn("Failed to create a new channel from an accepted socket.", t); try { ch.close(); } catch (Throwable t2) { logger.warn("Failed to close a socket.", t2); } } return 0; }
在nio中,我们是通过serverSocketChannel产生新的socketChannel对象,获取tcp连接,在netty中通过SocketChannel ch = SocketUtils.accept(javaChannel());获得。
//每次连接一个客户端都会产生新的SocketChannel SocketChannel socketChannel = serverSocketChannel.accept();
buf.add(new NioSocketChannel(this, ch));获取到一个JDK的SocketChannel, 然后,使用NioSocketChannel 进行封装。最后添加到容器中,这样容器中就有了SocketChannel对象
再来看看unsafe.read()下的fireChannelRead()方法:
readBuf里面有一个实例对象NioSocketChannel
int size = readBuf.size(); for (int i = 0; i < size; i ++) { readPending = false; pipeline.fireChannelRead(readBuf.get(i)); }
追踪fireChannelRead方法到invokeChannelRead()方法,这里有一个channelRead()方法。
private void invokeChannelRead(Object msg) { if (invokeHandler()) { try { ((ChannelInboundHandler) handler()).channelRead(this, msg); } catch (Throwable t) { notifyHandlerException(t); } } else { fireChannelRead(msg); } }
debug我们会发现会执行几次这个方法,这个时候的pipeline有四个handler:分别是Head, LoggingHandler, ServerBootstrapAcceptor, Tail。
主要看ServerBootstrapAcceptor这个handler调用的channelRead()方法
@Override @SuppressWarnings("unchecked") 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 {
//将客户端连接注册到workGroup中,并添加监听器 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); } }
上述源码总结几点:
1) msg强转成Channel ,实际上就是NioSocketChannel 。
2)添加NioSocketChannel 的pipeline 的handler ,就是我们main方法里面设置的childHandler 方法里的
3)设置NioSocketChannel的各种属性。
4)将该NioSocketChannel注册到childGroup 中的一个EventLoop. 上, 并添加一个监听器。
5)这个childGroup就是我们main方法创建的数组workerGroup.
继续追register,这里的next方法会返回一个EventExecutor 执行器
@Override public ChannelFuture register(Channel channel) { return next().register(channel); }
@Override public EventExecutor next() {
//与运算,idx是一个原子类,线程安全 return executors[idx.getAndIncrement() & executors.length - 1]; }
最后追到AbstractNioChannel的doBeginRead方法:
@Override protected void doBeginRead() throws Exception { // Channel.read() or ChannelHandlerContext.read() was called
//获取对应的selectKey
final SelectionKey selectionKey = this.selectionKey; if (!selectionKey.isValid()) { return; } readPending = true; final int interestOps = selectionKey.interestOps(); if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp); } }
netty接受请求流程小结:
总体流程:接受连接--->创建- -个新的NioSocketCh-n------_>注册到- -个worker EventLoop上----->注册selecot Read事件。 1)服务器轮询Accept 事件,获取事件后调用unsafe 的read 方法,这个unsafe 是ServerSocket 的内部类,该方法内部由2部分组成 2) doReadMessages 用于创建NioSocketChannel 对象,该对象包装JDK的Nio Channel客户端。该方法会像创建ServerSocketChanel类似创建相关的pipeline ,unsafe, config 3)随后执行执行pipeline.fireChannelRead 方法,并将自己绑定到一个chooser选择器选择的workerGroup 中的一个EventLoop。 并且注册一个0,表示注册成功,但并没有注册读(1) 事件
3、Pipeline Handler HandlerContext创建源码剖析
1. ChannelPipeline| ChannelHandler| ChannelHandlerContext 介绍
1.1 三者关系
1) 每当ServerSocket 创建一-个新的连接,就会创建一个Socket, 对应的就是目标客户端。
2)每一个新创建的Socket 都将会分配一个全新的ChannelPipeline
3)每一个ChannelPipeline 内部都含有多个ChannelHandlerContext
4)他们一起组成了双向链表,这些Context 用于包装我们调用addLast 方法时添加的ChannelHandler
当一个请求进来的时候,会进入Socket对应的pipeline, 并经过pipeline 所有的handler, 就是设计模式中的过滤器模式。
ChannelPipeline:
接口继承关系:
通过ChannelPipeline的接口继续关系和该接口的方法:我们可以知道
可以看到该接口继承了inBound, outBound, Iterable 接口,表示他可以调用数据出站的方法和入站的方法,同时
也能遍历内部的链表, 看看他的几个代表性的方法,基本上都是针对handler 链表的插入,追加,删除,替换操
作,类似是一个LinkedList。 同时,也能返回channel (也就是socket)
示意图:
常用方法:
public interface ChannelHandler { //当把ChannelHandler 添加到pipeline 时被调用 void handlerAdded(ChannelHandlerContext ctx) throws Exception; //当从pipeline 中移除时调用 void handlerRemoved(ChannelHandlerContext ctx) throws Exception; //当处理过程中在pipeline 发生异常时调用 @Deprecated void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; }
ChannelHandler:
ChannelHandler 的作用就是处理I0事件或拦截I0事件,并将其转发给下一个处理程序ChannelHandler.
Handler处理事件时分入站和出站的,两个方向的操作都是不同的,因此,Netty 定义了两个子接口继承
ChannelHandler:ChannelInboundHandler和ChannelOutboundHandler
ChannelInboundHandle接口方法:
* channelActive用于当Channel 处于活动状态时被调用;
* channelRead 当从Channel 读取数据时被调用等等方法。
*我们一般需要重写一些方法,当发生关注的事件,需要在方法中实现我们的业务逻辑,因为当事件发生时,Netty会
回调对应的方法。
ChannelOutboundHandler接口方法:
* bind方法,当请求将Channel 绑定到本地地址时调用
* close方法,当请求关闭Channel 时调用等等
出站操作都是一些连接和写出数据类似的方法。
还有一个接口:ChannelDuplexHandler可以同时处理入站和出站的事件,不建议使用。
继承关系图:
方法:
ChannelHandlerContext :
继承关系图:
ChannelHandlerContext继承了出站方法调用接口和入站方法调用接口
ChannelInboundInvoker方法:
ChannelOutboundInvoker方法:
*这两个invoker就是针对入站或出站方法来的,就是在入站或出站handler 的外层再包装一层,达到在方法前
后拦截并做一些特定操作的目的
ChannelHandlerContext方法:
* ChannelHandlerContext 不仅仅时继承了他们两个的方法,同时也定义了一些自己的方法
这些方法能够获取Context. 上下文环境中对应的比如channel, executor, handler ,pipeline, 内存分配器,关
联的handler 是否被删除。
Context就是包装了handler 相关的一切,以方便Context 可以在pipeline 方便的操作handler
ChannelPipeline| ChannelHandler| ChannelHandlerContext 创建过程:
Socket 创建的时候创建pipeline在SocketChannel的抽象父类AbstractChannel的构造方法中
protected AbstractChannel(Channel parent) { this.parent = parent; id = newId(); unsafe = newUnsafe(); pipeline = newChannelPipeline(); }
调用newChannelPipeline()方法,然后追踪到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; }
1》将channel 赋值给channel字段,用于pipeline 操作channel。
2》创建一个future 和promise, 用于异步回调使用。
3》创建一个inbound 的tailContext, 创建一个既是inbound 类型又是outbound 类型的headContext.
4》最后,将两个Context 互相连接,形成双向链表。
5) tailContext和HeadContext 非常的重要,所有pipeline 中的事件都会流经他们,
TailContext:是DefaultChannelPipeline的内部类
final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
HeadContext:是DefaultChannelPipeline的内部类,它实现了ChannelOutboundHandler, ChannelInboundHandler,所以可以处理出站和入站的事件。
final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler {
Context的创建:
debug定位到DefaultChannelPipeline的addLast()方法:
@Override public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) { final AbstractChannelHandlerContext newCtx; synchronized (this) { checkMultiplicity(handler); //创建一个context newCtx = newContext(group, filterName(name, handler), handler); addLast0(newCtx); // If the registered is false it means that the channel was not registered on an eventloop yet. // In this case we add the context to the pipeline and add a task that will call // ChannelHandler.handlerAdded(...) once the channel is registered. if (!registered) { newCtx.setAddPending(); callHandlerCallbackLater(newCtx, true); return this; } EventExecutor executor = newCtx.executor(); if (!executor.inEventLoop()) { newCtx.setAddPending(); executor.execute(new Runnable() { @Override public void run() { callHandlerAdded0(newCtx); } }); return this; } } callHandlerAdded0(newCtx); return this; }
说明:
1) pipeline 添加handler, 参数是线程池,name 是null,handler 是我们或者系统传入的handler。Netty 为了防止
多个线程导致安全问题,同步了这段代码,步骤如下:
2)检查这 个handler 实例是否是共享的,如果不是,并且已经被别的pipeline 使用了,则抛出异常。
3)调用newContext(group, filterName(name, handler), handler)方法,创建-一个Context。 从这里可以看出来了,
每次添加一个handler 都会创建-一个关联Context.
4)调用addLast方法,将Context 追加到链表中。
5)如果这个通道还没有注册到selecor. 上,就将这个Context添加到这个pipeline 的待办任务中。当注册好了以
后,就会调用callHandlerAdded0 方法(默认是什么都不做,用户可以实现这个方法)。
6)到这里,针对三对象创建过程,了解的差不多了,和最初说的一样,每当创建ChannelSocket 的时候都会创建
一个绑定的pipeline,一对一的关系,创建pipeline 的时候也会创建tail 节点和head 节点,形成最初的链表。tail
是入站inbound 类型的handler, head 既是inbound 也是outbound 类型的handler。 在调用pipeline 的addLast
方法的时候,会根据给定的handler 创建一个Context, 然后,将这个Context 插入到链表的尾端(tail 前面)。
到此就OK了
ChannelPipeline| ChannelHandler| ChannelHandlerContext 创建过程小结:
1)每当创建ChannelSocket的时候都会创建一个绑定的pipeline,一对--的关系,创建pipeline的时候也会创建 tail节点和head节点,形成最初的链表。 2)在调用pipeline 的addLast 方法的时候,会根据给定的handler 创建-一个 Context, 然后,将这个Context 插 入到链表的尾端(tail 前面)。 3) Context 包装handler, 多个Context 在pipeline 中形成了双向链表 4)入站方向叫 inbound, 由head 节点开始,出站方法叫outbound ,由tail 节点开始
4、channelPipelne调度handler的源码剖析:
当一个请求进来的时候channelPipeline如何调用handler?在前面我们知道服务器启动的时候会事件循环一直监听着
客户端的请求,最终会调用NioEventLoop中的processSelectedKey方法,部分源码:
int readyOps = k.readyOps(); // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise // the NIO JDK channel implementation may throw a NotYetConnectedException. if ((readyOps & SelectionKey.OP_CONNECT) != 0) { // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking // See https://github.com/netty/netty/issues/924 int ops = k.interestOps(); ops &= ~SelectionKey.OP_CONNECT; k.interestOps(ops); unsafe.finishConnect(); } // Process OP_WRITE first as we may be able to write some queued buffers and so free memory. if ((readyOps & SelectionKey.OP_WRITE) != 0) { // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write ch.unsafe().forceFlush(); } // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead // to a spin loop if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { unsafe.read(); }
unsafe.read();会去调用AbstractNioMessageChannel的read方法:
int size = readBuf.size(); for (int i = 0; i < size; i ++) { readPending = false; pipeline.fireChannelRead(readBuf.get(i)); }
这里的pipeLine实际上是DefaultChannelPipeline
DefaultChannelPipeline有很多和inbound相对应的方法:
ChannelInboundHandler:
pipeline.fireChannelRead()方法会去调用AbstractChannelHandlerContext的invokeChannelRead(),先获取执行器,判断,再去执行invokeChannelRead()
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeChannelRead(m); } else { executor.execute(new Runnable() { @Override public void run() { next.invokeChannelRead(m); } }); } }
next.invokeChannelRead(m);这里会调用真正的handler的channelRead()方法
private void invokeChannelRead(Object msg) { if (invokeHandler()) { try { ((ChannelInboundHandler) handler()).channelRead(this, msg); } catch (Throwable t) { notifyHandlerException(t); } } else { fireChannelRead(msg); } }
例如我这里的handler是LoggingHandler:
说明:
可以看出来,这些方法都是inbound 的方法,也就是入站事件,调用静态方法传入的也是inbound 的类型head
handler。 这些静态方法则会调用head 的ChannelInboundInvoker 接口的方法,再然后调用handler 的真正方
法
DefaultChannelPipeline有很多和Outbound相对应的方法:
ChannelOutboundHandler:
说明:
1)这些都是出站的实现,但是调用的是outbound 类型的tail handler来进行处理,因为这些都是outbound 事
2)出站是tail 开始,入站从head 开始。因为出站是从内部向外面写,从tail开始,能够让前面的handler 进
行处理,防止handler 被遗漏,比如编码。反之,入站当然是从head 往内部输入,让后面的handler 能够处理这
些输入的数据。比如解码。因此虽然head 也实现了outbound 接口,但不是从head 开始执行出站任务
调度示意图:
这里面一个循环调度
说明:
1) pipeline 首先会调用Context 的静态方法fireXXX, 并传入Context
2)然后,静态方法调用Context 的invoker 方法,而invoker 方法内部会调用该Context 所包含的
Handler的真正的XXX方法,调用结束后,如果还需要继续向后传递,就调用Context 的fireXXX2 方法,循环
往复。
源码演示:
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeChannelRead(m); } else { executor.execute(new Runnable() { @Override public void run() { next.invokeChannelRead(m); } }); } }
↓
private void invokeChannelRead(Object msg) { if (invokeHandler()) { try { ((ChannelInboundHandler) handler()).channelRead(this, msg); } catch (Throwable t) { notifyHandlerException(t); } } else { fireChannelRead(msg); } }
↓
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ctx.fireChannelRead(msg); }
↓
@Override public ChannelHandlerContext fireChannelRead(final Object msg) { invokeChannelRead(findContextInbound(), msg); return this; }
↓
private AbstractChannelHandlerContext findContextInbound() { AbstractChannelHandlerContext ctx = this; do { ctx = ctx.next; } while (!ctx.inbound); return ctx; }
↓
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeChannelRead(m); } else { executor.execute(new Runnable() { @Override public void run() { next.invokeChannelRead(m); } }); } }
→
channelPipelne调度handler调度小结:
1) Context 包装handler, 多个Context 在pipeline 中形成了双向链表,入站方向叫inbound, 由head节点开始,出站方法叫outbound ,由tail 节点开始。
2)而节点中间的传递通过AbstractChannelHandlerContext 类内部的fire 系列方法,找到当前节点的下一个节点不断的循环传播。是一个过滤器形式完成对handler的调度
5、Netty心跳源码剖析:
心跳机制heartbeat,通过心跳检查对方是否有效,这是RPC框架中是必不可少的功能。下面我们分析下Netty 内部源码实现
Netty提供了IdleStateHandler ,ReadTimeoutHandler, WriteTimeoutHandler 三个Handler检测连接的有效性,重点分析IdleStateHandler 。
IdleStateHandler |
当连接的空闲时间(读或者写)太长时,将会触发一个IdleStateEvent事件。然后,你可以通 过你的ChannellnboundHandler中重写userEventTrigged方法来处理该事件。 |
ReadTimeoutHandler |
如果在指定的事件没有发生读事件,就会抛出这个异常,并自动关闭这个连接。你可以在 exceptionCaught方法中处理这个异常。 |
WriteTimeoutHandler |
当一个写操作不能在一定的时间内完成时 ,抛出此异常,并关闭连接。你同样可以在 |
3) ReadTimeout 事件和WriteTimeout事件都会自动关闭连接,而且,属于异常处理,所以,这里只是介绍以下,我们重点看IdleStateHandler。
IdleStateHandler:
4个属性:
private final boolean observeOutput; //是否考虑出站时较慢的情况。默认值是false private final long readerldleTimeNanos,//读事件空闲时间,0则禁用事件 private final long writerldleTimeNanos;//写事件空闲时间,0则禁用事件 private final long alldle TimeNanos;//读或写空闲时间,0则禁用事件
初始化:
private void initialize(ChannelHandlerContext ctx) { // Avoid the case where destroy() is called before scheduling timeouts. // See: https://github.com/netty/netty/issues/143 switch (state) { case 1: case 2: return; } state = 1; initOutputChanged(ctx); lastReadTime = lastWriteTime = ticksInNanos(); if (readerIdleTimeNanos > 0) { readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx), readerIdleTimeNanos, TimeUnit.NANOSECONDS); } if (writerIdleTimeNanos > 0) { writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx), writerIdleTimeNanos, TimeUnit.NANOSECONDS); } if (allIdleTimeNanos > 0) { allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx), allIdleTimeNanos, TimeUnit.NANOSECONDS); } }
只要给定的参数大于0,就创建一个定时任务,单位纳秒,每个事件都创建。同时,将state 状态设置为1,防止重复初始化。
调用initOutputChanged 方法,初始化“ 监控出站数据属性”。
IdleStateHandler有3个定时任务内部类,
这3个定时任务对应读,写,读或者写事件。他们共有一个父类(AbstractIdleTask)。
这个父类提供了一个模板方法
private abstract static class AbstractIdleTask implements Runnable { private final ChannelHandlerContext ctx; AbstractIdleTask(ChannelHandlerContext ctx) { this.ctx = ctx; } @Override public void run() { if (!ctx.channel().isOpen()) { return; } run(ctx); } protected abstract void run(ChannelHandlerContext ctx); }
说明通道关闭了,就不再执行方法,否则就执行子类的run方法。
demo:服务端NettyHeartBeat
public class NettyHeartBeat { public static void main(String[] args) { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workGroup = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup,workGroup) .channel(NioServerSocketChannel.class) .childOption(ChannelOption.SO_BACKLOG,128) .option(ChannelOption.SO_KEEPALIVE,true) //日志打印 .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline channelPipeline = ch.pipeline(); /** * 加入-一个netty提供IdleStateHandler说明. * 1. IdleStateHandler是netty提供的处理空闲状态的处理器 * 2. long readerIdleTime:表示多长时间没有读,就会发送-一个心跳检测包检测是否连接 * 3. long writerIdleTime:表示多长时间没有写,就会发送- -个心跳检测包检测是否连接 * 4. long alldleTimne:表示多长时间没有读写,就会发送一个心跳检测包检测是否连接 * * triggers an {@link IdleStateEvent} when a {@link Channel} has not performed * * read, write, or both operation for a while. * 当IdleStateEvent 触发后,就会传递给管道的下一个handler去处理 * 通过调用(触发)下一个handler的userEventTiggered(所以我们需要在自定义的handler中重写该方法) , * 在该方法中去处理IdleStateEvent(空闲,写空闲,读写空闲) */ channelPipeline.addLast(new IdleStateHandler(3,5,7, TimeUnit.SECONDS)); //空闲检测加入自定义的handler channelPipeline.addLast(new MyheartbeatHandler()); } }); try { ChannelFuture channelFuture = serverBootstrap.bind(7777).sync(); channelFuture.addListener((ch)->{ if(ch.isSuccess()){ System.out.println("服务器启动成功"); }else{ System.out.println("服务器启动失败"); } }); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); }finally { bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } }
handler:MyheartbeatHandler
public class MyheartbeatHandler extends ChannelInboundHandlerAdapter { /** * * @param ctx 上下文 * @param evt 触发的事件 * @throws Exception */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { //先判断是否是IdleStateEvent事件 if(evt instanceof IdleStateEvent){ IdleStateEvent idleStateEvent = (IdleStateEvent)evt; String eventType = ""; switch (idleStateEvent.state()){ case READER_IDLE: eventType = "读空闲"; break; case WRITER_IDLE: eventType = "写空闲"; break; case ALL_IDLE: eventType = "读写都空闲"; break; } System.out.println("连接状态。。。"+eventType); } } }
读事件的run方法(即ReaderldleTimeoutTask的run方法)
private final class ReaderIdleTimeoutTask extends AbstractIdleTask { ReaderIdleTimeoutTask(ChannelHandlerContext ctx) { super(ctx); } @Override protected void run(ChannelHandlerContext ctx) {
//用户传入的时间 long nextDelay = readerIdleTimeNanos; if (!reading) {
//计算时间 nextDelay -= ticksInNanos() - lastReadTime; } //小于0,就执行读超时的任务 if (nextDelay <= 0) { // Reader is idle - set a new timeout and notify the callback.
//用于取消任务的promise readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS); boolean first = firstReaderIdleEvent; firstReaderIdleEvent = false; try {
//提交任务 IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
//触发我们定义的handler user channelIdle(ctx, event); } catch (Throwable t) { ctx.fireExceptionCaught(t); } } else { // Read occurred before the timeout - set a new timeout with shorter delay. readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS); } } }
如果超时了将会触发我们自定义发的userEventTriggered()方法
说明:
1)得到用户设置的超时时间。
2)如果读取操作结束了(执行了channelReadComplete 方法设置), 就用当前时间减去给定时间和最后一次读(执操作的时间行了channelReadComplete 方法设置),
如果小于0,就触发事件。反之,继续放入队列。间隔时间是新的计算时间。
3)触发的逻辑是:首先将任务再次放到队列,时间是刚开始设置的时间,返回一个promise 对象,用于做取消操作。然后,设置first 属性为false,
表示,下一次读取不再是第一一次了,这个属性在channelRead方法会被改成true。
4)创建一个IdleStateEvent 类型的写事件对象,将此对象传递给用户的UserEventTriggered 方法。完成触发事件的操作
5)总的来说,每次读取操作都会记录-一个时间,定时任务时间到了,会计算当前时间和最后-次读的时间
的间隔,如果间隔超过了设置的时间,就触发UserEventTriggered 方法。
WriterldleTimeoutTask和AllIdleTimeoutTask和上面的流程基本一致,后两者添加
判断是否出站慢数据的操作
if (hasOutputChanged(ctx, first)) { return; }
心跳机制小结:
1) IdleStateHandler 可以实现心跳功能,当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会 触发用户handler的userEventTriggered 方法。用户可以在这个方法中尝试向对方发送信息,如果发送失败,则关 闭连接。 2) IdleStateHandler 的实现基于EventLoop 的定时任务,每次读写都会记录-个值,在定时任务运行的时候, 通过计算当前时间和设置时间和上次事件发生时间的结果,来判断是否空闲。 3)内部有3个定时任务,分别对应读事件,写事件,读写事件。通常用户监听读写事件就足够了。 4)同时,IdleStateHandler内部也考虑了一些极端情况:客户端接收缓慢,-.次接收数据的速度超过了设置的 空闲时间。Netty通过构造方法中的observeOutput属性来决定是否对出站缓冲区的情况进行判断。 5)如果出站缓慢, Netty 不认为这是空闲,也就不触发空闲事件。但第-次无论如何也是要触发的。因为第一 次无法判断是出站缓慢还是空闲。当然,出站缓慢的话,可能造成OOM , OOM比空闲的问题更大。 6)所以,当你的应用出现了内存溢出,0OM之类,并且写空闲极少发生(使用了observeOutput 为true) , 那么就需要注意是不是数据出站速度过慢。 7)还有一个注意的地方:就是ReadTimeoutHandler ,它继承自IdleStateHandler, 当触发读空闲事件的时候, 就触发ctx.fireExceptionCaught 方法,并传入-个ReadTimeoutException,然后关闭Socket。 8)而WriteTimeoutHandler 的实现不是基于IdleStateHandler 的,他的原理是,当调用write 方法的时候,会 创建-一个定时任务,任务内容是根据传入的p promise的完成情况来判断是否超出了写的时间。当定时任务根据指 定时间开始运行,发现promise 的isDone 方法返回false, 表明还没有写完,说明超时了,则抛出异常。当write 方法完成后,会打断定时任务。
6、Netty中的EventLoop的源码剖析:
首先看看NioEventLoop的关系继承图:
说明:
1) ScheduledExecutorService 接口表示是一个定时任务接口,EventLoop 可以接受定时任务。
2) EventLoop 接口: Netty 接口文档说明该接口作用:一旦Channel 注册了,就处理该Channel对应的所有I/O 操作。
3) SingleThreadEventExecutor 表示这是-一个单个线程的线程池
4) EventLoop是一个单例的线程池,里面含有一个死循环的线程不断的做着3件事情:监听端口,处理端口
事件,处理队列事件。每个EventLoop 都可以绑定多个Channel, 而每个Channel 始终只能由一一个EventLoop 来
处理
根据前面的debug,我们知道EventLoop的监听循环事件,是在serverBootstrap.bind()的进行的操作·,可以追踪到
AbstractBootstrap中的initAndRegister()方法
ChannelFuture regFuture = config().group().register(channel);
继承追踪到AbstractChannel中的register()方法
eventLoop.execute(new Runnable() { @Override public void run() { register0(promise); } });
继承追踪到SingleThreadEventExecutor的execute()方法:
public void execute(Runnable task) { if (task == null) { throw new NullPointerException("task"); } boolean inEventLoop = inEventLoop(); if (inEventLoop) { addTask(task); } else {
//启动线程 startThread();
//任务添加 addTask(task); if (isShutdown() && removeTask(task)) {
//拒绝,抛出异常
reject(); } } if (!addTaskWakesUp && wakesUpForTask(task)) {
//尝试唤醒selector wakeup(inEventLoop); } }
1)首先判断该EventLoop的线程是否是当前线程,如果是,直接添加到任务队列中去,如果不是,则尝试
启动线程(但由于线程是单个的,因此只能启动一次),随后再将任务添加到队列中去。
2)如果线程已经停止,并且删除任务失败,则执行拒绝策略,默认是抛出异常。
3)如果addTaskWakesUp 是false, 并且任务不是NonWakeupRunnable 类型的,就尝试唤醒selector。 这
个时候,阻塞在selecor 的线程就会立即返回
继续追踪 startThread()
private void startThread() {
//先判断是否已经启动 if (state == ST_NOT_STARTED) { if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { doStartThread(); } } }
该方法首先判断是否启动过了,保证EventLoop只有一个线程,如果没有启动过,则尝试使用cas算法将state 状
态改为ST_ STARTED,也就是已启动。然后调用doStartThread 方法。如果失败,则进行回滚
doStartThread()
private void doStartThread() { assert thread == null; executor.execute(new Runnable() { @Override public void run() { thread = Thread.currentThread();
//判断是否打断 if (interrupted) { thread.interrupt(); } boolean success = false;
//更新时间 updateLastExecutionTime(); try {
//执行真正的run方法 SingleThreadEventExecutor.this.run(); success = true; } catch (Throwable t) { logger.warn("Unexpected exception from an event executor: ", t); } finally {
//修改状态 for (;;) { int oldState = state; if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet( SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) { break; } } // Check if confirmShutdown() was called at the end of the loop. if (success && gracefulShutdownStartTime == 0) { logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " + SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must be called " + "before run() implementation terminates."); } try { // Run all remaining tasks and shutdown hooks. for (;;) { if (confirmShutdown()) { break; } } } finally { try { cleanup(); } finally { STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED); threadLock.release(); if (!taskQueue.isEmpty()) { logger.warn( "An event executor terminated with " + "non-empty task queue (" + taskQueue.size() + ')'); } terminationFuture.setSuccess(null); } } } } }); }
1)首先调用executor 的execute 方法,这个executor 就是在创建Event LoopGroup 的时候创建的
2)任务中,首先判断线程中断状态,然后设置最后一次的执行时间。
3)执行当前NioEventLoop 的run 方法,注意:这个方法是个死循环,是整个EventLoop 的核心
4)在finally 块中,使用CAS不断修改state 状态,改成ST_ SHUTTING_ DOWN。也就是当线程Loop结
束的时候。关闭线程。最后还要死循环确认是否关闭,否则不会break。 然后,执行cleanup 操作,更新状
态为
5) ST _TERMINATED,并释放当前线程锁。如果任务队列不是空,则打印队列中还有多少个未完成的任务。
并回调terminationFuture 方法。
追踪SingleThreadEventExecutor.this.run();这里实际上是去执行NioEventLoop中的方法:
NioEventLoop中的run()方法:这是最核心的部分
@Override protected void run() { for (;;) { try { switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) { case SelectStrategy.CONTINUE: continue; case SelectStrategy.SELECT: select(wakenUp.getAndSet(false)); if (wakenUp.get()) { selector.wakeup(); } default: // fallthrough } cancelledKeys = 0; needsToSelectAgain = false; final int ioRatio = this.ioRatio; if (ioRatio == 100) { try { processSelectedKeys(); } finally { // Ensure we always run tasks. runAllTasks(); } } else { final long ioStartTime = System.nanoTime(); try { processSelectedKeys(); } finally { // Ensure we always run tasks. final long ioTime = System.nanoTime() - ioStartTime; runAllTasks(ioTime * (100 - ioRatio) / ioRatio); } } } catch (Throwable t) { handleLoopException(t); } // Always handle shutdown even if the loop processing threw an exception. try { if (isShuttingDown()) { closeAll(); if (confirmShutdown()) { return; } } } catch (Throwable t) { handleLoopException(t); } } }
run方法做了三件事:
1》select 获取对应的事件,如连接、读、写事件。
2》processSelectedKeys处理事件。
3》runAllTasks执行队列中的任务。
继续追踪select()方法
private void select(boolean oldWakenUp) throws IOException { Selector selector = this.selector; try { int selectCnt = 0; long currentTimeNanos = System.nanoTime(); long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos); for (;;) { long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L; if (timeoutMillis <= 0) { if (selectCnt == 0) { selector.selectNow(); selectCnt = 1; } break; } // If a task was submitted when wakenUp value was true, the task didn't get a chance to call // Selector#wakeup. So we need to check task queue again before executing select operation. // If we don't, the task might be pended until select operation was timed out. // It might be pended until idle timeout if IdleStateHandler existed in pipeline. if (hasTasks() && wakenUp.compareAndSet(false, true)) { selector.selectNow(); selectCnt = 1; break; } int selectedKeys = selector.select(timeoutMillis); selectCnt ++; // 如果1秒后返回,有返回值II select 被用户唤醒|| 任务队列有任务II 有定时任务即将被 执行;则跳出循环 if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) { // - Selected something, // - waken up by user, or // - the task queue has a pending task. // - a scheduled task is ready for processing break; } if (Thread.interrupted()) { // Thread was interrupted so reset selected keys and break so we not run into a busy loop. // As this is most likely a bug in the handler of the user or it's client library we will // also log it. // // See https://github.com/netty/netty/issues/2426 if (logger.isDebugEnabled()) { logger.debug("Selector.select() returned prematurely because " + "Thread.currentThread().interrupt() was called. Use " + "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop."); } selectCnt = 1; break; } long time = System.nanoTime(); if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) { // timeoutMillis elapsed without anything selected. selectCnt = 1; } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) { // The selector returned prematurely many times in a row. // Rebuild the selector to work around the problem. logger.warn( "Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.", selectCnt, selector); rebuildSelector(); selector = this.selector; // Select again to populate selectedKeys. selector.selectNow(); selectCnt = 1; break; } currentTimeNanos = time; } if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) { if (logger.isDebugEnabled()) { logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.", selectCnt - 1, selector); } } } catch (CancelledKeyException e) { if (logger.isDebugEnabled()) { logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?", selector, e); } // Harmless exception - log anyway } }
说明:
调用selector 的select 方法,默认阻塞一秒钟, 如果有定时任务,则在定时任务剩余时间的基础上在加.上0.5
秒进行阻塞。当执行execute 方法的时候,也就是添加任务的时候,唤醒selecor, 防止selecotr 阻塞时间过长
再回头继续追踪addTask()方法:
/** * Add a task to the task queue, or throws a {@link RejectedExecutionException} if this instance was shutdown * before. */ protected void addTask(Runnable task) { if (task == null) { throw new NullPointerException("task"); } if (!offerTask(task)) { reject(task); } } final boolean offerTask(Runnable task) {
if (isShutdown()) { reject(); } return taskQueue.offer(task); }
EventLoop小结:
每次执行ececute 方法都是向队列中添加任务。当第一次添加时就启动线程,执行run 方法,而run 方法 是整个EventLoop的核心,就像EventLoop 的名字-样,Loop Loop,不停的Loop,Loop做什么呢?做3件 事情。 ●调用 selector 的select 方法,默认阻塞一秒钟,如果有定时任务,则在定时任务剩余时间的基础上在加上0.5 秒进行阻塞。当执行execute 方法的时候,也就是添加任务的时候,唤醒selecor, 防止selecotr 阻塞时间过 长。 ●当selector 返回的时候,回调用processSelectedKeys 方法对selectKey 进行处理。 ●当procesSelectedKeys 方法执行结束后,则按照ioRatio 的比例执行runAllTasks 方法,默认是I0任务时间 和非IO任务时间是相同的,你也可以根据你的应用特点进行调优。比如非I0任务比较多,那么你就将
ioRatio调小一点,这样非I0任务就能执行的长-一点。防止队列积攒过多的任务。
7、handler中加入线程池和Context中添加线程池的源码剖析
在我们前面的demo中,handler里面,我们是这样的处理业务
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ctx.channel().eventLoop().execute(new Runnable() { @Override public void run() { try{ Thread.sleep(5000); System.out.println("当前线程:"+Thread.currentThread().getName()); ctx.writeAndFlush(Unpooled.copiedBuffer("hello 任务1",CharsetUtil.UTF_8)); System.out.println("当前任务队列。。"+ctx.channel().hashCode()); }catch (Exception e){ e.printStackTrace(); } } }); System.out.println("当前线程2:"+Thread.currentThread().getName());
根据打印结果我们知道,这里执行的线程都是同一个线程,这样如果是要执行具有复杂的业务,这样难免会造成阻塞。
当前线程2:nioEventLoopGroup-3-1
当前线程:nioEventLoopGroup-3-1
1) 在Netty 中做耗时的,不可预料的操作,比如数据库,网络请求,会严重影响Netty 对Socket 的处理速度。
2)而解决方法就是将耗时任务添加到异步线程池中。但就添加线程池这步操作来讲,可以有2种方式,而且这2
种方式实现的区别也蛮大的。
3)处理耗时业务的第一种 方式----andler中加入线程池
4)处理耗时业务的第二种方式----Context中添加线程池
第一种方法handler加入线程池
修改前面的handler的代码:
public class NettyHandelServer2 extends ChannelInboundHandlerAdapter { //创建线程池 static EventExecutorGroup eventExecutorGroup = new DefaultEventExecutorGroup(16); /** * @param ctx 上下文对象,含有管道pipeline,通道channel,地址 * @param msg 就是客户端发送的数据默认Object * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("当前线程1 ... " + Thread.currentThread().getName()); eventExecutorGroup.submit(new Callable<Object>() { @Override public Object call() throws Exception { try { Thread.sleep(10*1000); System.out.println("当前线程2 call ... " + Thread.currentThread().getName()); ctx.writeAndFlush(Unpooled.copiedBuffer("hello 任务2",CharsetUtil.UTF_8)); } catch (InterruptedException e) { e.printStackTrace(); } return null; } }); System.out.println("读取数据结束。。。。。。。"); } }
打印结果:
当前线程1 ... nioEventLoopGroup-3-1
读取数据结束。。。。。。。
10秒后。。。
当前线程3 call ... defaultEventExecutorGroup-4-2
当前线程2 call ... defaultEventExecutorGroup-4-1
可以知道,将任务提交到线程池中的操作将不会再是一个线程去执行,而是有多个线程去执行,这样就不会阻塞io线程。
我们来debug,来看看源码:
ctx.writeAndFlush(Unpooled.copiedBuffer("hello 任务2",CharsetUtil.UTF_8));
根据追踪会执行到这里
private void write(Object msg, boolean flush, ChannelPromise promise) { AbstractChannelHandlerContext next = findContextOutbound(); final Object m = pipeline.touch(msg, next); EventExecutor executor = next.executor();
//先判断是否是当前线程 if (executor.inEventLoop()) { if (flush) { next.invokeWriteAndFlush(m, promise); } else { next.invokeWrite(m, promise); } } else { AbstractWriteTask task;
//否则就将当前工作封装成任务到队列中 if (flush) {
//包装成任务 task = WriteAndFlushTask.newInstance(next, m, promise); } else { task = WriteTask.newInstance(next, m, promise); }
//执行 safeExecute(executor, task, promise, m); } }
在executor.inEventLoop()追踪,我们发现一个是我们NioEventLoopGroup线程,一个是我们自定义的defaultEventExecutorGroup线程
不是当前线程,会返回false。
说明:
1)当判定下个outbound的executor 线程不是当前线程的时候,会将当前的工作封装成task ,然后放入
mpsc队列中,等待I0任务执行完毕后执行队列中的任务。
2)当我们使用了group .submit(new Callable<Object>O{}在handler 中加入线程池,就会进入到safeExecute(executor, task,
promise, m);如果去掉这段代码,而使用普通方式来执行耗时的业务,那么就不会进入到safeExecute(executor,task, promise, m);
第二种方式在Context中添加线程池
在main方法中加入线程池:
//创建线程池 static EventExecutorGroup eventExecutorGroup = new DefaultEventExecutorGroup(5); .childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道测试对象 //给pipeline设置处理器 @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(eventExecutorGroup,new NettyHandelServer3());//调用处理器 } });
addLast()方法中,加一个线程池。
这时候handler里面的线程就是我们自定义的线程
/** * @param ctx 上下文对象,含有管道pipeline,通道channel,地址 * @param msg 就是客户端发送的数据默认Object * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("当前线程:"+Thread.currentThread().getName()); Thread.sleep(10*1000); System.out.println("当前线程:"+Thread.currentThread().getName()); }
打印结果:
当前线程:defaultEventExecutorGroup-2-1
当前线程:defaultEventExecutorGroup-2-1
说明:
1) handler中的代码就使用普通的方式来处理耗时业务。
2)当我们在调用addLast 方法添加线程池后,handler将优先使用这个线程池,如果不添加,将使用I0线程
3)当走到AbstractChannelHandlerContext 的invokeChannelRead 方法的时候,executor.inEventLoop( 是不
会通过的,因为当前线程是I0线程Context(也就是Handler) 的executor 是业务线程,所以会异步执行, debug
我们来debug,来看看源码:前面的也是先判断是否是当前线程,否则就会invokeChannelRead()方法,
它会去调用真正的handler执行channelRead()方法:
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeChannelRead(m); } else { executor.execute(new Runnable() { @Override public void run() { next.invokeChannelRead(m); } }); } }
private void invokeChannelRead(Object msg) { if (invokeHandler()) { try { ((ChannelInboundHandler) handler()).channelRead(this, msg); } catch (Throwable t) { notifyHandlerException(t); } } else { fireChannelRead(msg); } }
handler中加入线程池和Context中添加线程池小结:
两种方式的比较 1)第一种方式在handler 中添加异步,可能更加的自由,比如如果需要访问数据库,那我就异步,如果不需 要,就不异步,异步会拖长接口响应时间。因为需要将任务放进mpscTask 中。如果I0时间很短,task很多,可 能一个循环下来,都没时间执行整个task,导致响应时间达不到指标。 2)第二种方式是Netty 标准方式(即加入到队列),但是,这么做会将整个handler都交给业务线程池。不论 耗时不耗时,都加入到队列里,不够灵活。 3)各有优劣,从灵活性考虑,第-种较好