精华内容
下载资源
问答
  • Tomcat NIO

    千次阅读 2018-04-11 15:00:23
    说起TomcatNIO,不得不提的就是Connector这个Tomcat组件。Connector是Tomcat的连接器,其主要任务是负责处理收到的请求,并创建一个Request和Response的对象,然后用一个线程用于处理请求,Connector会把Request和...

    说起Tomcat的NIO,不得不提的就是Connector这个Tomcat组件。Connector是Tomcat的连接器,其主要任务是负责处理收到的请求,并创建一个Request和Response的对象,然后用一个线程用于处理请求,Connector会把Request和Response对象传递给该线程,该线程的具体的处理过程就是Container容器的事了。

    在tomcat启动过程中,会初始化Connector,并调用Connector的startInternal()方法开启Connector,开始监听、处理请求。


    想了解Tomcat NIO的工作方式,就得先了解一下Connector的实现原理。下面从三个方面来了解一下Connector组件:Connector的数据结构、Connector初始化以及Connector开启。


    Connector

    Connector的数据结构

    先了解一下Connector的数据结构。Connector的一个主要的属性:ProtocolHandler protocolHandler(协议)


    protocolHandler(协议)

    • 维护服务器使用的协议,如http1.1等。ProtocolHandler是接口,实现类有Http11Nio2Protocol 、Http11Nio2Protocol等
    • 维护服务提供的IO方式,负责EndPoint的初始化、启动。目前有BIO、NIO、AIO等IO方式,来实现监听端口、读写socket数据的功能。通过EndPoint封装实现不同的IO方式
    • EndPoint监听到IO读写,交给Tomcat线程池中的一个线程来处理,SocketProcessor会根据protocolHandler采用的协议,调用协议的process方法处理请求。
    • 维护adapter(适配器),可以将请求/响应数据进行适配


    protocolHandler会找到socket对应的处理器(如Http11Processor),然后进行数据读写、适配,处理。请求由adapter最终会交给servlet处理


    常说的BIO、NIO,主要的应用就在protocolHandler中。protocolHandler负责维护Connector使用的协议以及IO方式。在protocolHandler中,不同的IO方式,会使用不同的EndPoint,具体采用哪种IO方式,取决于采用哪个EndPoint,每一个EndPoint的实现类,都封装了一种IO策略。若采用NIO,则为NioEndpoint。


    Connector初始化


    创建Connector时,会拿到Tomcat目录下conf/server.xml中Connector的协议配置,利用反射创建ProtocolHandler:

    /**
     * Coyote Protocol handler class name.
     * Defaults to the Coyote HTTP/1.1 protocolHandler.
     */
    protected String protocolHandlerClassName = "org.apache.coyote.http11.Http11NioProtocol";
    
    
    public Connector(String protocol) {
    	//设置protocolHandlerClassName类名
        setProtocol(protocol);
        // Instantiate protocol handler
        ProtocolHandler p = null;
        try {
    		//根据server.xml中<connector/>标签的protocol属性值,获取到对应的http协议类
            Class<?> clazz = Class.forName(protocolHandlerClassName);
            p = (ProtocolHandler) clazz.getConstructor().newInstance();
        } catch (Exception e) {
            log.error(sm.getString(
                    "coyoteConnector.protocolHandlerInstantiationFailed"), e);
        } finally {
            this.protocolHandler = p;
        }
    
        if (Globals.STRICT_SERVLET_COMPLIANCE) {
            uriCharset = StandardCharsets.ISO_8859_1;
        } else {
            uriCharset = StandardCharsets.UTF_8;
        }
    }
    
    //设置protocolHandlerClassName类名
    public void setProtocol(String protocol) {
    
        boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
                AprLifecycleListener.getUseAprConnector();
    
    	//若配置了protocol="HTTP/1.1"或者没配,则默认是Http11NioProtocol或者Http11AprProtocol
        if ("HTTP/1.1".equals(protocol) || protocol == null) {
            if (aprConnector) {
                setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
            } else {
                setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
            }
        } else if ("AJP/1.3".equals(protocol)) {
            if (aprConnector) {
                setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
            } else {
                setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
            }
        } else {
    		//直接取配置的类名
            setProtocolHandlerClassName(protocol);
        }
    }

    以Tomcat8.5.20为例,这里默认是http1.1的NIO。



    Connector.start()开启


    Connector初始化后,调用start方法开启。主要涉及一下几个方法:


    Connector的startInternal()方法,会调用protocolHandler.start();

    protocolHandler中会调用endpoint.start(),从而达到开启endpoint、监听端口、读写Socket的目的:

    //Connector开启
    protected void startInternal() throws LifecycleException {
    
        // 校验端口
        if (getPort() < 0) {
            throw new LifecycleException(sm.getString(
                    "coyoteConnector.invalidPort", Integer.valueOf(getPort())));
        }
    	
    	//设置Connector的状态为开启
        setState(LifecycleState.STARTING);
    
        try {
    		//开启protocolHandler
            protocolHandler.start();
        } catch (Exception e) {
            String errPrefix = "";
            if(this.service != null) {
                errPrefix += "service.getName(): \"" + this.service.getName() + "\"; ";
            }
    
            throw new LifecycleException
                (errPrefix + " " + sm.getString
                 ("coyoteConnector.protocolHandlerStartFailed"), e);
        }
    }
    
    
    //protocolHandler开启
    public void start() throws Exception {
        if (getLog().isInfoEnabled())
            getLog().info(sm.getString("abstractProtocolHandler.start",
                    getName()));
        try {
    		//endpoint开启,初始化Processor缓存、event缓存、exector线程池,开启轮询线程、acceptor线程
            endpoint.start();
        } catch (Exception ex) {
            getLog().error(sm.getString("abstractProtocolHandler.startError",
                    getName()), ex);
            throw ex;
        }
    
        // Start async timeout thread
        asyncTimeout = new AsyncTimeout();
        Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout");
        int priority = endpoint.getThreadPriority();
        if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {
            priority = Thread.NORM_PRIORITY;
        }
        timeoutThread.setPriority(priority);
        timeoutThread.setDaemon(true);
        timeoutThread.start();
    }


    至此,Connector完成了开启的过程,开启监听端口、可以读写Socket了。


    总结一下,关于Connector:

    创建Connector时,会拿到Tomcat目录下conf/server.xml中Connector的协议配置,利用反射创建ProtocolHandler。

    ProtocolHandler负责维护Connector使用的协议以及IO方式,不同的IO方式如BIO、NIO、AIO封装在EndPoint中

    开启Connector时,会开启protocolHandler,从而达到EndPoint的开启,开始监听端口、读写socket数据了

    protocolHandler中将请求拿到的数据进行适配,通过adapter适配成Request和Response对象,最终交给Container去处理


    下面重点就来了,NIO。

    Tomcat NIO


    Tomcat在处理客户端请求时,读写socket数据是一种网络IO操作。目前Tomcat有几种IO方式,分别是BIO(同步阻塞),NIO(同步非阻塞)和AIO(异步非阻塞)。不同IO方式的读写机制,被封装在了Endpoint中。BIO、AIO不再赘述。这里主要看NIO。

    Tomcat NIO模型

    当然要了解一下Tomcat NIO的模型了。Tomcat NIO是基于Java NIO实现的,其基本原理如下:


    Tomcat NIO是对Java NIO的一种典型的应用方式:通过JDK提供的同步非阻塞的IO方式,实现了IO多路复用,即一个线程管理多个客户端的连接。了解Java NIO,可以看一下Java NIO

    Tomcat在NIO模式下,所有客户端的请求先由一个接收线程接收,然后由若干个(一般为CPU的个数)线程轮询读写事件,最后将具体的读写操作交由线程池处理。

    NioEndpoint


    要了解Tomcat的NIO实现,其实就是了解NioEndpoint的实现原理。


    数据结构

    它一共包含LimitLatch、Acceptor、Poller、SocketProcessor、Excutor5个部分


    • LimitLatch是连接控制器,它负责维护连接数的计算,nio模式下默认是10000,达到这个阈值后,就会拒绝连接请求。
    • Acceptor负责接收连接,默认是1个线程来执行,将请求的事件注册到事件列表
    • Poller来负责轮询上述产生的事件。Poller线程数量是cpu的核数Math.min(2,Runtime.getRuntime().availableProcessors())。由Poller将就绪的事件生成SocketProcessor,然后交给Excutor去执行。
    • SocketProcessor继承了SocketProcessorBase,实现了Runnable接口,可以提交给线程池Excutor来执行。它里面的doRun()方法,封装了读写Socket、完成Container调用的逻辑
    • Excutor线程池是一个Tomcat线程池。用来执行Poller创建的SocketProcessor。Excutor线程池的大小就是我们在Connector节点配置的maxThreads的值。


    SocketProcessor被一个线程执行的时候,会完成从socket中读取http request,解析成HttpServletRequest对象,分派到相应的servlet并完成逻辑,然后将response通过socket发回client。在从socket中读数据和往socket中写数据的过程,并没有像典型的非阻塞的NIO的那样,注册OP_READ或OP_WRITE事件到主Selector,而是直接通过socket完成读写,这时是阻塞完成的,但是在timeout控制上,使用了NIO的Selector机制,但是这个Selector并不是Poller线程维护的主Selector,而是BlockPoller线程中维护的Selector,称之为辅Selector,实现可见org.apache.coyote.http11.Http11InputBuffer#fill。


    了解了NioEndPoint的数据结构之后,可以看一下它们的关系图

    NioEndpoint组件关系图



    以上过程就以同步非阻塞的方式完成了网络IO。


    其实是一个Reactor模型:

    • 一个Acceptor(当然多个也行,不过一般场景一个够了)负责accept事件,把接收到SocketChannel注册到按某种算法从Reactor池中取出的一个Reactor上,注册的事件为读,写等,之后这个Socket Channel的所有IO事件都和Acceptor没关系,都由被注册到的那个Reactor来负责。
    • 每个Acceptor和每个Reactor都各自持有一个Selector
    • 当然每个Acceptor和Reactor都是一个线程


    这里的Poller池其实就是一个Reactor池,可以是多个线程。



    NioEndPoint实现


    工作原理简单了解了一下,接下来看一下具体的代码实现吧。先上一个NioEndpoint的UML图:


    NioEndPoint启动


    AbstractEndpoint里实现了一些EndPoint的抽象的通用的方法,其中主要的一个入口方法是org.apache.tomcat.util.net.AbstractEndpoint#start方法

    public final void start() throws Exception {
        if (bindState == BindState.UNBOUND) {
            bind();
            bindState = BindState.BOUND_ON_START;
        }
        startInternal();
    }

    其中,bind()方法和startInternal()方法,由其子类具体实现。

    bind()方法用于初始化endpoint,绑定监听端口等、设置最大线程数、ssl等。

    startInternal()方法在EndPoint初始化完毕后,创建pollers轮询线程以及acceptors线程并开启。

    /**
     * 开启 NIO endpoint, 创建pollers轮询线程以及acceptors线程
     */
    @Override
    public void startInternal() throws Exception {
    
        if (!running) {
            running = true;
            paused = false;
    		//SocketProcessor缓存。若缓存没有,则创建新的SocketProcessor
            processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                    socketProperties.getProcessorCache());
    		//poller事件缓存
            eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                            socketProperties.getEventCache());
    		//nioChannels缓存。
            nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                    socketProperties.getBufferPool());
    
            // Create worker collection 创建线程池
            if ( getExecutor() == null ) {
                createExecutor();
            }
    
            initializeConnectionLatch();
    
            // Start poller threads
            pollers = new Poller[getPollerThreadCount()];
            for (int i=0; i<pollers.length; i++) {
                pollers[i] = new Poller();
                Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                pollerThread.setPriority(threadPriority);
                pollerThread.setDaemon(true);
                pollerThread.start();
            }
    
    		//start acceptor threads
            startAcceptorThreads();
        }
    }
    
    
    protected final void startAcceptorThreads() {
        int count = getAcceptorThreadCount();
        acceptors = new Acceptor[count];
    
    
        for (int i = 0; i < count; i++) {
            acceptors[i] = createAcceptor();
            String threadName = getName() + "-Acceptor-" + i;
            acceptors[i].setThreadName(threadName);
            Thread t = new Thread(acceptors[i], threadName);
            t.setPriority(getAcceptorThreadPriority());
            t.setDaemon(getDaemon());
            t.start();
        }
    }

    NioEndPoint时序图


    看完了开启EndPoint的过程,再来详细看一下NioEndpoint处理的的时序图:

    通过上面的时序图,结合代码来详细了解一下Acceptor和Poller的工作方式。


    Acceptor接收请求


    NioEndPoint中的Acceptor方法实现了Runnable接口,主要干的活就是上述图中的3,4,5,6,7


    @Override
    public void run() {
    
        int errorDelay = 0;
    
        // 循环,直到收到一个关闭的命令
        while (running) {
    
            // 如果EndPoint被暂停,则循环sleep
            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 {
                    // 创建一个socketChannel,接收下一个从服务器进来的连接
                    socket = serverSock.accept();
                } catch (IOException ioe) {
                    // We didn't get a socket
                    countDownConnection();
                    if (running) {
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    } else {
                        break;
                    }
                }
                // 成功接收,重置error delay
                errorDelay = 0;
    
                // 如果处于EndPoint处于running状态并且没有没暂停,Configure the socket
                if (running && !paused) {
                    // setSocketOptions()将把socket传递给适当的处理器。如果成功,会关闭socket。
                    // 否则,在这里关闭socket
                    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;
    }

    看的出来,Acceptor使用serverSock.accept()阻塞的监听端口,如果有连接进来,拿到了socket,并且EndPoint处于正常运行状态,则调用NioEndPoint的setSocketOptions方法,一顿操作。

    至于setSocketOptions做了什么,概括来说就是根据socket构建一个NioChannel,然后把这个的NioChannel注册到Poller的事件列表里面,等待poller轮询。


    看下setSocketOptions的代码:

    /**
     * 处理指定的连接
     * @param socket The socket channel
     * @return  
     *  如果socket配置正确,并且可能会继续处理,返回true 
     *  如果socket需要立即关闭,则返回false
     */
    protected boolean setSocketOptions(SocketChannel socket) {
        // Process the connection
        try {
            //非阻塞模式
            socket.configureBlocking(false);
            Socket sock = socket.socket();
            socketProperties.setProperties(sock);
    		
    		//从缓存中拿一个nioChannel  若没有,则创建一个。将socket传进去
            NioChannel channel = nioChannels.pop();
            if (channel == null) {
                SocketBufferHandler bufhandler = new SocketBufferHandler(
                        socketProperties.getAppReadBufSize(),
                        socketProperties.getAppWriteBufSize(),
                        socketProperties.getDirectBuffer());
                if (isSSLEnabled()) {
                    channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
                } else {
                    channel = new NioChannel(socket, bufhandler);
                }
            } else {
                channel.setIOChannel(socket);
                channel.reset();
            }
    		//从pollers数组中获取一个Poller对象,注册这个nioChannel
            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;
    }

    显然,下面的重点就是register这个方法了。这个方法是NioEndPoint中的Poller实现的,主要干的事就是在Poller注册新创建的套接字。
    /**
     * 使用轮询器注册新创建的socket
     *
     * @param socket    新创建的socket
     */
    public void register(final NioChannel socket) {
        socket.setPoller(this);
    	//创建一个NioSocketWrapper,包装一下socket。然后一顿设置。
        NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
        socket.setSocketWrapper(ka);
        ka.setPoller(this);
        ka.setReadTimeout(getSocketProperties().getSoTimeout());
        ka.setWriteTimeout(getSocketProperties().getSoTimeout());
        ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
        ka.setSecure(isSSLEnabled());
        ka.setReadTimeout(getConnectionTimeout());
        ka.setWriteTimeout(getConnectionTimeout());
    
    
    	//从缓存中取出一个PollerEvent对象,若没有则创建一个。将socket和NioSocketWrapper设置进去
        PollerEvent r = eventCache.pop();
        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);
    
    
    	//添到到该Poller的事件列表
        addEvent(r);
    }

    总结一下,从Acceptor接收到请求,它做了这么些工作:

    • 如果达到了最大连接数,则等待。否则,阻塞监听端口。
    • 监听到有连接,则创建一个socketChannel。若服务正常运行,则把socket传递给适当的处理器。如果成功,会关闭socket。


    在这里,适当的处理是指调用NioEndPoint的setSocketOptions方法,处理指定的连接:

    • 将socket设置为非阻塞
    • 从缓存中拿一个nioChannel  若没有,则创建一个。将socket传进去。
    • 从pollers数组中获取一个Poller对象,把nioChannel注册到该Poller中。


    其中最后一步注册的过程,是调用Poller的register()方法:

    • 创建一个NioSocketWrapper,包装socket。然后配置相关属性,设置感兴趣的操作为SelectionKey.OP_READ
    • PollerEvent。PollerEvent可以是从缓存中取出来的,若没有则创建一个。初始化或者重置此Event对象,设置感兴趣的操作为OP_REGISTER (Poller轮询时会用到)
    • 将新的PollerEvent添加到这个Poller的事件列表events,等待Poller线程轮询。


    Poller轮询


    其实上面已经提到了Poller将一个事件注册到事件队列的过程。接下来便是Poller线程如何处理这些事件了,这就是Poller线程的工作机制。

    Poller作为一个线程,实现了Runnable接口的run方法,在run方法中会轮询事件队列events,将每个PollerEvent中的SocketChannel感兴趣的事件注册到Selector中,然后将PollerEvent从队列里移除。之后就是SocketChanel通过Selector调度来进行非阻塞的读写数据了。

    看下Poller.run()代码:

    /**
     * The background thread that adds sockets to the Poller, checks the
     * poller for triggered events and hands the associated socket off to an
     * appropriate processor as events occur.
     */
    @Override
    public void run() {
        // 循环直到 destroy() 被调用
        while (true) {
    
            boolean hasEvents = false;
    
            try {
                if (!close) {
    				//将events队列,将每个事件中的通道感兴趣的事件注册到Selector中
                    hasEvents = events();
                    if (wakeupCounter.getAndSet(-1) > 0) {
                        //如果走到了这里,代表已经有就绪的IO通道
                        //调用非阻塞的select方法,直接返回就绪通道的数量
                        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 (Throwable x) {
                ExceptionUtils.handleThrowable(x);
                log.error("",x);
                continue;
            }
            //如果上面select方法超时,或者被唤醒,先将events队列中的通道注册到Selector上。
            if ( keyCount == 0 ) hasEvents = (hasEvents | events());
    
            Iterator<SelectionKey> iterator =
                keyCount > 0 ? selector.selectedKeys().iterator() : null;
            // 遍历已就绪的通道,并调用processKey来处理该Socket的IO。
            while (iterator != null && iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
                // 如果其它线程已调用,则Attachment可能为空
                if (attachment == null) {
                    iterator.remove();
                } else {
                    iterator.remove();
    				//创建一个SocketProcessor,放入Tomcat线程池去执行
                    processKey(sk, attachment);
                }
            }//while
    
            //process timeouts
            timeout(keyCount,hasEvents);
        }//while
    
        getStopLatch().countDown();
    }

    读取已就绪通道的部分,是常见的Java NIO的用法,Selector调用selectedKeys(),获取IO数据已经就绪的通道,遍历并调用processKey方法来处理每一个通道就绪的事件。而processKey方法会创建一个SocketProcessor,然后丢到Tomcat线程池中去执行。

    其中需要注意的一个点是,events()方法,用来处理PollerEvent事件,执行PollerEvent.run(),然后将PollerEvent重置再次放入缓存中,以便对象复用。

    /**
     * Processes events in the event queue of the Poller.
     *
     * @return <code>true</code> if some events were processed,
     *   <code>false</code> if queue was empty
     */
    public boolean events() {
        boolean result = false;
    
        PollerEvent pe = null;
        while ( (pe = events.poll()) != null ) {
            result = true;
            try {
    			//把SocketChannel感兴趣的事件注册到Selector中
                pe.run();
                pe.reset();
                if (running && !paused) {
                    eventCache.push(pe);
                }
            } catch ( Throwable x ) {
                log.error("",x);
            }
        }
    
        return result;
    }
    可以看出,PollerEvent.run()方法才是重点:
    public void run() {
    	//Acceptor调用Poller.register()方法时,创建的PollerEvent感兴趣的事件为OP_REGISTER,因此走这个分支
        if (interestOps == OP_REGISTER) {
            try {
    			//将SocketChannel的读事件注册到Poller线程的Selector中,使用Selector来调度IO。
                socket.getIOChannel().register(
                        socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper);
            } catch (Exception x) {
                log.error(sm.getString("endpoint.nio.registerFail"), x);
            }
        } else {
            final SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
            try {
                if (key == null) {
                    // The key was cancelled (e.g. due to socket closure)
                    // and removed from the selector while it was being
                    // processed. Count down the connections at this point
                    // since it won't have been counted down when the socket
                    // closed.
                    socket.socketWrapper.getEndpoint().countDownConnection();
                } else {
                    final NioSocketWrapper socketWrapper = (NioSocketWrapper) key.attachment();
                    if (socketWrapper != null) {
                        //we are registering the key to start with, reset the fairness counter.
                        int ops = key.interestOps() | interestOps;
                        socketWrapper.interestOps(ops);
                        key.interestOps(ops);
                    } else {
                        socket.getPoller().cancelledKey(key);
                    }
                }
            } catch (CancelledKeyException ckx) {
                try {
                    socket.getPoller().cancelledKey(key);
                } catch (Exception ignore) {}
            }
        }
    }

    至此,可以看出Poller线程的作用

    • 将Acceptor接收到的请求注册到Poller的事件队列中
    • Poller轮询事件队列中,处理到达的事件,将PollerEvent中的通道注册到Poller的Selector中
    • 轮询已就绪的通道,对每个就绪通道创建一个SocketProcessor,交个Tomcat线程池去处理


    剩下的事情,就是SocketProcessor怎么适配客户端发来请求的数据、然后怎样交给Tomcat容器去处理了。


    SocketProcessor处理请求


    简单提一下SocketProcessor的处理过程,不是这篇文章的重点。通过上面可以知道,具体处理一个请求,是在SocketProcessor通过线程池去执行的。执行一次请求的时序图




    SocketProcessor中通过Http11ConnectionHandler,取到Htpp11Processor,Htpp11Processor调用prepareRequest方法,准备好请求数据。然后调用CoyoteAdapter的service方法进行request和response的适配,之后交给容器进行处理。


    在CoyoteAdapter的service方法中,主要干了2件事:

    • org.apache.coyote.Request -> org.apache.catalina.connector.Request extends HttpServletRequest,org.apache.coyote.Response -> org.apache.catalina.connector. Response extends HttpServletResponse
    • 将请求交给StandardEngineValue处理


    将请求交给Tomcat容器处理后,后将请求一层一层传递到Engin、Host、Context、Wrapper,最终经过一系列Filter,来到了Servlet,执行我们自己具体的代码逻辑。其中,容器之间数据的传递用到了管道流的机制。这里就不在赘述,以后有时间专门写一篇Tomcat容器的工作原理。


    参考文章:

    《Tomcat内核设计剖析》

    深度解读Tomcat中的NIO模型


    展开全文
  • Tomcat架构解析之NIONIO2

    千次阅读 2019-08-02 15:38:22
    为了提高I/O性能,JDK与1.4版本引入NIo,他弥补了原来BIO方式的不足,在标准的Java代码中提供了高速、面向块的I/O。通过定义包含数据的类以及块的形式处理数据,NIO可以再不编写表弟代码的气哭下利用底层优化,这是...

    一、前言

        传统的BIO方式是基于流行进行读写的,而且是阻塞的,整体性能比较差。为了提高I/O性能,JDK与1.4版本引入NIo,他弥补了原来BIO方式的不足,在标准的Java代码中提供了高速、面向块的I/O。通过定义包含数据的类以及块的形式处理数据,NIO可以再不编写表弟代码的气哭下利用底层优化,这是BIO无法做到的。

    二、NIO

        与BIO相比,NIO有如下几个新的概念:
        1.通道
        通道(Channel)是对BIO中流的模拟,到任何目的地(或者来自任何地方)的所有数据都必须通过一个通道对象。

        通道与流的不同之处在于通道是双向的。流只是在一个方向上移动(一个流要么用于读,要么用于写),而通道可以用于读、写或者同事用于读写。因为通道是双向的,所以他可以比流更好的反应底层操作系统的真实情况(特别是在UNIX模型中底层操作系统通道同样是双向的情况下)。

        2.缓冲区
        尽管通道用于读写数据,但是我们却并吧直接操作通道进行读写,而是通过缓冲区(Buffer)完成。缓冲区实质上是一个容器对象。发送给通道的所有对象都必须先放到缓冲区中,同样从通道中读取的任何数据都要先读到缓冲区中。

        缓冲区体现了NIO与BIO的一个重要区别。在BIO中,读写可以直接操作流对象。简单讲,缓冲区通常是一个字节数组,也可以使用其他类型的数组。但是缓冲区不仅仅是一个数组,他提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

        3.选择器
        Java NIO提供了选择器组件(Selector)用于同时检测多个通道的事件以实现异步I/O。我们将感兴趣的事件注册到Selector上,当事件发生时可以通过Selector获得事件发生的通道,并进行相关的操作。

        异步I/O的一个优势在于,他允许你同时根据大量的输入、输出执行I/O操作。同步I/O一般要借助于轮询,或者创建许许多多的线程以处理大量的链接。使用异步I/O,你可以监听任意数量的通道事件,不必轮询,也不必启动额外的线程。

        由于Selector.select()方法是阻塞的,因此Tomcat采用轮询的方式进行处理,轮询线程称为Poller。每个Poller维护了一个Selector实例以及一个PollerEvent事件队列。每当接收到新的链接时,会将获得的SocketChannel对象封装为org.apache.tomcat.util.net.NioChannel,并且将其注册到Poller(创建一个PollerEvent实例,添加到事件队列)。

        Poller运行时,首先将新添加到队列中的PollerEvent取出,并将SocketChannel的读事件(OP_READ)注册到Poller持有的Selector上,然后执行Selector.select。当捕获到读事件时,构造SocketProcessor,并提交到线程池进行请求处理。

        为了提升对象的利用率,NioEndpoint分别为NioChannel和PollerEvent对象创建了缓存队列。当需要NioChannel和PollerEvent对象时,会检测缓存队列中是否存在可用对象,如果存在则从队列中取出对象并且重置,如果不存在则新建。

        NioEdnpoint的处理过程如下图所示:
    在这里插入图片描述

    • NioEndpoint中ServerSocketChannel是阻塞的。因此,仍采用多线程并发接收客户端链接。
    • NioEndpoint根据pollerThreadCount配置的数量创建Poller线程。与Acceptor相同,Poller线程也是单独启动,不会占用请求处理的线程池。默认Poller线程个数与JVM可以使用的处理器个数相关,上限为2。
    • Acceptor接收到新的链接后,将获得的SocketChannel置为非阻塞,构造NioChannel对象,按照轮转法(Round-Robin)获取Poller实例,并且将NioChannel注册到PollerEvent事件队列。
    • Poller负责为SocketChannel注册读事件。接收到读事件后,由SocketProcessor完成客户端请求处理。

        Poller在将SocketProcessor添加到请求处理线程池之前,会将接收到读事件的SocketChannel从Poller维护的Selector上取消注册,避免当前Socket多线程同时处理。而读写过程中的事件处理则是由NioSelectorPool完成的。事件变化如下图所示:
    在这里插入图片描述
        NioSelectorPool提供了一个Selector池,用于获取有效的Selector供SocketChannel读写使用。他由NioEndpoint维护,可以通过系统属性org.apache.tomcat.util.net.NioSelcetorShard配置是否在SocketChannel之间共享Selector,如果为true则所有SocketChannel均共享一个Selector实例,否则每一个SocketChannel使用不同的Selector,NioSelectorPool池维护的Selector实例数上限由属性maxSelectors确定。

        NIOSelectorPool读信息分为阻塞和非阻塞两种方式:

    • 在阻塞模式下,如果第一次读取不到数据,则会在NioSelectorPool提供的Selector对象上注册OP_READ事件,并循环调用Selector.select,超时等待OP_READ事件。如果OP_READ事件就绪,则进行数据读取。
    • 在非阻塞模式下,如果读不到数据,则直接返回。

        同样,在NioEndpoint中写详细也分为阻塞和非阻塞两种方式:

    • 在阻塞模式下,如果第一次写数据没有成功,则会在NioSelectorPool提供的Selector对象上注册OP_WRITE事件,并循环调用Selector.select()方法,超时等待OP_WRITE事件。如果OP_WRITE事件就绪,则会进行写数据操作。
    • 在非阻塞模式下,写数据之前不会监听OP_WRITE事件。如果没有成功,则直接返回。

        综上可知,Tomcat在阻塞方式下读/写时并没有监听OP_READ/OP)WRITE事件,而是当第一次操作没有成功时再进行注册。这实际上是一种乐观设计,即假设网络大多数情况下是正常的。第一次操作不成功,则表明网络存在异常,此时再对事件进行监听。

    三、NIO2

        NIO2是JDK7新增的文件及网络I/O特性,他继承自NIO,同时添加了众多特性及功能改进,其中最重要的即是对异步I/O(AIO)的支持。

        1.通道
        在AIO中,通道必须实现接口java.nio.channels.AsynchronousChannel。JDK7提供了3个通道实现类:java.nio.channels.AsynchronousFileChannel用于文件I/O,Java.nio.channels.AsyschronousServerSocketChannel和java.nio.channels.AsyschronousSocketChannel用于网络I/O。

        2.缓冲区
        AIO仍通过操作缓冲区完成数据的读写操作,此处不再描述。

        3.Future和CompletionHandler
        AIO操作存在两种操作方式:Future和CompletionHandler。

        首先,AIO使用了Java并发包的API,无论接收Socket请求还是读写操作,均可以返回一个java.util.concurrent.Future对象来表示I/O处于等待状态。通过Future的方法,我们可以检测操作是否完成(isDone)、等待完成并取得操作结果(get)等。当接收请求(accept)结束时,Future.get返回值为AsynchronousSocketChannel;读写操作时(read/write),Future.get返回值为读写操作结果。

        除了Future外,接收请求以及读写操作还支持指定一个java.nio.channels.CompletionHandler<V,A>接口(此时不再返回Future对象),当I/O操作完成时,会调用接口的completed()方法,当操作失败时,则会调用failed()方法。

        比较两种操作方式,Future方式需要我们自己检测I/O操作状态或者直接通过Future.get()方法等待I/O操作结束,而CompletionHandler方式则由JDK检测I/O状态,我们需要实现每种操作状态的处理即可。在实际应用中,我们可以只采用Future方式或者CompletionHandler方式,也可以两者混合使用。

        4.异步通道组
        AIO新引入了异步通道组(Asynchronous Channel Group)的概念,每个异步通道均属于一个指定的异步通道组,同一个通道组内的通道共享一个线程池。线程池内的线程接收指令来执行I/O事件并将结果分发到CompletionHandler。异步通道组包括线程池以及所有通道工作线程共享的资源。通道生命周期受所属通道组影响,当通道组关闭后,通道也随着关闭。

        在实际开发中,除了可以手动创建异步通道组外,JVM还维护了一个系统分为的通道组实例,作为默认通道组。如果创建通道时为指定通道组或者指定的通道组为空,那么将会使用默认通道组。

        默认通道组通过两个系统属性进行配置。首先是java.nio.channels.DefaultThreadPool.threadFactory,该属性值为具体的java.util.concurrent.ThreadFactory类,由系统类加载器加载并且实例化,用于创建默认通道组线程池的线程。其次为java.nio.channels.DefaultThreadPool.initialSize,用于指定线程池的初始化大小。

        如果默认 通道组不能满足需要,我们还可以通过AsynchronousChannelGroup的下列3个方法来创建自定义的通道组:

    • withFixedThreadPool用于创建固定大小的线程池,固定大小的线程池适合简单的场景:一个线程等待I/O事件、完成I/O事件、执行CompletionHandler(内核将事件直接分发到这些线程)。当CompletionHandler正常终止,线程将返回线程池并且等待下一个事件。但是如果CompletionHandler未能及时完成,他将会阻塞处理线程。如果所有线程均在CompletionHandler内部阻塞,整个应用将会被阻塞。此时所有新事件均会排队等待,知道有一个线程变为有效。最糟糕的场景是没有线程被释放,内核将不再执行任何操作。这个问题避免的方法是在CompletionHandler内部不采用阻塞或者长时间的操作,也可以使用一个缓存线程池或者超时时间来避免这个问题。
    • withCachedThreadPool用于创建缓存线程池。异步通道组提交时间到线程池,线程池知识简单地执行CompletionHandler的方法。此时大家会有疑问,如果线程只是简单地执行CompletionHandler的方法,那么是谁执行具体的I/O操作?答案是隐藏线程池。这是一组独立的线程用于等待I/O事件。更准确的讲,内核I/O操作由一个或者多个不可见的内部线程处理并且将事件分发到缓存线程池,缓存线程池依次执行CompletionHandler。隐藏线程池非常重要,因为他显著降低了应用程序阻塞的可能性(解决了固定大小线程池的问题),确保内核能够完成I/O操作。但是他仍存在一个问题,由于缓存线程池需要无边界的队列,这将使队列无限制的增长并最终导致outOfMemoryError。因此仍需要注意避免CompletionHanler中的阻塞以及长时间的操作。

        Tomcat中AIO的使用可以创建Nio2Endpoint。与BIO、NIO类似,Tomcat仍使用Acceptor线程池的方式接收客户端请求。在Acceptor中,采用Fy=uture方式进行请求接收。此外,Tomcat分别采用Future方式实现阻塞读写,采用CompletionHandler方式实现非阻塞读写。

    • Nio2Endpoint创建异步通道时,指定了自定义异步通道组,并且使用的是请求处理线程池。
    • Nio2Endpoint中接收请求仍采用多线程处理,以Future的方式阻塞调用。
    • 当接收到请求后,构造Nio2SocketWrapper以及SocketProcessor并且提交到请求处理线程池,最终由Http11Nio2Processor(HTTP协议)完成请求处理。
    • Nio2Endpoint通过Nio2Channel封装了AsynchronousSocketChannel和读写ByteBugger,并提供了Nio2Channel缓存以实现ByteBuffer的重复利用。当接收到客户端请求后,Nio2Endpoint先从缓存中查找可用的Nio2Channel。如果存在,则使用当前的AsynchronousSocketChannel进行重置,否则创建新的Nio2Channel实例。
    • Nio2Endpoint只有在读取请求头时采用非阻塞方式,即CompletionHandler。在读取请求体和写响应均采用阻塞方式,即为Futrue。

        Enio2Endpoint的处理过程如下图所示:
    在这里插入图片描述

    展开全文
  • springboot内置tomcatNIO处理流程详解

    千次阅读 2020-07-30 23:59:03
    springboot内置的tomcat目前默认是基于NIO来实现的,本文介绍下tomcat接受请求的一些组件及组件之间的关联 tomcat组件 本文只介绍NIOtomcat的组件 我们直接看NIO的核心类NioEndpoint的startInternal方法 Acceptor...

    前言

    springboot内置的tomcat目前默认是基于NIO来实现的,本文介绍下tomcat接受请求的一些组件及组件之间的关联

    tomcat组件

    本文只介绍NIO中tomcat的组件
    我们直接看NIO的核心类NioEndpoint的startInternal方法

    Acceptor组件

    public void startInternal() throws Exception {
    
            if (!running) {
                running = true;
                paused = false;
    
                processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getProcessorCache());
                eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                                socketProperties.getEventCache());
                nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getBufferPool());
    
                // Create worker collection
                if ( getExecutor() == null ) {
                    createExecutor();
                }
    
                initializeConnectionLatch();
    
                // Start poller threads
                // 核心代码1
                pollers = new Poller[getPollerThreadCount()];
                for (int i=0; i<pollers.length; i++) {
                    pollers[i] = new Poller();
                    Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                    pollerThread.setPriority(threadPriority);
                    pollerThread.setDaemon(true);
                    pollerThread.start();
                }
    
    			// 核心代码2
                startAcceptorThreads();
            }
        }
    

    看核心代码1的位置构造了一个Poller数组,Poller是一个实现了Runnable的类,并且启动了该线程类,
    getPollerThreadCount()方法返回了2和当前物理机CPU内核数的最小值,即创建的数组最大值为2

    接下来看核心代码2startAcceptorThreads()

    protected final void startAcceptorThreads() {
        int count = getAcceptorThreadCount();
        acceptors = new ArrayList<>(count);
    
        for (int i = 0; i < count; i++) {
            Acceptor<U> acceptor = new Acceptor<>(this);
            String threadName = getName() + "-Acceptor-" + i;
            acceptor.setThreadName(threadName);
            acceptors.add(acceptor);
            Thread t = new Thread(acceptor, threadName);
            t.setPriority(getAcceptorThreadPriority());
            t.setDaemon(getDaemon());
            t.start();
        }
    }
    

    创建了多个Acceptor类,Acceptor也是实现了Runnable的线程类,创建个数默认是1
    然后我们看下Acceptor启动后做了什么,我们直接看run方法

      public void run() {
    
            ......
    
                    U socket = null;
                    try {
                        // Accept the next incoming connection from the server
                        // socket
                        // 核心代码1
                        socket = endpoint.serverSocketAccept();
                    } catch (Exception ioe) {
                        ......
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;
    
                    // Configure the socket
                    if (endpoint.isRunning() && !endpoint.isPaused()) {
                        // setSocketOptions() will hand the socket off to
                        // an appropriate processor if successful
                        // 核心代码2
                        if (!endpoint.setSocketOptions(socket)) {
                            endpoint.closeSocket(socket);
                        }
                   ......
        }
    

    核心代码1很明显是一个阻塞模型,即接受客户端连接的,当没有客户端连接时会处于阻塞,这里可以看到默认情况下tomcat在nio模式下只有一个Acceptor线程类来接受连接

    然后看核心代码2

     protected boolean setSocketOptions(SocketChannel socket) {
            // Process the connection
            try {
                //disable blocking, APR style, we are gonna be polling it
                socket.configureBlocking(false);
                Socket sock = socket.socket();
                socketProperties.setProperties(sock);
    
                NioChannel channel = nioChannels.pop();
                if (channel == null) {
                    SocketBufferHandler bufhandler = new SocketBufferHandler(
                            socketProperties.getAppReadBufSize(),
                            socketProperties.getAppWriteBufSize(),
                            socketProperties.getDirectBuffer());
                    if (isSSLEnabled()) {
                        channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
                    } else {
                        channel = new NioChannel(socket, bufhandler);
                    }
                } else {
                    channel.setIOChannel(socket);
                    channel.reset();
                }
                // 核心代码
                getPoller0().register(channel);
            } catch (Throwable t) {
               ......
            }
            return true;
        }
    

    我们看核心代码getPoller0().register(channel)

    public void register(final NioChannel socket) {
            socket.setPoller(this);
            NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
            socket.setSocketWrapper(ka);
            ka.setPoller(this);
            ka.setReadTimeout(getConnectionTimeout());
            ka.setWriteTimeout(getConnectionTimeout());
            ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
            ka.setSecure(isSSLEnabled());
            PollerEvent r = eventCache.pop();
            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);
            // 核心代码
            addEvent(r);
        }
    

    看addEvent

      private void addEvent(PollerEvent event) {
            events.offer(event);
            if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();
        }
    

    events的定义

    private final SynchronizedQueue<PollerEvent> events =
                    new SynchronizedQueue<>();
    

    这里可以看到封装了一个PollerEvent 并且扔到了一个队列里面,然后当前类就结束了

    由此可得Acceptor的作用就是接受客户端连接,并且把连接封装起来扔到了一个队列中

    Poller

    我们前面已经创建并且启动了多个Poller线程类,默认的数量是小于等于2的。
    然后我们看下Poller类做了什么,同样我们看run方法

      @Override
        public void run() {
            // Loop until destroy() is called
            while (true) {
    
                boolean hasEvents = false;
    
                try {
                    if (!close) {
                    	// 核心代码1
                        hasEvents = events();
                        
                .......
                
                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();
                    NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
                    // Attachment may be null if another thread has called
                    // cancelledKey()
                    if (attachment == null) {
                        iterator.remove();
                    } else {
                        iterator.remove();
                        // 核心代码2
                        processKey(sk, attachment);
                    }
                }//while
    
                //process timeouts
                timeout(keyCount,hasEvents);
            }//while
    
            getStopLatch().countDown();
        }
    

    先看核心代码1 hasEvents = events()

     public boolean events() {
            boolean result = false;
    
            PollerEvent pe = null;
            for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
                result = true;
                try {
                	// 核心代码
                    pe.run();
                    pe.reset();
                    if (running && !paused) {
                        eventCache.push(pe);
                    }
                } catch ( Throwable x ) {
                    log.error("",x);
                }
            }
    
            return result;
        }
    

    核心代码run

    @Override
        public void run() {
            if (interestOps == OP_REGISTER) {
                try {
                	// 核心代码,注册到selector轮训器
                    socket.getIOChannel().register(
                            socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper);
                } catch (Exception x) {
                    log.error(sm.getString("endpoint.nio.registerFail"), x);
                }
            } else {
                ......
            }
        }
    

    可以看出大概的意思就是从刚才我们放进去的队列events里面取数据放到了eventCache里面,eventCache的定义SynchronizedStack eventCache,当取到数据后返回true,这个时候就会进入核心代码2处的processKey(sk, attachment),也就是开始处理请求了

    protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
            try {
                if ( close ) {
                    cancelledKey(sk);
                } else if ( sk.isValid() && attachment != null ) {
                    if (sk.isReadable() || sk.isWritable() ) {
                        if ( attachment.getSendfileData() != null ) {
                            processSendfile(sk,attachment, false);
                        } else {
                            unreg(sk, attachment, sk.readyOps());
                            boolean closeSocket = false;
                            // Read goes before write
                            if (sk.isReadable()) {
                            	// 核心代码
                                if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
                                    closeSocket = true;
                                }
                            }
                            if (!closeSocket && sk.isWritable()) {
                                if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
                                    closeSocket = true;
                                }
                            }
                          ......
        }
    

    这里就可以看到我们的NIO的模型了,也就是多路复用的模型,轮询来判断key的状态,当key是可读或者可写时执行processSocket

      public boolean processSocket(SocketWrapperBase<S> socketWrapper,
                SocketEvent event, boolean dispatch) {
            try {
                if (socketWrapper == null) {
                    return false;
                }
                SocketProcessorBase<S> sc = processorCache.pop();
                if (sc == null) {
                    sc = createSocketProcessor(socketWrapper, event);
                } else {
                    sc.reset(socketWrapper, event);
                }
                // 核心代码
                Executor executor = getExecutor();
                if (dispatch && executor != null) {
                    executor.execute(sc);
                } else {
                    sc.run();
                }
            } catch (RejectedExecutionException ree) {
                getLog().warn(sm.getString("endpoint.executor.fail", socketWrapper) , ree);
                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
                getLog().error(sm.getString("endpoint.process.fail"), t);
                return false;
            }
            return true;
        }
    

    这里就是核心代码了,可以看到getExecutor()方法,获取线程池,这个线程池是在初始化tomcat时提前初始化好的,默认情况下核心线程是10,最大线程是200。线程池的配置可以根据我们自己配置来设置大小。

    这里拿到线程池然后包装了一个SocketProcessorBase线程类扔到线程池里面取执行

    从这里可以看到Poller的功能就是从前面的队列里面获取连接然后包装成SocketProcessorBase之后扔到线程池里面去执行,SocketProcessorBase才是最终真正处理请求的

    总结

    根据上面的分析我们已经可以看到tomcat的执行流程了,这里盗用网上的一张比较好的图
    在这里插入图片描述
    大致流程为:
    1、创建一个Acceptor线程来接收用户连接,接收到之后扔到events queue队列里面,默认情况下只有一个线程来接收
    2、创建Poller线程,数量小于等于2,Poller对象是NIO的核心,在Poller中,维护了一个Selector对象;当Poller从队列中取出socket后,注册到该Selector中;然后通过遍历Selector,找出其中可读的socket,然后扔到线程池中处理相应请求,这就是典型的NIO多路复用模型
    3、扔到线程池中的SocketProcessorBase处理请求

    相较于BIO模型的tomcat,NIO的优势分析:
    1、BIO中的流程应该是接收到请求之后直接把请求扔给线程池去做处理,在这个情况下一个连接即需要一个线程来处理,线程既需要读取数据还需要处理请求,线程占用时间长,很容易达到最大线程

    2、NIO的流程的不同点在于Poller类采用了多路复用模型,即Poller类只有检查到可读或者可写的连接时才把当前连接扔给线程池来处理,这样的好处是大大节省了连接还不能读写时的处理时间(如读取请求数据),也就是说NIO“读取socket并交给Worker中的线程”这个过程是非阻塞的,当socket在等待下一个请求或等待释放时,并不会占用工作线程,因此Tomcat可以同时处理的socket数目远大于最大线程数,并发性能大大提高。

    以上就是笔者对于tomcat中nio处理模型的一些理解

    展开全文
  • Java NIOTomcat 原理理解

    千次阅读 2015-12-11 11:47:17
    tomcat的运行模式有3种.修改他们的运行模式.3种模式的运行是否成功,可以看他的启动控制台,或者启动日志.或者登录他们的默认页面http://localhost:8080/查看其中的服务器状态。 1)bio 默认的模式,性能非常低下,没有...

    源链接:http://m.oschina.net/blog/163549

    tomcat的运行模式有3种.修改他们的运行模式.3种模式的运行是否成功,可以看他的启动控制台,或者启动日志.或者登录他们的默认页面http://localhost:8080/查看其中的服务器状态。

    1)bio

    默认的模式,性能非常低下,没有经过任何优化处理和支持.

    2)nio

    利用java的异步io护理技术,no blocking IO技术.

    想运行在该模式下,直接修改server.xml里的Connector节点,修改protocol为

    <Connector port="80" protocol="org.apache.coyote.http11.Http11NioProtocol" 
    	connectionTimeout="20000" 
    	URIEncoding="UTF-8" 
    	useBodyEncodingForURI="true" 
    	enableLookups="false" 
    	redirectPort="8443" />

    启动后,就可以生效。

    3)apr

    安装起来最困难,但是从操作系统级别来解决异步的IO问题,大幅度的提高性能.

    必须要安装apr和native,直接启动就支持apr。下面的修改纯属多余,仅供大家扩充知识,但仍然需要安装apr和native

    如nio修改模式,修改protocol为org.apache.coyote.http11.Http11AprProtocol

     

    Tomcat 6.X实现了JCP的Servlet 2.5和JSP2.1的规范,并且包括其它很多有用的功能,使它成为开发
    和部署web应用和web服务的坚实平台。
           NIO (No-blocking I/O)从JDK 1.4起,NIO API作为一个基于缓冲区,并能提供非阻塞I/O操作的API
    被引入。 


           作为开源web服务器的java实现,tomcat几乎就是web开发者开发、测试的首选,有很多其他商业服务
    器的开发者也会优先选择tomcat作为开发时候使用,而在部署的时候,把应用发布在商业服务器上。也有
    许多商业应用部署在tomcat上,tomcat承载着其核心的应用。但是很多开发者很迷惑,为什么在自己的应
    用里使用tomcat作为平台的时候,而并发用户超过一定数量,服务器就变的非常繁忙,而且很快就出现了
    connection refuse的错误。但是很多商业应用部署在tomcat上运行却安然无恙。

          其中有个很大的原因就是,配置良好的tomcat都会使用APR(Apache Portable Runtime),APR是
    Apache HTTP Server2.x的核心,它是高度可移植的本地库,它使用高性能的UXIN I/O操作,低性能的
    java io操作,但是APR对很多Java开发者而言可能稍稍有点难度,在很多OS平台上,你可能需要重新编
    译APR。但是从Tomcat6.0以后, Java开发者很容易就可以是用NIO的技术来提升tomcat的并发处理能力。
    但是为什么NIO可以提升tomcat的并发处理能力呢,我们先来看一下java 传统io与 java NIO的差别。
         
    Java 传统的IO操作都是阻塞式的(blocking I/O), 如果有socket的编程基础,你会接触过堵塞socket和
    非堵塞socket,堵塞socket就是在accept、read、write等IO操作的的时候,如果没有可用符合条件的资
    源,不马上返回,一直等待直到有资源为止。而非堵塞socket则是在执行select的时候,当没有资源的时
    候堵塞,当有符合资源的时候,返回一个信号,然后程序就可以执行accept、read、write等操作,一般来
    说,如果使用堵塞socket,通常我们通常开一个线程accept socket,当读完这次socket请求的时候,开一
    个单独的线程处理这个socket请求;如果使用非堵塞socket,通常是只有一个线程,一开始是select状,
    当有信号的时候可以通过 可以通过多路复用(Multiplexing)技术传递给一个指定的线程池来处理请求,然
    后原来的线程继续select状态。 最简单的多路复用技术可以通过java管道(Pipe)来实现。换句话说,如果
    客户端的并发请求很大的时候,我们可以使用少于客户端并发请求的线程数来处理这些请求,而这些来不
    及立即处理的请求会被阻塞在java管道或者队列里面,等待线程池的处理。请求 听起来很复杂,在这个架
    构当道的java 世界里,现在已经有很多优秀的NIO的架构方便开发者使用,比如Grizzly,Apache Mina等
    等,如果你对如何编写高性能的网络服务器有兴趣,你可以研读这些源代码。

          简单说一下,在web服务器上阻塞IO(BIO)与NIO一个比较重要的不同是,我们使用BIO的时候往往会
    为每一个web请求引入多线程,每个web请求一个单独的线程,所以并发量一旦上去了,线程数就上去
    了,CPU就忙着线程切换,所以BIO不合适高吞吐量、高可伸缩的web服务器;而NIO则是使用单线程(单
    个CPU)或者只使用少量的多线程(多CPU)来接受Socket,而由线程池来处理堵塞在pipe或者队列里的请
    求.这样的话,只要OS可以接受TCP的连接,web服务器就可以处理该请求。大大提高了web服务器的可
    伸缩性。

        我们来看一下配置,你只需要在server.xml里把 HTTP Connector做如下更改,

        <Connector port="8080" protocol="HTTP/1.1" 
                   connectionTimeout="20000" 
                   redirectPort="8443" />
        改为
        <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" 
                   connectionTimeout="20000" 
                   redirectPort="8443" />

    然后启动服务器,你会看到org.apache.coyote.http11.Http11NioProtocol start的信息,表示NIO已经启动。其他的配置请参考官方配置文档。

    Enjoy it.

    最后贴上官方文档上对tomcat的三种Connector的方式做一个简单比较,
        

    Java Blocking Connector       Java Nio Blocking Connector       APR Connector
    
    Classname         Http11Protocol                  Http11NioProtocol         Http11AprProtocol
    
    Tomcat Version   3.x 4.x 5.x 6.x                       6.x                     5.5.x 6.x
    
    Support Polling         NO                             YES                        YES
    
    Polling Size           N/A                   Unlimited - Restricted by mem        Unlimited
    
    Read HTTP Request     Blocking                     Blocking                       Blocking
    
    Read HTTP Body        Blocking                     Blocking                       Blocking
    
    Write HTTP Response   Blocking                     Blocking                       Blocking
    
    SSL Support           Java SSL                     Java SSL                       OpenSSL
    
    SSL Handshake         Blocking                     Non blocking                   Blocking
    
    Max Connections       maxThreads                   See polling size               See polling size
     
     
    如果读者有socket的编程基础,应该会接触过堵塞socket和非堵塞socket,堵塞socket就是在accept、read、write等IO操作的的时候,如果没有可用符合条件的资源,不马上返回,一直等待直到有资源为止。而非堵塞socket则是在执行select的时候,当没有资源的时候堵塞,当有符合资源的时候,返回一个信号,然后程序就可以执行accept、read、write等操作,这个时候,这些操作是马上完成,并且马上返回。而windows的winsock则有所不同,可以绑定到一个EventHandle里,也可以绑定到一个HWND里,当有资源到达时,发出事件,这时执行的io操作也是马上完成、马上返回的。一般来说,如果使用堵塞socket,通常我们时开一个线程accept socket,当有socket链接的时候,开一个单独的线程处理这个socket;如果使用非堵塞socket,通常是只有一个线程,一开始是select状态,当有信号的时候马上处理,然后继续select状态。 
       
      按照大多数人的说法,堵塞socket比非堵塞socket的性能要好。不过也有小部分人并不是这样认为的,例如Indy项目(Delphi一个比较出色的网络包),它就是使用多线程+堵塞socket模式的。另外,堵塞socket比非堵塞socket容易理解,符合一般人的思维,编程相对比较容易。 
       
      nio其实也是类似上面的情况。在JDK1.4,sun公司大范围提升Java的性能,其中NIO就是其中一项。Java的IO操作集中在java.io这个包中,是基于流的阻塞API(即BIO,Block IO)。对于大多数应用来说,这样的API使用很方便,然而,一些对性能要求较高的应用,尤其是服务端应用,往往需要一个更为有效的方式来处理IO。从JDK 1.4起,NIO API作为一个基于缓冲区,并能提供非阻塞O操作的API(即NIO,non-blocking IO)被引入。 
       
      BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。  
       
      这个时候,问题就出来了:我们非常多的java应用是使用ThreadLocal的,例如JSF的FaceContext、Hibernate的session管理、Struts2的Context的管理等等,几乎所有框架都或多或少地应用ThreadLocal。如果存在冲突,那岂不惊天动地? 
       
      后来终于在Tomcat6的文档(http://tomcat.apache.org/tomcat-6.0-doc/aio.html)找到答案。根据上面说明,应该Tomcat6应用nio只是用在处理发送、接收信息的时候用到,也就是说,tomcat6还是传统的多线程Servlet,我画了下面两个图来列出区别:      tomcat5:客户端连接到达 -> 传统的SeverSocket.accept接收连接 -> 从线程池取出一个线程 -> 在该线程读取文本并且解析HTTP协议 -> 在该线程生成ServletRequest、ServletResponse,取出请求的Servlet -> 在该线程执行这个Servlet -> 在该线程把ServletResponse的内容发送到客户端连接 -> 关闭连接。      我以前理解的使用nio后的tomcat6:客户端连接到达 -> nio接收连接 -> nio使用轮询方式读取文本并且解析HTTP协议(单线程) -> 生成ServletRequest、ServletResponse,取出请求的Servlet -> 直接在本线程执行这个Servlet -> 把ServletResponse的内容发送到客户端连接 -> 关闭连接。      实际的tomcat6:客户端连接到达 -> nio接收连接 -> nio使用轮询方式读取文本并且解析HTTP协议(单线程) -> 生成ServletRequest、ServletResponse,取出请求的Servlet -> 从线程池取出线程,并在该线程执行这个Servlet -> 把ServletResponse的内容发送到客户端连接 -> 关闭连接。           从上图可以看出,BIO与NIO的不同,也导致进入客户端处理线程的时刻有所不同:tomcat5在接受连接后马上进入客户端线程,在客户端线程里解析HTTP协议,而tomcat6则是解析完HTTP协议后才进入多线程,另外,tomcat6也比5早脱离客户端线程的环境。      实际的tomcat6与我之前猜想的差别主要集中在如何处理servlet的问题上。实际上即使抛开ThreadLocal的问题,我之前理解tomcat6只使用一个线程处理的想法其实是行不同的。大家都有经验:servlet是基于BIO的,执行期间会存在堵塞的,例如读取文件、数据库操作等等。tomcat6使用了nio,但不可能要求servlet里面要使用nio,而一旦存在堵塞,效率自然会锐降。          所以,最终的结论当然是tomcat6的servlet里面,ThreadLocal照样可以使用,不存在冲突。 
    展开全文
  • tomcat8.0.53_windows_X64_个人优化版+垃圾回收机制+乱码问题处理,可以直接部署,也可供参考学习
  • tomcat与netty以及Nio的使用

    千次阅读 2019-04-04 17:42:00
    就IO而言:概念上有5中模型:blocking I/O,nonblocking I/O,I/O multiplexing (select and poll),signal driven I/O (SIGIO),... tomcat就是针对http层的,所以我建议http还是选择tomcat(或者其他成熟的...
  • tomcat有3种运行模式 1、 bio 默认的模式,性能非常低下,没有经过任何优化处理和支持. 2、 nio nio(new I/O),是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、...
  • 关于Tomcat8.5优化中Nio模式问题

    千次阅读 2020-01-04 16:10:08
    警告 [main] org.apache.tomcat.util.net.Nio2Endpoint.bind The NIO2 connector requires an exclusive executor to operate properly on shutdown 原因: protocol,Tomcat 8 设置 nio2 更好:org.apache.coy...
  • tomcat nio与bio技术对比

    千次阅读 2015-10-17 22:03:52
    tomcat的运行模式有3种.修改他们的运行模式.3种模式的运行是否成功,可以看他的启动控制台,或者启动日志.或者登录他们的默认页面http://localhost:8080/查看其中的服务器状态。 1)bio 默认的模式,性能非常低下,...
  • 手写 tomcat nio

    2018-01-22 17:11:01
    手写 tomcat nio http://knight-black-bob.iteye.com/blog/2408450
  • Tomcat8.0起已经默认nio模式,不需要做修改,BIO模式也已经抛弃了,今天主要介绍下tomcat的三种运行模式:BIO、NIO、ARP。 简述及配置运行模式 1、bio:没经过任何优化和处理,几百并发性能极低下。 配置server.xml ...
  • 说它是同步,是因为,这个执行是在...TomcatNIO是基于I/O复用来实现的。对这点一定要清楚,不然我们的讨论就不在一个逻辑线上。下面这张图学习过I/O模型知识的一般都见过,出自《UNIX网络编程》,I/O模型一共有阻塞式
  • tomcat7 配置NIO

    2020-04-26 17:47:59
    I/O类型:磁盘I/O模型,网络I/...NIO(同步非阻塞) 多路复用器轮询有效IO AIO(异步非阻塞) OSI七层模型 应用层:为操作系统或应用程序提供访问网络服务的接口 表示层: 会话层: 传输层: 网络层: 数据链路层: ...
  • tomcat8.5.56_windows_X64_个人优化版+垃圾回收机制+乱码问题处理,可以直接部署,也可供参考学习
  • Tomcat NIO模式源码解读

    2021-04-12 13:39:09
    Tomcat解读前提NIO概述引用总览Spring Boot内置tomcat源码NIO解读启动流程请求返回流程 前提 这里提及前提,那是因为如果没有前提知识,要读懂tomcat是有一定的困难,个人认为要解读tomcat源码之前,最好具备以下...
  • Tomcat的BIO、NIO、APR模式对比与性能测试结果

    万次阅读 多人点赞 2018-05-23 15:16:52
    11.11活动当天,服务器负载过大,导致部分页面出现了不可访问的状态、那后来主管就要求调优了,下面是tomcat bio、nio、apr模式以及后来自己测试的一些性能结果。原理方面的资料都是从网上找的,并且把多个地方的...
  • 为什么要使用NIO 在Java中使用Socket(套接字)实现了基于TCP/IP协议的网络编程。以HTTP协议为例,在HTTP服务器端的开发中,如果不使用NIO该如何实现呢? 单个线程定义客户端连接 使用ServerSocket绑定某个端口号...
  • Tomcat的BIO和NIO问题

    千次阅读 2017-11-05 18:41:15
    前言最近一些朋友通过书籍找到我,问了一些关于tomcat中BIO和NIO的问题,这里列一下方便需要的朋友。后续也将前面有朋友问的问题整理下。。问只把 Tomcat 的 bio 模式改为 nio 模式,是否能提高服务器的吞吐量?发现...
  • Tomcat8 配置NIO

    2016-05-11 15:17:00
    protocol,Tomcat 8 设置 nio2 更好:org.apache.coyote.http11.Http11Nio2Protocol protocol,Tomcat 6 设置 nio 更好:org.apache.coyote.http11.Http11NioProtocol enableLookups,禁用DNS查询 acceptCount...
  • tomat启动nio,apr分析前言好了,正文开始,我们了解一下启动tomcat的日志信息 前言 在正文开始之前,我们先在idea工具中看看启动的信息,顺便看下启动的基本信息 可以看到信息有 tomcat版本 操作系统版本 java版本...
  • Tomcat NIO模式修改

    千次阅读 2017-09-20 17:26:18
    bio(blocking I/O),顾名思义,即阻塞式I/O操作,表示Tomcat使用的是传统的Java I/O操作(即java.io包及其子包)。Tomcat在默认情况下,就是以bio模式运行的。遗憾的是,就一般而言,bio模式性能...nio(new I/O),是Jav
  • Tomcat7 性能优化,提高并发-NIO模式

    千次阅读 2019-03-08 13:17:00
    Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。 利用Java的异步IO处理,可以通过少量的线程处理大量的...
  • 浅析tomcat nio 配置

    2018-03-17 17:50:21
    注:Tomcat 8.5 and 9.0 have completely dropped the BIO connector, default is NIO.Tomcat 6.X实现了JCP的Servlet 2.5和JSP2.1的规范,并且包括其它很多有用的功能,使它成为开发和部署web应用和web服务的坚实...
  • tomcat9使用Nio以及内存修改

    千次阅读 2018-12-11 14:30:57
    org.apache.coyote.http11.Http11NioProtocol:调整工作模式为Nio maxThreads:最大线程数,默认150。增大值避免队列请求过多,导致响应缓慢。 minSpareThreads:最小空闲线程数。 acceptCount:当处理请求...
  • Tomcat NIO 模型的实现

    2019-05-05 12:22:12
    Tomcat 对 BIO 和 NIO 两种模型都进行了实现,其中 BIO 的实现理解起来比较简单,而 NIO 的实现就比较复杂了,并且它跟常用的 Reactor 模型也略有不同,具体设计如下: 可以看出多了一个 BlockPoller 的设计,这是...
  • tomcat7开启nio

    2018-05-09 15:35:00
    useBodyEncodingForURI="true" 开启tomcat服务器可以传递中文参数,可以不开启 加上enableLookups="false";这样就不使用DNS查询,也不会有延迟了 除非需要所有连接到服务器的HTTP客户端的完整主机名称 转载于:...
  • 笔者这几天被NIO、BIO搞的头昏脑胀的,不过最后还是记住了两幅图,在此围绕这两幅图,阐述一下Tomcat中的BIO模式和NIO模式的运行原理。 目录一、Tomcat中的BIO模式二、Tomcat中的NIO模式 一、Tomcat中的BIO模式 ...
  • 4.3 tomcat中的NIO 可以参考前面博文: Tomcat 接收连接的accept流程 简单的说,就是tomcat内部分为两大块: Connector和Container容器 Connector中又分为: Http11NioProtocol NioEndpoint ...
  • 下面小编就为大家分享一篇Tomcat在Linux服务器上的BIO、NIO、APR模式设置方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
  • tomcat bio、nio、apr

    2019-04-18 13:58:00
    Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。 apr   (Apache Portable Runtime/Apache可移植运行库)...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 38,112
精华内容 15,244
关键字:

niotomcat