NIO 在Tomcat中的应用

对NIO的理解

个人单方面认为,NIO与BIO的最大区别在于主动和被动,使用BIO的方式需要等待被调用方返回数据,很明显此时调用者是被动的。

举个例子

阻塞IO 假设你是一个胆小又害羞的男孩子,你约了隔壁测试的妹子,但你并不敢主动约会,所以你把自己的手机号码给她,并暗示她想要约会的时候打电话给你。很明显此时你陷入了被动,约不约会的结果需要妹子主动告知你,如果她忘了,那么你要陷入长时间的等待中以及无尽的猜测和自我怀疑中(太惨了)。[如果你是一个胆小害羞又好色的男孩子,那就惨了]

非阻塞IO 我们知道,渣男通常有很多的备胎,我管这个叫做备胎池(SpareTirePool), 那么当他想要约会的时候,只要群发问妹子要不要约会,如果要约会的话就和妹子约会,约会结束之后,处理其他约会事件,如果没有继续下一次询问。在这个例子中约会可以视为IO事件,问妹子的过程可以视为备胎池的轮询。


以下代码基于tomcat 7

 

Tomcat 如何使用NIO

既然是网络通信的I/O那必然有以下两个步骤

  • SeverSocket的启动
  • I/O事件的处理

关键代码在 package org.apache.tomcat.util.net.NioEndpoint 中

 

ServerSocket的启动

在最开始看代码,是震惊的,真的,如果你看Reactor模型的话

以下bind方法代码是启动ServerSocket的流程,主要流程如下

  • 绑定地址
  • 设置接收新连接的方式为阻塞方式(关键点)
  • 设置Acceptor和Poller的数量以及初始化SelectorPool
    @Override
    public void bind() throws Exception {

        if (!getUseInheritedChannel()) {
            serverSock = ServerSocketChannel.open();
            socketProperties.setProperties(serverSock.socket());
            InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
            serverSock.socket().bind(addr,getAcceptCount());
        } else {
            // Retrieve the channel provided by the OS
            Channel ic = System.inheritedChannel();
            if (ic instanceof ServerSocketChannel) {
                serverSock = (ServerSocketChannel) ic;
            }
            if (serverSock == null) {
                throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
            }
        }
        // 以阻塞的方式来接收连接!!
        serverSock.configureBlocking(true); //mimic APR behavior

        // 设置Acceptor和Poller的数量
        if (acceptorThreadCount == 0) {
            // FIXME: Doesn't seem to work that well with multiple accept threads
            // 顾名思义,Acceptor是用来处理新连接的
            acceptorThreadCount = 1;
        }
        if (pollerThreadCount <= 0) {
            // Poller 用来处理I/O事件
            pollerThreadCount = 1;
        }
        setStopLatch(new CountDownLatch(pollerThreadCount));

        // Initialize SSL if needed
        initialiseSsl();
        // 从此处可以看出tomcat池化了selector
        selectorPool.open();
    }

 

Tomcat NIO 如何处理I/O事件

先说结论,Tomcat NIO模型中有以下关键角色

  • Acceptor 用于接收新连接,每个Acceptor一个线程,以阻塞的方式接收新连接
  • Poller 当Acceptor接收到新连接,进行处理之后选择一个Poller处理该连接上的I/O事件。
  • LimitLatch 一个用来限制连接数的锁

Acceptor

Acceptor的主要工作就是不断接收来自客户端的连接,在简单处理之后将该连接交给Poller处理

