Tomcat中BIO与NIO

一、 前言

二、 Connector

研究过tomcat的童鞋应该都知道tomcat的容器构造:

image.png

Connector是一个桥梁它把Server和Engine链接了起来,Connector的作用是接受客户端端的请求,然后把请求委托为engine容器去处理。 Connector内部使用endpoint进行处理,根据处理方式的不同分为NioEndpoint,JIoEndpoint,AprEndpoint。

image.png

三、 NioEndpoint

3.1 主流程启动时序

image.png

下面首先分析下套接字绑定代码:

 public void bind() throws Exception {

        //创建服务套接字
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket());
        InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
        serverSock.socket().bind(addr,getBacklog());
        serverSock.configureBlocking(true); //打开阻塞模式
        serverSock.socket().setSoTimeout(getSocketProperties().getSoTimeout());

        // 初始化接受线程个数和轮询链接套接字状态线程个数
        if (acceptorThreadCount == 0) {
            // FIXME: Doesn't seem to work that well with multiple accept threads
            acceptorThreadCount = 1;
        }
        if (pollerThreadCount <= 0) {
            //minimum one poller thread
            pollerThreadCount = 1;
        }
        stopLatch = new CountDownLatch(pollerThreadCount);

        // Initialize SSL if needed
        if (isSSLEnabled()) {
            SSLUtil sslUtil = handler.getSslImplementation().getSSLUtil(this);

            sslContext = sslUtil.createSSLContext();
            sslContext.init(wrap(sslUtil.getKeyManagers()),
                    sslUtil.getTrustManagers(), null);

            SSLSessionContext sessionContext =
                sslContext.getServerSessionContext();
            if (sessionContext != null) {
                sslUtil.configureSessionContext(sessionContext);
            }
            // Determine which cipher suites and protocols to enable
            enabledCiphers = sslUtil.getEnableableCiphers(sslContext);
            enabledProtocols = sslUtil.getEnableableProtocols(sslContext);
        }

        if (oomParachute>0) reclaimParachute(true);
        selectorPool.open();
    }

启动startInternal代码:

 public void startInternal() throws Exception {

        if (!running) {
            running = true;
            paused = false;

            // 创建处理线程池
            if ( getExecutor() == null ) {
                createExecutor();
            }

            initializeConnectionLatch();

            //启动poller 线程,nio需要
            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();
            }

            //启动链接接受线程
            startAcceptorThreads();
        }
    }

需要注意的是startAcceptorThreads里面可以创建多个线程去调用 serverSock.accept()来接受完成了三次握手的链接的socket,但是为什么这里会有多个线程那?平时我们不都是用一个线程?因为这个方法是阻塞的,那么多个线程去接受链接有好处?

答案是肯定的,因为服务套接字调用listen方法后,就可以接受链接了,它会吧接受到了链接放入到TCP缓存队列,当调用accept时候是从这个缓存队列里面获取一个已经完成三次握手的套接字处理,而这个缓存大小是受acceptcount影响的。

3.2 Acceptor线程

accept线程作用是接受客户端发来的请求并放入到事件队列。

image.png

看下代码:

protected class Acceptor extends AbstractEndpoint.Acceptor {

        @Override
        public void run() {

            int errorDelay = 0;

            // 一直循环直到接收到shutdown命令
            while (running) {

                ...

                if (!running) {
                    break;
                }
                state = AcceptorState.RUNNING;

                try {
                    //如果达到max connections个请求则等待
                    countUpOrAwaitConnection();

                    SocketChannel socket = null;
                    try {
                        // 从TCP缓存获取一个完成三次握手的套接字,没有则阻塞
                        // socket
                        socket = serverSock.accept();
                    } catch (IOException ioe) {
                        ...
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                   ....
                } catch (SocketTimeoutException sx) {
                    // Ignore: Normal condition
                ....
            }
            state = AcceptorState.ENDED;
        }
    }

3.3 Poll线程

poll线程作用是从事件队列里面获取事件把链接套接字加入selector,并且监听socket事件进行处理。

image.png

public void run() {
    while (true) {
        try {
            ...
            if (close) {
               ...
            } else {
                hasEvents = events();
            }
            try {
                ...
            } catch ( NullPointerException x ) {...
            }
         
            Iterator<SelectionKey> iterator =
                keyCount > 0 ? selector.selectedKeys().iterator() : null;
            // 遍历所有注册的channel对感兴趣的事件处理
            while (iterator != null && iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                KeyAttachment attachment = (KeyAttachment)sk.attachment();
               
                if (attachment == null) {
                    iterator.remove();
                } else {
                    attachment.access();
                    iterator.remove();
                    processKey(sk, attachment);
                }
            }//while

            //process timeouts
            timeout(keyCount,hasEvents);
            if ( oomParachute > 0 && oomParachuteData == null ) checkParachute();
        } catch (OutOfMemoryError oom) {
            ...
        }
    }//while
    synchronized (this) {
        this.notifyAll();
    }
    stopLatch.countDown();

}
//如配置线程池则请求交给线程池处理。
public boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) {
    try {
        KeyAttachment attachment = (KeyAttachment)socket.getAttachment();
        if (attachment == null) {
            return false;
        }
        attachment.setCometNotify(false); //will get reset upon next reg
        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) {
       ...
    }
    return true;
}

3.4 总结

NIO 是可以多个线程来接受客户端的链接,这个和bio是一样的,不一样在于是NIO会把接受到的链接放入事件队列,然后多个poll线程会从事件队列获取事件,并且NIO可以每个poll线程去监听多个链接socket的事件,然后交给线程池去处理,也就说一个poll 线程可以监听多个socket的读写事件,然后交给线程池去处理,这相比于bio节省了很多线程资源。

四、JioEndpoint

4.1 主启动流程

image.png

首先看下bind函数有啥不同

public void bind() throws Exception {

    // Initialize thread count defaults for acceptor
    if (acceptorThreadCount == 0) {
        acceptorThreadCount = 1;
    }
    // Initialize maxConnections
    if (getMaxConnections() == 0) {
        // User hasn't set a value - use the default
        setMaxConnections(getMaxThreadsExecutor(true));
    }
    
    //创建serversocket工厂
    if (serverSocketFactory == null) {
        if (isSSLEnabled()) {
            serverSocketFactory =
                handler.getSslImplementation().getServerSocketFactory(this);
        } else {
            serverSocketFactory = new DefaultServerSocketFactory(this);
        }
    }

    //创建serversocket
    if (serverSocket == null) {
        try {
            if (getAddress() == null) {
                serverSocket = serverSocketFactory.createSocket(getPort(),
                        getBacklog());
            } else {
                serverSocket = serverSocketFactory.createSocket(getPort(),
                        getBacklog(), getAddress());
            }
        } catch (BindException orig) {
            String msg;
            if (getAddress() == null)
                msg = orig.getMessage() + " <null>:" + getPort();
            else
                msg = orig.getMessage() + " " +
                        getAddress().toString() + ":" + getPort();
            BindException be = new BindException(msg);
            be.initCause(orig);
            throw be;
        }
    }

}

内部实际是调用new ServerSocket 创建seversocket.

4.2 Acceptor线程

image.png

代码如下:

