Tomcat剖析(四):Tomcat默认链接器(1)

Tomcat剖析(四):Tomcat默认链接器(1)

第一部分:概述

这一节你们能够学到不少东西,不过若是不懂的话可能会很困惑。html

本节对应《深刻剖析 Tomcat》第四章:Tomcat 默认链接器。java

你们必定要先去下载 Tomcat4 相关代码,不然确定会不知所云。 代码能够在个人github上下载git

在第 3 节中的链接器已经能较好的运行,既使用了线程启动,也完成了对大部分请求解析,可是仍而后不少不足。github

一个 Tomcat 链接器必须符合如下条件web

  1. 必须实现接口 org.apache.catalina.Connector
  2. 必须建立请求对象,该请求对象的类必须实现接口 org.apache.catalina.Request
  3. 必须建立响应对象,该响应对象的类必须实现接口 org.apache.catalina.Response

能够看到,上节的简单链接器基本的条件都没有实现。apache

Tomcat4 的默认链接器相似于上节的简单链接器。它等待前来的 HTTP 请求,建立 request和 response 对象,而后把 request 和 response 对象传递给容器(上节只是交给响应的处理器Processor处理)。链接器是经过调用接口org.apache.catalina.Container 的 invoke 方法来传递 request 和 response 对象的。服务器

写了一半才发现要写彻底部知识点须要用灰常灰常大的篇幅,因此有些内容留到下一节再讲.eclipse

首先须要牢记链接器Connector的功能是什么:建立ServerSocket,等待请求,解析请求并传递给容器(由容器处理请求)。socket

这一节讲解:建立ServerSocket和等待请求部分,其中重点提到经过实现多个请求的处理,下一节的补充内容为解析请求部分和其余一些琐碎的问题。学习

整体来讲,处理用户请求有如下几个步骤

  1. 先建立serverSocket实例
  2. 创建HttpProcessor处理器池,等待请求
  3. 请求到来时从处理器池中获取HttpProcessor实例,HttpProcessor实例负责解析请求
  4. 完成请求后将实例返回到池中。实现对多请求的处理。

核心类:

  • HttpConnector.java:链接器类,负责整个流程
  • HttpProcessor.java:处理器类,封装请求须要的信息,包括请求对象和响应对象

第二部分:代码讲解

1. 启动

Bootstrap.java做为启动类

为何是先从启动类开始讲呢?由于能够经过启动过程比较完整的分析代码。

对于什么是SimpleContainer,为何一个链接器要setContainer(container)将指定容器设置到链接器中,你们能够先无论,在下一节对描述处理请求过程会用到Container,因此下一节还会再回头看这个类。 还有须要先提早注意一点,这里的HttpConnector已是Tomcat4中核心包中的代码,再也不是属于ex**中的类,有点高级,哈哈。

package ex04.pyrmont.startup;

import ex04.pyrmont.core.SimpleContainer;
import org.apache.catalina.connector.http.HttpConnector;

public final class Bootstrap {

