Tomcat源码阅读系列(四)Connector连接器

本文是Tomcat源码阅读系列的第四篇文章,本系列前三篇文章如下:
Tomcat源码阅读系列(一)使用IntelliJ IDEA运行Tomcat6源码
Tomcat源码阅读系列(二)Tomcat总体架构
Tomcat源码阅读系列(三)启动和关闭过程
本文主要介绍Tomcat的Connector连接器相关。

        Tomcat主要由两大核心组件,一个是Coyote为代表的connector,一个是Catalina为代表的container。connector负责的是底层的网络通信的实现,而container负责的是上层servlet业务的实现。对于tomcat的servlet container这部分代码很少改动,一个应用服务器的性能很大程度上取决于网络通信模块connector的性能,因此connector对于tomcat而言是重中之重。本文首先从应用层次分析Tomcat所有的connector种类及用法,然后介绍Connector的初始化以及使用过程,最后对一些关节步骤进行说明。本文以BIO的Http11Protocol为例进行说明,如有需要可自行阅读Http11AprProtocol、AjpAprProtocol、AjpProtocol、Http11NioProtocol和JkCoyoteHandler等,不同的协议实现对接不同的技术,如NIO、APR和AJP等,具体技术说明可以参考《Tomcat源码阅读系列(二)Tomcat总体架构》。

1. Connector介绍

1.1 Connector的种类

        根据Connector与外界交互协议的不同,可以将Connector分为使用HTTP协议进行交互类型和使用AJP协议交互类型两种,而这两种又可以分别于APR组件机型配合,所以产生的Protocol比较多,功能相对复杂。

  1. Http Connector,基于HTTP协议,负责建立HTTP连接。它又分为BIO Http Connector与NIO Http Connector两种,后者提供非阻塞IO与长连接Comet支持。其中BIO Http Connector为Http11Protocol,NIO Http Connector为Http11NioProtocol,Http Connector与APR协议的结合Http11AprProtocol。
  2. AJP Connector,基于AJP协议,AJP是专门设计用来为Tomcat与http服务器之间通信专门定制的协议,能提供较高的通信速度和效率。如与Apache服务器集成时,采用这个协议。对应实现分别为AjpProtocol、JkCoyoteHandler和AJP与APR的结合AjpAprProtocol。