    protected class Acceptor extends AbstractEndpoint.Acceptor {

        @Override
        public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

               ...
                state = AcceptorState.RUNNING;

                try {
                    //if we have reached max connections, wait
                    countUpOrAwaitConnection();

                    Socket socket = null;
                    try {
                        //接受链接
                        socket = serverSocketFactory.acceptSocket(serverSocket);
                    } catch (IOException ioe) {
                       。。。
                    }
                  
                    //配置套接字选项
                    if (running && !paused && setSocketOptions(socket)) {
                        // 处理套接字
                        if (!processSocket(socket)) {
                            countDownConnection();
                            // Close socket right away
                            closeSocket(socket);
                        }
                    } else {
                        countDownConnection();
                        // Close socket right away
                        closeSocket(socket);
                    }
                } catch (IOException x) {
                   。。。
                }
            }
            state = AcceptorState.ENDED;
        }
    }

把设置好套接字选项后的套接字放入线程池处理

 protected boolean processSocket(Socket socket) {
        // Process the request from this socket
        try {
            SocketWrapper<Socket> wrapper = new SocketWrapper<Socket>(socket);
            wrapper.setKeepAliveLeft(getMaxKeepAliveRequests());
            wrapper.setSecure(isSSLEnabled());
            // During shutdown, executor may be null - avoid NPE
            if (!running) {
                return false;
            }
            getExecutor().execute(new SocketProcessor(wrapper));
        } catch (RejectedExecutionException x) ...
        }
        return true;
    }

4.3 总结

可知BIO也可以使用多线程去接受链接,然后把接受的socket放入线程池进行处理,是每个线程处理一个链接sockcet,这是典型的处理方式。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android 研究

OKHttp源码解析(八)--中阶之连接与请求前奏

在http请求中,对于请求速度提升和降低延迟,keepalive在网络连接发挥着重大作用。

3522
来自专栏黑泽君的专栏

day50_BOS项目_02

我们再补上IUserDao和UserDaoImpl的示例代码: IUserDao.java

862
来自专栏熊二哥

快速入门系列--WebAPI--04在老版本MVC4下的调整

WebAPI是建立在MVC和WCF的基础上的,原来微软老是喜欢封装的很多,这次终于愿意将http编程模型的相关细节暴露给我们了。在之前的介绍中,基本上都基于.N...

2456
来自专栏木木玲

Netty 源码解析 ——— NioEventLoop 详解

7234
来自专栏专注 Java 基础分享

Java--JDBC连接数据库

     我们知道Java中的jdbc是用来连接应用程序和数据系统的,本篇文章主要就来看看关于JDBC的实现和使用细节。主要包含以下几点内容: JDBC的基本知...

4015
来自专栏Rindew的iOS技术分享

苏宁一面

1854
来自专栏yukong的小专栏

解开BIO、NIO、AIO神秘的面纱

本文内容涉及同步与异步, 阻塞与非阻塞, BIO、NIO、AIO等概念, 这块内容本身比较复杂, 很难用三言两语说明白. 而书上的定义更不容易理解是什么意思. ...

1374
来自专栏龙首琴剑庐

Java.NIO编程一览笔录

Java标准IO 与 Java NIO 的简单差异示意: Java标准IO Java NIO API调用 简单 复杂 底层实现 面向流(str...

3528
来自专栏腾讯云API

腾讯云API:无服务器函数

无服务器函数是一个很好玩的东西,可以通过这个程序跑一些脚本,在一定程度上,是很方便的。但是作为新鲜事物,一般很难被大家接受,所以,我今天在这里,就做一个小例子,...

9225
来自专栏Star先生的专栏

从源码中分析 Hadoop 的 RPC 机制

RPC是Remote Procedure Call(远程过程调用)的简称,这一机制都要面对两个问题:对象调用方式余与序列/反序列化机制。本文给大家介绍从源码中分...

7320

扫码关注云+社区

领取腾讯云代金券