    public static void main(String[] args) {

        HttpConnector connector = new HttpConnector();
        SimpleContainer container = new SimpleContainer();
        connector.setContainer(container);
        try {
            connector.initialize();
            connector.start();

            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接下来你们看看HttpConnector.java,能够发现实现除了Connector接口外,还实现了Runnable接口和Lifecycle接口。

为何要实现Runnable接口,由于它的实例能够运行在本身的线程上,也就是须要启动线程。

Lifecycle 将之后解释,如今你不须要担忧它,只要明白经过实现 Lifecycle,在你建立 HttpConnector 实例以后,你应该调用它的 initialize 和 start 方法。这两个方法在组件的 生命周期里必须只调用一次。

2. 链接器初始化

功能:完成ServerSocket建立

对应connector.initialize()

能够看到HttpConnector.java的initialize方法的做用是完成服务器端的ServerSocket的建立,并赋值给HttpConnector的实例变量serversocket中,里面调用了这个类的私有方法open();

serverSocket = open();

看看open(),open()方法负责建立SeverSocket这个方法有下面这些关键语句

private ServerSocket open(){
    ServerSocketFactory factory = getFactory();

    try {
            return (factory.createSocket(port, acceptCount, is));
     } catch (BindException be) {
            throw new BindException(be.getMessage() + ":" + address +
                                        ":" + port);
     }
}

getFactory()是用单例模式返回具体工厂,即代码中的DefaultServerSocketFactory实例。DefaultServerSocketFactory对象负责建立ServerSocket对象,也就factory.createSocket(...)。

public ServerSocketFactory getFactory() {

        if (this.factory == null) {
            synchronized (this) {
                this.factory = new DefaultServerSocketFactory();
            }
        }
        return (this.factory);

}

经过对比前一节或者ex03包下的HttpConnector类,能够发现一个简单的建立ServerSocket对象变得复杂许多,惟一不变的就是建立的地点都是在HttpConnector.java中,只是再也不是在启动HttpConnector线程时建立,而是在以前建立,不依赖于HttpConnector线程的启动。

下面是上一节建立 ServerSocket对象的方式。

ServerSocket serverSocket = null;
int port = 8080;
try {
    serverSocket = new ServerSocket(port, 1,
                InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
    e.printStackTrace();
    System.exit(1);
}

3. 启动链接器

对应 connector.start();

下面是这个方法关键代码。

public void start(){

    threadStart();//启动HttpConnector线程
    while (curProcessors < minProcessors) {
            if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
                break;
            HttpProcessor processor = newProcessor();//建立HttpProcessor池
            recycle(processor);
    }      
}

启动 HttpConnector 线程 threadStart() 放在本小节后半部分说,先介绍处理器池。

HttpConnector 维护一个 HttpProcessor 的实例池,从而避免每次建立 HttpProcessor 实例。

这些 HttpProcessor 对象是存放在 HttpConnector.java 中一个叫 processors 实例的 java.io.Stack 中:

private Stack processors = new Stack();

池的实现通常都是享元模式单例模式的综合使用哦。

启动链接器归纳1:启动 HttpConnector 线程,每一趟 while 循环 new 出 HttpProcessor 对象,并调用 recyle 方法将对象放入表明池的 processors 栈中。

void recycle(HttpProcessor processor) {

    processors.push(processor);//将建立的实例压入栈中

}

如何建立 HttpProcessor 实例的: newProcessor() 方法

这个方法获取到的信息是:建立出一个 HttpProcessor 对象后就当即启动这个线程

启动链接器归纳2:启动 HttpConnector 线程,一个 while 循环事后,建立了一些 HttpProcessor 实例,而后启动它们的线程,最后经过 recyle() 方法放入 processors 实例中

启动 Processor 线程后发生了什么,稍后再说。

private HttpProcessor newProcessor() {

        //建立实例
        HttpProcessor processor = new HttpProcessor(this, curProcessors++);
        if (processor instanceof Lifecycle) {
            try {
                ((Lifecycle) processor).start();//启动处理器线程
            } catch (LifecycleException e) {
                log("newProcessor", e);
                return (null);
            }
        }
        created.addElement(processor);
        return (processor);
    }

看看 HttpProcesor.jav a的构造方法

构造方法中,从 connector 中获取端口,包括代理端口

同时建立HttpRequestImpl 和 HttpResponseImpl 实例,以便于请求到来时不用再建立这些对象,只须要再放入相应的请求信息就行。

同时 HttpConnector.java 经过建立处理器实例,在这个过程绑定了请求和 connector 之间的关系,即 request.setConnector(this);

好了,如今对启动链接器过程发生了什么能够归纳为:

启动链接器归纳3:启动 HttpConnector 线程,一个 while 循环事后,建立了一些 HttpProcessor 实例;建立每个 HttpProcessor 实例过程当中,都会建立请求和响应对象,避免请求到来时再建立,并绑定链接器与请求之间的关系;而后启动处理器线程,最后经过 recyle() 方法放入 processors 实例中。

public HttpProcessor(HttpConnector connector, int id) {

        super();
        this.connector = connector;
        this.debug = connector.getDebug();
        this.id = id;
        this.proxyName = connector.getProxyName();
        this.proxyPort = connector.getProxyPort();
        this.request = (HttpRequestImpl) connector.createRequest();
        this.response = (HttpResponseImpl) connector.createResponse();
        this.serverPort = connector.getPort();
        this.threadName =
          "HttpProcessor[" + connector.getPort() + "][" + id + "]";

    }

public Request createRequest() {

    HttpRequestImpl request = new HttpRequestImpl();
    request.setConnector(this);
    return (request);

}

接下来看看 HttpProcessor 线程启动后发生了什么事,固然就是它的run方法。

如今好比有10个处理器线程,那么从注释中能够看到,启动一个处理器线程后,这个线程经过await就一直处于等待请求状态了,请求到来后就能够调用 process方法处理

启动链接器归纳4:启动 HttpConnector 线程,一个 while 循环事后,建立了一些 HttpProcessor 实例,这些实例中没有用户的 socket 信息;建立每个 HttpProcessor 实例过程当中,都会建立请求和响应对象,避免请求到来时再建立,并绑定链接器与请求之间的关系;而后启动处理器线程,这个线程启动后 await 方法进入阻塞状态等待请求;经过 recyle() 方法放入 processors 实例中;若是请求到来,获取一个处理器,处理这个请求。

public void run() {

        while (!stopped) {

            Socket socket = await(); //阻塞,直到用户请求到来获取到这个处理器才被唤醒
            if (socket == null)
                continue;

            try {
                process(socket); //处理用户请求
            } catch (Throwable t) {
                log("process.invoke", t);
            }

            connector.recycle(this);  //链接器回收处理器
        }
    }

听起来可能有些迷糊,由于还没将链接器线程的启动。

咱们知道 HttpConnector 实现了 Runnable 接口,Bootstrap.java 中调用 connector.start(),最后经过 threadStart方法 启动 HttpConnector 线程

threadStart方法就是建立建立了 HttpConnector 线程,因此会调用 HttpConnector 类的run方法。

private void threadStart() {

        log(sm.getString("httpConnector.starting"));

        thread = new Thread(this, threadName);
        thread.setDaemon(true);
        thread.start();

 }

HttpConnector 的 run 方法究竟作了什么呢?链接器的用途是什么还记得吗?就是建立ServerSocket,等待请求,解析请求,传递请求响应对象给容器(固然也能够认为是 Processor 作的,可是通常认为Tomcat的组件核心是 Connector 和 Container,能够将 Processor 看成 Connector 的一部分)。

前面讲了建立 ServerSocket,那剩下的功能固然是等待请求部分。(解析请求下一节讲)

还记得上一节中是如何等待客户端请求的吗?固然就是调用 serverSocket 的 accept 方法

通过默认链接器改进了,可是在启动 HttpConnector 线程后,run 方法依然是使用一样的方式等待用户请求。只是更加具体。

下面贴上 theadStart 方法中的关键代码。

public void run() {
        while (!stopped) {
            Socket socket = null;
            try {
                socket = serverSocket.accept();//等待用户请求,阻塞
            } catch (AccessControlException ace) {
                continue;
            } catch (IOException e) {
                //省略
                continue;
            }
            HttpProcessor processor = createProcessor(); //从池中获取处理器实例
            if (processor == null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    ;
                }
                continue;
            }
            processor.assign(socket); //将请求的socket交给获得的处理器实例中
        }
    }

简单说说 createProcessor 方法:

  • 在 HttpConnector 中,建立的 HttpProcessor 实例数量是有两个变量决定的: minProcessors和 maxProcessors。默认状况下, minProcessors 为 5 而 maxProcessors 为 20,可是你能够经过setMinProcessors 和 setMaxProcessors 方法来改变他们的值。   

    protected int minProcessors = 5;   
    
     private int maxProcessors = 20;
    1. 开始的时候, HttpConnector 对象建立 minProcessors 个 HttpProcessor 实例。

    2. 若是一次有比 HtppProcessor 实例更多的请求须要处理时, HttpConnector 建立更多的 HttpProcessor 实例,直到实例数量达到 maxProcessors 个。

    3. 在到达这点以后,仍不够 HttpProcessor 实例的话,请来的请求将会给忽略掉。

    4. 若是你想让 HttpConnector 继续建立 HttpProcessor 实例的话,把maxProcessors 设置为一个负数。还有就是变量 curProcessors 保存了 HttpProcessor 实例的当前数量。

      private HttpProcessor createProcessor() {
      
      synchronized (processors) {
          if (processors.size() > 0) {
              return ((HttpProcessor) processors.pop());  
              //从池中拿处理器对象
          }
          if ((maxProcessors > 0) && (curProcessors < maxProcessors)) {
              return (newProcessor()); //没有超过上界,建立新的处理器
          } else {
              if (maxProcessors < 0) {
                  return (newProcessor());
                  //若是没有上界,能够随意建立处理器实例
              } else {
                  return (null);
              }
          }
      }

      }

从上面的递推的说明中。能够知道整个流程是这样子的:

首先 connector.initialize 方法建立 ServerSocket 实例。connector.start() 方法中启动 HttpConnector 线程,这个线程经过 serverSocket.accept 方法进入等待用户请求状态。

-随后 connector.start 方法建立了若干个 HttpProcessor 处理器实例,同时启动了处理器线程,因为这些处理器实例中没有用户的 socket 信息,没法处理请求,因此所有进入阻塞状态,即 await 方法。当用户请求到来时,经过 assign 方法将 socket 放入处理器实例中,以便让处理器处理用户请求。

那Tomcat4是如何实现同时处理多个请求的呢?

就要看assgin和await方法了。

用户请求到来,获得了用户的 socket,调用 assign 方法,由于 avaiable 默认是 false,因此跳过 while 须要,将 socket 放入获取到的处理器实例中,同时将 avaiable 设为 true,唤醒线程,此时 await 方法中的 wait 方法被唤醒了,同时由于 avaliable 为 true,跳出循环,将 avaiable 设为 false 从新进入阻塞,获得用户的返回用户的 socket,最后就可以经过 process 处理请求了。

synchronized void assign(Socket socket) {

    while (available) { //avaiable默认是false,,第一次执行时跳过while
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }

    this.socket = socket; //将从池中获取到的HttpProcessor实例中的socket变量赋值
    available = true;
    notifyAll();//唤醒线程。
}

private synchronized Socket await() {

    while (!available) {//默认是false,因此进入循环阻塞,由于处理器实例没有socket信息,
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }

    Socket socket = this.socket;  //获得了socket
    available = false;  //从新进入阻塞
    notifyAll();

    if ((debug >= 1) && (socket != null))
        log("  The incoming request has been awaited");

    return (socket);

}

疑问:

为何 await 须要使用一个本地变量(socket)而不是返回实例的 socket 变量呢?

  • 由于这样一来,在当前 socket 被彻底处理以前,实例的 socket 变量能够赋给下一个前来的 socket。

为何 await 方法须要调用 notifyAll 呢?

  • 这是为了防止在 available 为 true 的时候另外一个 socket 到来。在这种状况下,链接器线程将会在 assign 方法的 while 循环中中止,直到接收处处理器线程的 notifyAll 调用。

4. 回收处理器

process方法处理请求后,最后经过connecotr.recyle()方法回收处理器,即将处理器压回处理器池中

void recycle(HttpProcessor processor) {

    processors.push(processor);//将建立的实例压入栈中

}

第三部分:小结

本节讲解了Tomcat默认链接器是如何实现多个请求处理

并无涉及process方法具体是如何处理请求的,在下一节补充。

但愿你们看过这篇博客以后有所帮助。

若是以为还不错,能够推荐一下或加关注哟,以为写得很差的也体谅一下。

相应代码能够在个人github上找到下载,拷贝到eclipse,而后打开对应包的代码便可。

如发现编译错误,多是因为jdk不一样版本对编译的要求不一样致使的,能够无论,供学习研究使用。

相关文章
相关标签/搜索