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

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

第一部分:概述

这一节大家可以学到很多东西,不过如果不懂的话可能会很困惑。

本节对应《深入剖析 Tomcat》第四章:Tomcat 默认连接器。

大家一定要先去下载 Tomcat4 相关代码,否则肯定会不知所云。 代码可以在我的github上下载

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

一个 Tomcat 连接器必须符合以下条件

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

可以看到,上节的简单连接器基本的条件都没有实现。

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

写了一半才发现要写完全部知识点需要用灰常灰常大的篇幅,所以有些内容留到下一节再讲.

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

这一节讲解:创建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不同版本对编译的要求不同导致的,可以不管,供学习研究使用。

上一篇:在WPF应用程序中使用Font Awesome图标


下一篇:ios基础篇(十二)——UINavgationController的使用(三)ToolBar