1.2 Connector的参数说明

        对Connector的配置位于conf/server.xml文件中,关于Connector的配置参数说明可以参见 http://tomcat.apache.org/tomcat-6.0-doc/config/http.html,其中关键参数说明如下,
  • allowTrace 如果需要服务器能够处理用户的HAED/TRACE请求,这个值应该设置为true,默认值是false;
  • emptySessionPath 如果设置为true,所有session,cookie的path将会被设置为/,这种设置通常是在portlet中比较有用,默认值是false;
  • enableLookups 如果需要在调用request.getRemoteHost()方法时获取到客户端的机器名,则需要配置为true,如果配置为false,将会跳过DNS查询直接返回客户端机器的IP地址,通常为了提高性能,将此值设置为false,默认值是true;
  • maxPostSize POST方法能够提交的数据的最大大小,如果没有声明或者设置为小于等于0,则表示POST提交的数据大小是不限制的,默认值是2Megabytes.
  • protocol 设置处理请求的协议,默认是HTTP/1.1,即org.apache.coyote.http11.Http11Protocol,此外还 支持的协议有:org.apache.coyote.http11.Http11NioProtocol(通过NIO处理用户请求,可以提高系统性能), org.apache.coyote.http11.HttpAprProtocol。
  • proxyName/proxyPort 如果Web服务器使用了代理服务器,配置此参数意味着在调用request.getServerName的时候将会获取代理服务器的名称,getServerPort()将会返回proxyPort。
  • redirectPort 如果Connector的配置是支持非SSL的请求,当一个SSL请求到来时,服务器会自动的将请求重定位到redirectPort。
  • URIEncoding URI字节转化成String的时候的编码方式,默认为ISO-8859-1,如果页面需要支持中文,一般可以将其设置为UTF-8或者GBK,GB2312。
  • useBodyEncodingForURI 如果设置为true,则会根据页面的编码决定URI的编码方式,默认是false。
  •    Http/1.1 Connector提供的配置项:
  • acceptCount 等待队列的长度,默认值是100。
  • address 如果Tomcat所在的主机有多个IP,这个值声明了用于监听HTTP请求的IP地址。
  • bufferSize Connector创建的输入流的大小,默认值是2048 bytes,提高这个值可以提升性能,增加内存消耗。
  • compressableMimeType 使用HTTP压缩的MIME类型,使用逗号分割,默认值是 text/html,text/xml,text/plain。
  • compression 为了节省带宽,可以将这个值设置为on,从而启用HTTP/1.1 GZIP压缩。off关闭压缩,forces强制使用压缩,默认值是off。
  • connectionTimeout Connector接受一个连接后等待的时间(milliseconds),默认值是60000。
  • executor 在Service节点下,Connector节点前可以配置一个Executor节点用于管理线程,这个属性的值是配置的Executor的名称,如果应用了此属性且executor存在,那么任何其他的关于thread的配置将会被忽略。
  • keepAliveTimeout 在Connector关闭连接前,Connector为另外一个请求Keep Alive所等待的微妙数,默认值和 connectionTimeout 一样。
  • maxHttpHeaderSize HTTP请求、响应头信息的最大大小,默认是8192bytes。
  • maxKeepAliveRequests HTTP/1.0 Keep Alive 和HTTP/1.1 Keep Alive / Pipeline的最大请求数目,如果设置为1,将会禁用掉Keep Alive和Pipeline,如果设置为小于0的数,Keep Alive的最大请求数将没有限制。默认为100。
  • maxThreads 用于处理用户请求的最大线程数,默认值是20。
  • noCompressionUserAgents: 设置不使用HTTP GZIP压缩的客户端,使用逗号分隔,在某些浏览器不支持压缩的时候可以使用此属性。
  • port Connector监听的端口。
  • restrictedUserAgents 设置不使用Keep Alive的客户端代理名称,使用逗号分割,默认值是空字符串。
  • server 覆盖HTTP响应的serve头信息,如果不设置的话,默认值是 Apache-Coyote/1.1。一般情况下不需要关注此属性。
  • socketBuffer Socket输出流缓冲区的大小,默认是9000bytes,如果设置为小于0的值,则表示不使用此缓冲区。
  • tcpNoDelay 默认值是true,设置为true可以提高系统性能。
  • threadPriority 请求处理线程的优先级,默认的优先级是NORMAL。
  • scheme 将该属性设置为request.getScheme()返回的协议的名称。例如,对于SSL连接器,你会将此属性设置为“HTTPS ”。默认值是“ HTTP ”。
  • secure 主要用于标识Connector是否是安全SSL安全方式,request.isSecure()作为其返回值,默认为false,HTTP.
  • xpoweredBy 主要用于标识是否在头字段中表明Tomcat支持Servlet标准,默认为false
  • useIPVHosts 标识能够使用基于IP的Virtual Host,默认为false。

1.3 Connector典型配置

BIO HTTP/1.1 Connector配置

 <Connector port="8080" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               redirectPort="8443" />
使用ThreadPoolExecutor的BIO HTTP/1.1 Connector配置
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" 
        maxThreads="150" minSpareThreads="4"/>
<Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               redirectPort="8443" />
SSL配置
<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"  
              maxThreads="150" scheme="https" secure="true"  
              clientAuth="false" sslProtocol="TLS"   
       keystoreFile="D:\Tools\Web\ssl\tomcat.keystore"  
       keystorePass="tomcat"  
       ciphers="tomcat"/>  
NIO HTTP/1.1 Connector配置
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" 
    maxThreads="150" connectionTimeout="20000" redirectPort="8443" />
AJP 1.3 Connector配置
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
 Native APR Connector配置

具体如何安装,可自行搜索。配置如下:

<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol" 
maxThreads="150" connectionTimeout="20000" redirectPort="8443"/>

查看Tomcat当前使用的是哪个protocol可以根据Tomcat的启动日志来查看

对Tomcat7而言,

bio
信息: Starting ProtocolHandler ["http-bio-8080"] 2015-8-20 22:17:50 org.apache.coyote.AbstractProtocol start
nio
信息: Starting ProtocolHandler ["http-nio-8080"] 2015-8-20 22:27:50 org.apache.coyote.AbstractProtocol start
apr
信息: Starting ProtocolHandler ["http-apr-8080"] 2015-8-20 22:29:50 org.apache.coyote.AbstractProtocol start