接收来自客户端连接, 如果你不想看代码,以下是其主要流程

  • 接收来自客户端的连接,并将其交给Poller处理
      @Override
        public void run() {

            int errorDelay = 0;

            // running的检测贯穿了Accpetor的处理流程,在每次关键操作的时候都会执行检测
            while (running) {

                // 如果进入暂停状态则每隔一段时间检测一下
                while (paused && running) {
                    state = AcceptorState.PAUSED;
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }
                // 再次检测
                if (!running) {
                    break;
                }
                state = AcceptorState.RUNNING;

                try {
                    //检查是否达到最大连接数如果是则陷入等待,如果不是则增加当前连接数
                    countUpOrAwaitConnection();

                    SocketChannel socket = null;
                    try {
                        //接收新连接
                        socket = serverSock.accept();
                    } catch (IOException ioe) {
                        // 发生异常,则减少连接数
                        countDownConnection();
                        if (running) {
                         handleExceptionWithDelay(errorDelay);
                            // re-throw
                            throw ioe;
                        } else {
                            break;
                        }
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                    // Configure the socket
                    if (running && !paused) {
                        //setSocketOptions会导致将该连接交给Poller处理
                        if (!setSocketOptions(socket)) {
                            closeSocket(socket);
                        }
                    } else {
                        closeSocket(socket);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }

再来看看setSocketOptions做了什么,不想看代码的话,总结如下

  • 将客户端socket设置为非阻塞模式
  • 将客户端的socket封装为NioChannelSecureNioChannel(使用了对象池技术)
  • Poller池中获取一个Poller,将NioChannel注册到Poller上

 

  protected boolean setSocketOptions(SocketChannel socket) {
        // Process the connection
        try {
            //设置为非阻塞模式,以便通过selector进行查询
            socket.configureBlocking(false);
            Socket sock = socket.socket();
            socketProperties.setProperties(sock);
            //从对象池中获取一个NioChannel,tomcat会复用一切可以复用的对象以减少创建新对象所带来的消耗
            NioChannel channel = nioChannels.pop();
            if (channel == null) {
               // 没有获取到,那就新建一个呗
                SocketBufferHandler bufhandler = new SocketBufferHandler(
                        socketProperties.getAppReadBufSize(),
                        socketProperties.getAppWriteBufSize(),
                        socketProperties.getDirectBuffer());
                // SSL这一块还没研究
                if (isSSLEnabled()) {
                    channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
                } else {
                    channel = new NioChannel(socket, bufhandler);
                }
            } else {
                channel.setIOChannel(socket);
                //重新设置SocketBufferHandler,将其设置为可写和可读
                channel.reset();
            }
            //从Poller池中获取一个Poller(按照次序获取,可以理解为一个圆环),并将Channel注册到上面
            getPoller0().register(channel);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            try {
                log.error("",t);
            } catch (Throwable tt) {
                ExceptionUtils.handleThrowable(tt);
            }
            // Tell to close the socket
            return false;
        }
        return true;
    }

每来一个 都注册到pollers线程上面处理  这样就快了

 

 

Poller

从连接注册到Poller说起

不加锁的获取一个Poller

具体说明见代码

关键点:对一个数A取余会将余数的结果限制在A的范围内

 

/**
     * Return an available poller in true round robin fashion.
     * 很明显,取余的方式揭示了获取Poller的方法。你可以理解为
     * Poller会组成一个圆环,这样我们就可以通过不断递增获取
     * 下一个Poller,但是数据会溢出所以我们要取绝对值
     * @return The next poller in sequence
     */
    public Poller getPoller0() {
        int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
        return pollers[idx];
    }

 

 

 

channel的注册

该方法会对新的建的连接进行封装,并以PollerEvent的形式注册到相应的Poller中

需要注意的是,真正的注册读事件并不是在此方法注册的(当前方法调用者为Acceptor线程),而是在Poller线程中注册读事件的

 

public void register(final NioChannel socket) {
            socket.setPoller(this);

            // 获取一个KeyAttachment对象,将当前socket的相关信息设置进去
            KeyAttachment key = keyCache.poll();
            final KeyAttachment ka = key!=null?key:new KeyAttachment(socket);
            ka.reset(this,socket,getSocketProperties().getSoTimeout());
            ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
            ka.setSecure(isSSLEnabled());

            // 获取一个PollerEvent对象,本事件为一个注册事件,对读事件感兴趣(这里暂时还没有真正的向select去注册事件)
            PollerEvent r = eventCache.poll();
            ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
            if ( r==null) {
                r = new PollerEvent(socket,ka,OP_REGISTER);
            } else {
                r.reset(socket,ka,OP_REGISTER);
            }

            // 把PollerEvent添加到事件列表中去
            addEvent(r);
        }

 

Poller处理I/O 事件

Poller 处理I/O事件的的代码较长,而且细节也较多,总结其主要作用如下

  • 检测是否有Acceptor提交PollerEvent,如果有则调用PolllerEvent的run方法注册读事件
  • 在执行关键操作的时候检测该Poller是否被关闭如果是,则执行相应的资源释放和关闭操作
  • 调用selector.select() 轮询事件,如果有读事件则交给processKey处理

 

@Override
        public void run() {
            // Loop until destroy() is called
            // 一直循环直到destroy方法被调用
            while (true) {
                try {
                    // Loop if endpoint is paused
                    while (paused && (!close) ) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            // Ignore
                        }
                    }

                    boolean hasEvents = false;

                    // Time to terminate?
                    if (close) {
                        // 处理Acceptors注册到Poller中的PollerEvent
                        events();
                        //selector time out 或者poller被关闭就会调用timeout方法
                        timeout(0, false);
                        try {
                            selector.close();
                        } catch (IOException ioe) {
                            log.error(sm.getString(
                                    "endpoint.nio.selectorCloseFail"), ioe);
                        }
                        break;
                    } else {
                        // 执行PollerEvent事件,向Selector注册读写事件
                        hasEvents = events(); // 真正的向selector注册
                    }
                    try {
                        if ( !close ) {
                            if (wakeupCounter.getAndSet(-1) > 0) {
                                //if we are here, means we have other stuff to do
                                //do a non blocking select
                                // 上面的events()会去注册事件,而这里是去查询是否有事件就绪
                                // 不阻塞
                                keyCount = selector.selectNow();
                            } else {
                                // 阻塞,超时会继续执行下面的代码,不会报错
                                keyCount = selector.select(selectorTimeout);
                            }
                            wakeupCounter.set(0);
                        }
                        if (close) {
                            events();
                            timeout(0, false);
                            try {
                                selector.close();
                            } catch (IOException ioe) {
                                log.error(sm.getString(
                                        "endpoint.nio.selectorCloseFail"), ioe);
                            }
                            break;
                        }
                    } catch ( NullPointerException x ) {
                        //sun bug 5076772 on windows JDK 1.5
                        if ( log.isDebugEnabled() ) log.debug("Possibly encountered sun bug 5076772 on windows JDK 1.5",x);
                        if ( wakeupCounter == null || selector == null ) throw x;
                        continue;
                    } catch ( CancelledKeyException x ) {
                        //sun bug 5076772 on windows JDK 1.5
                        if ( log.isDebugEnabled() ) log.debug("Possibly encountered sun bug 5076772 on windows JDK 1.5",x);
                        if ( wakeupCounter == null || selector == null ) throw x;
                        continue;
                    } catch (Throwable x) {
                        ExceptionUtils.handleThrowable(x);
                        log.error("",x);
                        continue;
                    }
                    //either we timed out or we woke up, process events first
                    if ( keyCount == 0 ) hasEvents = (hasEvents | events());
                    // 执行 select 操作,查询I/O事件
                    // 如果存在就绪事件,那么则遍历并处理事件
                    Iterator<SelectionKey> iterator =
                        keyCount > 0 ? selector.selectedKeys().iterator() : null;
                    // Walk through the collection of ready keys and dispatch
                    // any active event.
                    // 循环处理当前就绪的事件
                    while (iterator != null && iterator.hasNext()) {
                        SelectionKey sk = iterator.next();
                        KeyAttachment attachment = (KeyAttachment)sk.attachment();
                        // Attachment may be null if another thread has called
                        // cancelledKey()
                        if (attachment == null) {
                            iterator.remove();
                        } else {
                            attachment.access();
                            iterator.remove();
                            // 处理事件
                            // 处理检测到的I/O事件
                            processKey(sk, attachment);
                        }
                    }//while
                     //timeout 会检查是否关闭,如果已经关闭并且有事件未处理会调用 
                     cancelledKey方法
                //cancelledKey:该方法主要是对和该连接相关的资源执行关闭操作
                    //process timeouts
                    timeout(keyCount,hasEvents);
                    if ( oomParachute > 0 && oomParachuteData == null ) checkParachute();
                } catch (OutOfMemoryError oom) {
                    try {
                        oomParachuteData = null;
                        releaseCaches();
                        log.error("", oom);
                    }catch ( Throwable oomt ) {
                        try {
                            System.err.println(oomParachuteMsg);
                            oomt.printStackTrace();
                        }catch (Throwable letsHopeWeDontGetHere){
                            ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                        }
                    }
                }
            }//while

            stopLatch.countDown();
        }

 

processKey 处理I/O事件

processKey主要工作如下

  • 再次检测Poller是否关闭,如果是则释放资源
  • 检测查询到事件是否合法,如果合法则取消已注册到selector上的事件且被被本次轮询所查询到的事件
  • 再调用processSocket处理读事件,之后处理写事件
   protected boolean processKey(SelectionKey sk, KeyAttachment attachment) {
            boolean result = true;
            try {
                if ( close ) {
                    // 如果Poller关闭则关闭和释放和此连接相关的资源
                    cancelledKey(sk, SocketStatus.STOP, attachment.comet);
                } else if ( sk.isValid() && attachment != null ) {
                    attachment.access();//make sure we don't time out valid sockets
                    sk.attach(attachment);//cant remember why this is here

                    // 当前就绪事件对应的channel
                    NioChannel channel = attachment.getChannel();
                    // 读就绪或写就绪
                    if (sk.isReadable() || sk.isWritable() ) {
                        if ( attachment.getSendfileData() != null ) {
                            processSendfile(sk,attachment, false);
                        } else {
                            if ( isWorkerAvailable() ) {
                                unreg(sk, attachment, sk.readyOps()); //
                                boolean closeSocket = false;
                                // Read goes before write  先读后写
                                if (sk.isReadable()) {
                                    // 从channel中读取数据
                                    // 关键代码,调用processSocket方法处理读事件
                                    if (!processSocket(channel, SocketStatus.OPEN_READ, true)) {
                                        closeSocket = true;
                                    }
                                }
                                // 读完数据之后可能就要写数据
                                if (!closeSocket && sk.isWritable()) {
                                    // 将数据写入到channel中
                                    if (!processSocket(channel, SocketStatus.OPEN_WRITE, true)) {
                                        closeSocket = true;
                                    }
                                }
                                if (closeSocket) {
                                    cancelledKey(sk,SocketStatus.DISCONNECT,false);
                                }
                            } else {
                                result = false;
                            }
                        }
                    }
                } else {
                    //invalid key
                    cancelledKey(sk, SocketStatus.ERROR,false);
                }
            } catch ( CancelledKeyException ckx ) {
                cancelledKey(sk, SocketStatus.ERROR,false);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error("",t);
            }
            return result;
        }

 

 

public boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) {
        // 该方法是用来从socket中读数据或写数据的,dispatch表示是不是要把这个任务派发给线程池,也就是要不要异步

        try {
            KeyAttachment attachment = (KeyAttachment)socket.getAttachment();
            if (attachment == null) {
                return false;
            }
            attachment.setCometNotify(false); //will get reset upon next reg

            // 获取一个SocketProcessor对象
            SocketProcessor sc = processorCache.poll();
            if ( sc == null ) sc = new SocketProcessor(socket,status);
            else sc.reset(socket,status);

            // 派发给线程池
            if ( dispatch && getExecutor()!=null ) getExecutor().execute(sc);
            else sc.run();
        } catch (RejectedExecutionException rx) {
            log.warn("Socket processing request was rejected for:"+socket,rx);
            return false;
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            // This means we got an OOM or similar creating a thread, or that
            // the pool and its queue are full
            log.error(sm.getString("endpoint.process.fail"), t);
            return false;
        }
        return true;
    }

 

另外:

tomcat   nio 模型:读取请求体的话 是以阻塞方式读取的      请求头和请求行 是 nio方式读取 

  因为servlet规范规定了 读取请求体 以阻塞方式

使用 BlockPoller  加锁来实现的 

 

tomcat处理源码 与异步sevlet实现

 

因为servlet规范3.0  所以tomcat的nio读取请求体是阻塞的

下面的思路 设置nio阻塞是错的  因为会抛异常 如下图所示

 

 

 

tomcat的实现就是第一个 通道有数据就读取 没数据 注册一个辅助 seletor 然后放进去  加锁阻塞掉   线程poller(辅助的)不停循环 看有木有事件  有事件的话就解除加锁  也就解除了阻塞然后读数据

 

也就是说 tomcat7 主selector 处理完请求行和请求头 (nio非阻塞方式)  然后使用辅助selector处理 请求体 (nio 阻塞方式)

 

 

tomcat7 读取请求体阻塞 的实现 新开了个 selector

 

总结 tomcat的NIO模型

 

NIO模型

 

 

 

 

 

LimitLatch 为所有的Acceptor共用,用来限制当前的最大连接数

Acceptor 以阻塞的形式来接收新连接,并将其封装成PollerEvent对象提交到Poller中

Poller 接收来自Acceptor的PollerEvent并注册读事件,以及轮询和其绑定的客户端Socket有无读事件,如果有则执行进一步操作,将其提交到其他地方执行处理(解析Http协议)

思想迁移

学习源码就是为了学习其设计思想. -- 沃兹及.硕德

对象池化 池化对象、池化连接可以大大降低新建对象以及GC所带来的消耗,当需要使用从池中取出来重新设置相关值即可

环形队列 虽然这玩意不新鲜,但配合上原子类,就可以在高并发的情况,高效的获取队列中的下一个元素(环形队列中索引溢出的处理在之前我是没有考虑到的)  


从尾部加入数据A

再从尾部加入数据B

再从尾部加入数据C

从头部取出数据A

再从尾部加入数据D

再从头部取出数据B

再从尾部加入数据E

这时加入数据F却失败了,可是数组的前两个位置明明就是空的,但是却由于数组的最后一个位置有数据导致新数据无法正常加入,这种情况就是“假溢出”。

有人肯定会说,数组中的数据向前移动两位不久可以了吗?这确实可以,但是如果数组的长度很长,那么需要移动的数据量就会非常大,既消耗时间,又消耗性能,所以这种方法不太好。我这里介绍另外一种解决队列“假溢出”问题的方法:环形队列。
环形队列就是将顺序队列首尾相连形成环形结构的队列,这样环形结构的好处就是:只要队列没有真正的满,永远不会溢出

比如上述顺序队列中的例子,若首位相连,那么F就会放到数组的第一个位置上去

 

否则的话

 

阻塞获取链接,非阻塞处理IO事件 与Reactor模型形成强烈的对比,学习NIO的时候思维被限制住了,认为非阻塞的获取连接会获得更高的性能,但现在情况不一定了(还没测试,哪位老哥试了告诉我一下)

关键操作时,对标志位进行检测 如果你要通过一个标志变量来控制你的线程,且线程循环一次需要相对较长的时间(你代码太长,操作太多)那么最好在执行关键操作之前对你的标志变量进行检查,来决定是否要改变线程的行为(康康poller和Acceptor的代码)


 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值