我们已经准备好了,你呢?

2020我们与您携手共赢,为您的企业形象保驾护航!

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

 

这一节比较好玩,好玩在于如果大家如果看得懂的话,那么这一节可以学到很多东西,反之可能会很困惑。

本节对应《深入剖析Tomcat》第四章。

大家一定要先去下载Tomcat4的源码,放入eclipse中查看,否则肯定会不知所云。

代码可以在我的github上找到下载,拷贝到eclipse,然后打开对应包的代码即可。

https://github.com/zebinlin/Tomcat4-src

如发现编译错误,可能是由于jdk不同版本对编译的要求不同导致的,可以不管,供学习研究使用。

在“Tomcat剖析(三):连接器”中的连接器已经能较好的运行,既使用了线程启动,也完成了对大部分请求解析,但是仍然后很多不足。

一个 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 对象的。这样说可能不清楚。下面慢慢讲。

 这一节的核心就是对上一节基础上的改进:HttpConnector 如何创建一个服务器套接字,它如何维护一个 HttpProcessor 对象池,还有它如何处理 HTTP 请求。

写了一半才发现要写完全部知识点需要用灰常灰常大的篇幅,所以有些内容留到下一节再讲,这一节讲解Tomcat是如何处理多个请求的:里面重点提到池的实现。

总体来说,处理用户请求有以下几个步骤:

1.先创建serverSocket实例

2.建立HttpProcessor处理器池,等待请求

3.请求到来时从处理器池中获取HttpProcessor实例,HttpProcessor实例负责解析请求

4.完成请求后将实例返回到池中。实现对多请求的处理。 

首先是Bootstrap.java

为什么是这个启动类呢?因为可以通过启动过程比较完整的分析代码。

对于什么是SimpleContainer,为什么一个连接器要setContainer(container)将指定容器设置到连接器中,大家可以先不管,在下一节对本节的补充中大家就知道为什么了,最后还会再回头看这个类。

还有需要先提前注意一点,这里的HttpConnector已经是Tomcat4中核心包中的代码,不再是属于ex**中的类,有点,哈哈。

 1 package ex04.pyrmont.startup;
 2 
 3 import ex04.pyrmont.core.SimpleContainer;
 4 import org.apache.catalina.connector.http.HttpConnector;
 5 
 6 public final class Bootstrap {
 7     
 8     public static void main(String[] args) {
 9         
10         HttpConnector connector = new HttpConnector();
11         SimpleContainer container = new SimpleContainer();
12         connector.setContainer(container);
13         try {
14             connector.initialize();
15             connector.start();
16 
17             System.in.read();
18         } catch (Exception e) {
19             e.printStackTrace();
20         }
21     }
22 }

接下来大家看看HttpConnector.java,可以发现实现除了Connector接口外,还实现了Runnable接口和Lifecycle接口。

为什么要实现Runnable接口,因为它的实例可以运行在自己的线程上,也就是需要启动线程。

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

一、连接器初始化:完成服务器端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);
}

二、启动连接器

对应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 对象是存放在HttpProcessor.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.java的构造方法

构造方法中,从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(socket)处理

启动连接器概括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的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;

开始的时候, HttpConnector 对象创建 minProcessors 个 HttpProcessor 实例。如果一次有比 HtppProcessor 实例更多的请求需要处理时, HttpConnector 创建更多的 HttpProcessor 实例,直到实例数量达到 maxProcessors 个。在到 达这点之后,仍不够 HttpProcessor 实例的话,请来的请求将会给忽略掉。如果你想让 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 调用。

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

   void recycle(HttpProcessor processor) {

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

    }



 process方法就是处理HTTP请求的方法了,本节太长了,所以下一节再续上。

这篇博客写了我4个小时,希望大家看过之后有所帮助。

如果觉得还不错,可以推荐一下或加关注哟,觉得写得不好的也体谅一下。

附:

代码可以在我的github上找到下载,拷贝到eclipse,然后打开对应包的代码即可。

https://github.com/zebinlin/Tomcat4-src

如发现编译错误,可能是由于jdk不同版本对编译的要求不同导致的,可以不管,供学习研究使用。

我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为1000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版百度优化、名注册、主机空间、手机网站建设公众号开发小程序制作、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线: 13820372851,我们会详细为你一一解答你心中的疑难。项目经理在线

我们已经准备好了,你呢?

2020我们与您携手共赢,为您的企业形象保驾护航!

在线客服
联系方式

热线电话

13820372851

上班时间

周一到周五

公司电话

022-26262675

二维码
线
在线留言