对Tomcat6而言,

bio
2015-8-20 22:25:36 org.apache.coyote.http11.Http11Protocol start
信息: Starting Coyote HTTP/1.1 on http-8080
nio
2015-8-20 22:30:38 org.apache.coyote.http11.Http11NioProtocol start
信息: Starting Coyote HTTP/1.1 on http-8080

注意:Tomcat6的处理线程的名字如http-8080-1,而Tomcat7如http-bio-8080-exec-1,增加了protocol的协议信息

2. Connector的初始化以及使用过程

2.1 Connector的初始化及请求处理过程时序图


        初始化和开始的的过程不必多说,自己看一下源码就知道了。本文主要介绍一下,Tomcat是如果处理HTTP请求的。本文以Http11Protocol为例作介绍。

2.2 关键代码解读

2.2.1 JIoEndpoint.Acceptor

 /**
     * Server socket acceptor thread.
     */
    protected class Acceptor implements Runnable {
        /**
         * The background thread that listens for incoming TCP/IP connections and
         * hands them off to an appropriate processor.
         */
        public void run() {
            // Loop until we receive a shutdown command
            while (running) {
                // Loop if endpoint is paused
                while (paused) { //如果endpoint暂停的话,这里也停止运行。
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }
                // Accept the next incoming connection from the server socket
                try { //acceptSocket会阻塞,直到新的请求的到来。
                    Socket socket = serverSocketFactory.acceptSocket(serverSocket);
                    serverSocketFactory.initSocket(socket);
                    // Hand this socket off to an appropriate processor
                    if (!processSocket(socket)) { //关键代码 开启新线程或者利用老线程处理。
                        // Close socket right away
                        try {
                            socket.close();
                        } catch (IOException e) {
                            // Ignore
                        }
                    }
                }catch ( IOException x ) {
                    if ( running ) log.error(sm.getString("endpoint.accept.fail"), x);
                } catch (Throwable t) {
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
                // The processor will recycle itself when it finishes
            }
        }
    }

2.2.2 JIoEndpoint.processSocket方法

 /**
     * Process given socket.
     */
    protected boolean processSocket(Socket socket) {
        try {
            if (executor == null) {//关键代码,决定是使用Tomcat自己的线程管理系统还是JDK的ThreadPoolExecutor,
                getWorkerThread().assign(socket);
            } else {
                executor.execute(new SocketProcessor(socket));//如果Server.xml中的Connector配置了executor="tomcatThreadPool"则,使用JDK的ThreadPoolExecutor
            }
        } catch (Throwable 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;
    }
        以上代码比较关键,对于Tomcat6而言,默认情况下,是使用自己的模块管理线程的,只有配置了executor="tomcatThreadPool"才使用JDK的ThreadPoolExecutor,JDK的ThreadPoolExecutor管理线程时,具有线程收缩管理的功能,但是使用Tomcat 6自己的线程管理模块并没有伸缩管理的功能。
       使用Tomcat 6默认线程管理模块时,线程只会增长,而并没有收缩的管理,当Tomcat 6的线程达到峰值过后,其是不会处理线程的,线程的数量会一直保持在峰值位置,对于系统而言是一种资源的浪费。这块会在后续的代码中进行解析。
        在Tomcat 7时,已经去掉自己管理线程的模块,全部换成了ThreadPoolExecutor管理线程。
       默认情况下,Tomcat 6使用由Work数组组成的WorkerStack存储已经开启的线程,在线程处理完之后,重新将这个线程放到WorkerStack中,并没有回收操作。WorkerStack其实是一种资源池的概念,同ThreadPoolExecutor线程池类似,但是少了线程池的回收操作。

2.2.3 getWorkerThread、createWorkerThread和recycleWorkerThread方法

protected Worker getWorkerThread() {
        // Allocate a new worker thread
        synchronized (workers) {
            Worker workerThread;
            while ((workerThread = createWorkerThread()) == null) {//如果得不到workThread,则等待。只有在线程数到达最大线程数时,返回值才为null,这个时候需要等待有线程处理完某项工作,执行recycleWorkerThread中的workers.notify();才能被唤醒。
                try {
                    workers.wait();
                } catch (InterruptedException e) {
                    // Ignore
                }
            }
            return workerThread;
        }
    }
  protected Worker createWorkerThread() {
        synchronized (workers) {// 必须为线程安全的操作,因为可能有多个线程同时过来,这时候对WorkerStack的操作的非线程安全的,需要使用synchronized保证线程安全
            if (workers.size() > 0) {//如果WorkerStack不为空,则从WorkerStack中取值
                curThreadsBusy++;//
                return workers.pop();
            }
            if ((maxThreads > 0) && (curThreads < maxThreads)) {//如果最大线程数大于0并且当前最大线程数小于规定最大线程数
                curThreadsBusy++;
                if (curThreadsBusy == maxThreads) {
                    log.info(sm.getString("endpoint.info.maxThreads",
                            Integer.toString(maxThreads), address,
                            Integer.toString(port)));
                }
                return (newWorkerThread());//返回一个新线程
            } else {
                if (maxThreads < 0) {//最大线程数为负数,则无限制的返回Worker
                    curThreadsBusy++;
                    return (newWorkerThread());
                } else {
                    return (null);//如果大于最大线程数,则返回null
                }
            }
        }
    }
 protected Worker newWorkerThread() {
        Worker workerThread = new Worker();
        workerThread.start();
        return (workerThread);
    }
 public void start() {
            thread = new Thread(this);//新建线程
            thread.setName(getName() + "-" + (++curThreads));//此处设置线程名,http-端口
            thread.setDaemon(true);//所有工作线程都为daemon的
            thread.start();
        }
protected void recycleWorkerThread(Worker workerThread) {//处理完成后,将线程放到WorkerStack中!回收操作
        synchronized (workers) {//必须为线程安全的,因为WorkerStack为非线程安全
            workers.push(workerThread);//放到WorkerStack中
            curThreadsBusy--;
            workers.notify();
        }
    }
worker.run方法
 public void run() {
            // Process requests until we receive a shutdown signal
            while (running) {
                // Wait for the next socket to be assigned
                Socket socket = await();//等待新的请求到来后调用notifyall()方法
                if (socket == null)
                    continue;
                // Process the request from this socket
                if (!setSocketOptions(socket) || !handler.process(socket)) {//处理socket
                    // Close socket
                    try {
                        socket.close();
                    } catch (IOException e) {
                    }
                }
                // Finish up this request
                socket = null;
                recycleWorkerThread(this);//回收worker
            }

        }
        以上代码分析,可以知道,只有对WorkerStack的存取操作,并没有线程数的收缩操作, 在峰值过后,线程数仍然会维持在峰值位置

2.2.4 Http11Protocol.Http11ConnectionHandler.process方法和Http11Processor.process方法

        Http11Protocol.Http11ConnectionHandler使用了与WorkerStack类似的方法,使用protected ConcurrentLinkedQueue<Http11Processor> recycledProcessors =
            new ConcurrentLinkedQueue<Http11Processor>()作为资源池
 public boolean process(Socket socket) {
            Http11Processor processor = recycledProcessors.poll();//每次从资源池中取
            try {
                if (processor == null) {
                    processor = createProcessor();//取不到则创建
                }

               //....省略

                processor.process(socket);//Http11Processor.process
                return false;

            }//...省略
            catch (Throwable e) {//此处使用了Throwable顶级异常
                Http11Protocol.log.error
                    (sm.getString("http11protocol.proto.error"), e);
            } finally {
               
                if (processor instanceof ActionHook) {
                    ((ActionHook) processor).action(ActionCode.ACTION_STOP, null);
                    }
	            //回收processor
	            recycledProcessors.offer(processor);
            }
            return false;
        }
Http11Processor.process方法中的核心代码就是

adapter.service(request, response);
        此处的adapter是CoyoteAdapter,次类的地位非常重要,是连接catalina和coyote组件的适配器。是适配器模式的一个典型应用,关于适配器模式可以查看 设计模式笔记9:适配器模式(Adapter Pattern)

        CoyoteAdapter实现了Adapter,同时使用Connector作为自己的内部对象,是一种对象适配器模式,遵循了多用组合少用继承的设计准则,其适配coyote组件,为catalina组件提供服务。

        而Adapter.service()方法的核心代码就是

connector.getContainer().getPipeline().getFirst().invoke(request, response);
这段代码,直接将请求交给了catalina组件进行处理。关于catalina组件的处理过程,将在下篇博文中进行分析。

猜你喜欢

转载自blog.csdn.net/yangzl2008/article/details/46225031