`
jarfield
  • 浏览: 202645 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

[How Tomcat Works]第4章 Tomcat默认连接器

阅读更多

译者 jarfield

博 客 http://jarfield.iteye.com

概述


  3 章的连接器工作得很好,而且本可以设计地更好。但是,我们只是将它设计成教学工具,来介 绍Tomcat 4 的默认连接器。理解第3 章的连接器,是理解Tomcat 4 默认连 接器的关键。第4 章将会通过解剖(dissect )Tomcat 4 默认 连接器的代码,来讨论如何构建真正的Tomcat 连接器。
   
    提示:本章的“默认连接器” 就是指Tomcat 4 默认连接器。尽管默认连接器已经废弃(deprecated ),被更快的Coyote 连接器代替了,但是它仍然是一个很好的学习工具。

    Tomcat 连接器是一个可以插入Servlet 容 器的独立模块。现在已经有很多连接器,例如Coyotemod_jkmod_jk2mod_webapp 。一个Tomcat 连 接器 满足下面的需求:

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


    Tomcat 4 默认连接器和第3 章中简单连接器的工作原理类似。它等待接受HTTP 请 求,创建请求对象和响应对象,然后将这两个对象传给容器。连接器通过调用org.apache.catalina.Container 接 口的invoke 方法,将请求对象和响应对象传递给容器。invoke 方法的原型如下:

    public void invoke( org.apache.catalina.Request request, org.apache.catalina.Response response); 

 
    在invoke 方法内部,容器加载servlet 类, 调用其service 方法,管理会话,打印错误日志等等。默认连接器还利用了一些第3 章连接器没有的优化措施。首先,默认连接器提供了对象池来避免昂贵的对象创建。第二, 默认连接器在很多地方使用字符数组来代替字符串。

    本章的应用,是一个与默认连接器关联的、简单的容器。不过,本章的焦点不是这个容器,而是默认连接器。容器将在第5 章被讨论。不管怎么样,我们还是在本章的最后一节“简单的容器程序”讨论该容器,以演示如何使用默认连接器。

    另一个需要注意的地方是,默认连接器实现了那些在HTTP 1.1 中新加 的、同样可以服务HTTP 0.9HTTP 1.0 客户的特性。为了理解HTTP 1.1 的新特性,你首先需要理解这些特性,我们将在本章第一节解释它们。在此之后,我们讨论org.apache.catalina.Connector ,如何创建请求对象和响应对象。如果你理解第3 章的连接器是如何工作的,你也不难理解默认连接器。

    本章以HTTP 1.13 个 新特性作为开始。理解它们是理解默认连接器内部原理的关键。然后,本章介绍了所有连接器都必须是实现的org.apache.catalina.Connector 接 口。你会发现第3章中已经遇到的一些类,像HttpConnectorHttpProcessor 等等。不过,现在这些类比第3 章要高级的多。

HTTP 1.1 新特性

    本节介绍HTTP 1.1 的三个新特性。理解这些特性,对于理解默认连接器如何处理HTTP 请求,是非常关键的。

持久化连接(Persistent Connections )

    在HTTP 1.1 之 前,无论浏览器什么时候连接上Web 服务器,服务器在发送完被请求资源之后立 刻关闭连接。但是,一个网页可以包含其他资源,例如图片文件、applets 等。 因此,当一个页面被请求时,浏览器也需要下载该页面引用的资源。如果页面及其引用的所有资源都通过不同的连接下载,那么整个处理过程会很慢。这就是HTTP 1.1 引入持久化连接的原因。对于持久化连接,页面下载完成后,服务器不会直 接关闭连接,而是等待客户端请求该页面引用的所有资源。这样,页面及其引用的所有资源均使用同一个连接下载。考虑到建立和关闭HTTP 连接都是昂贵的操作,这种方式将大大节省了Web 服务器、客户端及网络的负载和时间。
   
    持久化连接是HTTP 1.1 的默认连接。浏览器也可以通过发送下面的connection 头部,显式地告诉服务器使用持久化连接:

    connection: keep-alive

Chunked 编码(Chunked Encoding )

    建立持久化连接的一个结果就是,服务器可以在同一个连接上发送多个资源的字节流,客户端也可以在同一个连接上发送多个请求。因此,发送者必须发送每个请求 或响应的content-length 头部,这样接收者才知道如何解析接收到字节流。 但是通常情况下,发送者并不知道要发送多少字节。举个例子,servlet 容 器可以在部分字节准备好时就开始发送响应,而不要等到所有字节都准备好。这意味着,必须有一种方法告诉接收者:在不能提前知道content- length头部的情况下,如何解析字节流。

    即使没有发送多个请求或响应,服务器或客户端也没有必要知道,究竟多少数据将被发送。在HTTP 1.0 中,服务器可以省略content-length 头部,直接向 连接写如数据。当写入完成时,服务器会简单地关闭连接。在这种情况下,客户端持续读取,直到返回标识字节流结束的-1

    HTTP 1.1 利 用了一个名为transfer-encoding 的特殊头部,来指示字节流将按照chunk 的形式被发送。每个chunk 的格式是:首先是十六进制的长度,后面跟着一个CR/LF , 然后是数据。零长度的chunk 标识了一个传输单元(transaction )。假设在某个传输中,你想以2chunk 的形式发送下面的38 个字节,第一个chunk 长 度为29 ,第二个chunk 长 度为9

I'm as helpless as a kitten up a tree.

    你可以发送下面的内容:

1D\r\n
I'm as helpless as a kitten u
9\r\n
p a tree.
0\r\n

    1D29 的 十六进制形式,指示第一个chunk 包括29 个字节。0\r\n 标识该传输单元的结 束。

100 (Continue ) 状态码的使用

    如果HTTP 1.1 客户端打算发送很长的请 求,但是不确定服务器是否愿意接收,那么,它可以在发送请求体之前先发送Expect: 100-continue 头部给服务器,然后等待服务器的确认。不这么做的话,如果客户端发送了很长的请求体,最终发现被服务器拒绝了,那 么这就太浪费了。

    接收到Expect: 100-continue 头 部之后,如果服务器愿意(will to )或能够(can )处理请求,那么服务器会返回下面的100-continue 头部,头部后面再跟两对CRLF

HTTP/1.1 100 Continue

    接着,服务器继续读取输入流。

Connector 接口

    Tomcat 连接器必须实现org.apache.catalina.Connector 接 口。该接口的众多方法中,最重要的是getContainersetContainercreateRequestcreateResponse

    setContainer 方法用来将连接器和容器关联起来。getContainer 方法返回关联的容器。createRequest 方 法为进来的HTTP 请求创建一个请求对象,createResponse 方法创建一个响应对象。

    org.apache.catalina.connector.http.HttpConnectorConnector 接口的一个实现类,下一节“The HttpConnector Class ”将讨论它。现在,我们看看默认连接其的类图Figure 4.1 。注意,为了简化类图,RequestResponse 接口的实现类被省略了。除 了Simplecontainer 类,其他都省略了前缀“org.apache.catalina ”,只保留类型名。

    因此,连接器...(原文找不到,省略部分文字)

    连接器和容器是一对一的关联关系。关联关系的箭头方向表明,连接器知道容器,而容器却不知道连接器。同时需要注意,与第3 章不同,HttpConnectorHttpProcessor 的关系变成了一对多。

HttpConnector

    第3 章已经介绍了org.apache.catalina.connector.http.HttpConnector 的 一个简化版,因此你其实已经知道了HttpConnector 的工作原理。HttpConnector 实现了org.apache.catalina.Connector 接 口 (为了和Catalina 协调),java.lang.Runnable 接口 (这样它的实例就可以运行在自己的线程中),以及

org.apache.catalina.Lifecycle 接口。Lifecycle 接口用来维护每个实现该接口的Catalina 组件的生命周期。

    我们将在第6 章介绍Lifecycle 接口,现在你只需要知道:实 现了Lifecycle 接口,创建HttpConnector 实 例之后,你应该调用它的initializestart 方法。这两个方法在组件的生命周期中只能被调用一次。下面,我们看看与第3HttpConnector 不 同的地方:如何创建服务器套接字,如何维护HttpProcessor 池,如何处理HTTP 请求。

创建服务器套接字

    HttpConnector的initialize方法调用了返回java.net.ServerSocket实例的私有open方法,然后将 ServerSocket实例赋值给serverSocket变量。不过,open方法从一个服务器套接字工厂获取ServerSocket实例,而不是 调用java.net.ServerSocket的构造函数。如果你想知道服务器套接字工厂的细节,那么可以阅读ServerSocketFactory 接口和DefaultServerSocketFactory类,它们在org.apache.catalina.net包中,代码很容易理解。

 维护HttpProcessor 实例

    在第3 章 中,HttpConnector 实例同时只拥有一个HttpProcessor 实例,因此同时只能处理一个HTTP 请求。在默认连接器中,HttpConnector 拥 有一个HttpProcessor 对象池,每个HttpProcessor 实例拥有一个自己的线程。因此,HttpConnector 可以同时处理多个HTTP 请求。

    HttpConnector 维 护了一个HttpProcessor 实例池,以避免每次都创建HttpProcessor 实例。HttpProcessor 实 例被存储名为processors 的栈(java.io.Stack )中:

private Stack processors = new Stack(); 
 
    在HttpConnector 中,HttpProcessor 实例的数量由两个成员变量确定:minProcessorsmaxProcessors 。默认情况下,minProcessors 的 值为5maxProcessors 的 值为20 。你可以通过setMinProcessorssetMaxProcessors 方法来修改它们的值。

protected int minProcessors = 5;
 
private int maxProcessors = 20; 
 
    开始时,HttpConnector 对 象创建minProcessorsHttpProcessor 实例。如果需要处理的请求数量超过HttpProcessor 的 数量,那么HttpConnector 就会创建更多的HttpProcessor 实例,直至其数量达到maxProcessors 。达到这个点之后,就不会创建更多的HttpProcessor 实 例,新的HTTP请求被忽略。如果你想让HttpConnector一直创建HttpProcessor实例,那么就将maxProcessors设置成 负数。此外,成员变量curProcessors维护了当前HttpProcessor的数量。创建初始数量HttpProcessor实例的代码在 HttpConnector的start方法中:
   while (curProcessors < minProcessors) {
     if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
       break;
     HttpProcessor processor = newProcessor();
     recycle(processor);
   }
 
    newProcessor方法创建一个HttpProcessor对象,并递增curProcessors。recycle方法将 HttpProcessor压回栈中。每个HttpProcessor实例都负责解析HTTP请求行(request line)和头部,并填充(populate)一个请求对象。因此,每个HttpProcessor实例都关联一个请求对象和一个响应对象。 HttpProcessor的构造函数调用了HttpConnector的createRequest和createResponse方法。

处理HTTP 请 求

    HttpConnector的主要逻辑还是在它的run方法中,就像第3章中那样。run方法包括一个while循环,服务器套接字循环等待HTTP请 求,直到HttpConnector被停止。
   while (!stopped) {
     Socket socket = null;
     try {
       socket = serverSocket.accept();
     ... 
 
    对于每个HTTP请求,HttpConnector通过调用私有createProcessor方法获取一个HttpProcessor实例。
  HttpProcessor processor = createProcessor(); 
 
   不过,createProcessor方法大部分时间都没有创建新的HttpProcessor实例,而是从实例池中获取。只要栈中还有一个 HttpProcessor实例,那么createProcessor方法就弹出一个。如果栈空了,而且HttpProcessor实例的数量还没有达到 最大值,createProcessor方法就会创建一个实例。但如果HttpProcessor实例数量达到最大值了,那么 createProcessor方法就返回null。这种情况下,套接字被简单关闭掉,而且HTTP请求不会被处理。
 
if (processor == null) {
       try {
         log(sm.getString("httpConnector.noProcessor"));
         socket.close();
       }
       ...
       continue; 
     
    如果createProcessor方法没有返回null,那么客户端套接字就会被传递给HttpProcessor的assign方法:
processor.assign(socket); 
 
    现在,轮到HttpProcessor读取套接字的输入流,解析HTTP请求了。需要特别注意的是,assign方法必须直接返回,而不要等待 HttpProcessor完成解析,以便下一个HTTP请求能够被处理。因为每个HttpProcess实例都拥有自己的线程用于解析,所以做到这点是 不难的。你在下一节“HttpProcessor类”中可以看到这是怎么实现的。

HttpProcessor

    默认连接器的HttpProcessor类是第3章中同名类的完整版。你已经知道它是如何工作的,因此本章更感兴趣的地方在 于:HttpProcessor是如何使得assign方法异步执行,从而让HttpConnector可以同时处理多个HTTP请求的。
   
    提 示:HttpProcessor另一个重要方法是私有process方法,它负责解析HTTP请求并调用容器的invoke方法。我们将在后面的“处理请 求”小节看到该方法。

    在第3章中,HttpConnector运行在自己的线程里。但是,在处理下一个HTTP请求前,它必须等待当前的HTTP请求被处理完毕。下面是第3 章HTTPConnector的run方法:
   public void run() {
      ...
     while (!stopped) {
       Socket socket = null;
       try {
         socket = serversocket.accept();
       }        catch (Exception e) {
         continue;
       }
       // Hand this socket off to an Httpprocessor
       HttpProcessor processor = new Httpprocessor(this);
       processor.process(socket);
     }
   } 

 

    第3章中,HttpProcessor的process方法是同步的。因此,run方法必须等待process方法结束,才能接收下一个请求。

 

    但是在默认连接器中,HttpProcessor实现了java.lang.Runnable接口,HttpProcessor的每个实例都运行在自己 的线程中,我们将这些线程成为"处理器线程(processor thread)"。对于HttpConnector每个HttpProcessor实例,都会调用它们的start方法,从而启动它们的“处理器线程”。 Listing 4.1列出了默认连接器中HttpProcessor的run方法:

Listing 4.1: The HttpProcessor class's run method.  
 
public void run() {
   // Process requests until we receive a shutdown signal
   while (!stopped) {
     // Wait for the next socket to be assigned
     Socket socket = await();
     if (socket == null)
       continue;
     // Process the request from this socket
     try {
       process(socket);
     }
     catch (Throwable t) {
       log("process.invoke", t);
     }
     // Finish up this request
     connector.recycle(this);
   }
   // Tell threadStop() we have shut ourselves down successfully
 
   synchronized (threadSync) {
     threadSync.notifyAll();
   }
}  
      
    run方法的while循环一直执行这个序列:获取套接字,处理套接字,调用connector的recycle方法将当前HttpProcessor 实例压回栈中。这儿是HttpConnector类的recycle方法:
void recycle(HttpProcessor processor) {
    processors.push(processor);
} 
 
    注意run方法的while循环在await方法出会暂停。await方法挂起“处理器线程”,直到从HttpConnector获得一个新的套接字。 换句话说,也就是直到HttpConnector调用HttpProcessor实例的assign方法。但是,await方法与assign方法运行在 不同的线程中。assign方法是从HttpConnector的run方法中调用的。我们将HttpConnector的run方法所在的线程称为“连 接器线程(connector thread)”。assign方法如何告诉await方法,自己(assign方法)被调用了呢?通过布尔变量 available,java.lang.Object的wait和notifyAll方法。


    提示:wait方法导致当前线程等待,直到另一个线程调用了该对象的notify或notifyAll方法。

    这里是HttpProcessor的assign方法和await方法:

synchronized void assign(Socket socket) {
   // Wait for the processor to get the previous socket
   while (available) {
     try {
       wait();
     }
     catch (InterruptedException e) {
     }
   }
   // Store the newly available Socket and notify our thread
   this.socket = socket;
   available = true;
   notifyAll();
   ...
}
 
private synchronized Socket await() {
   // Wait for the Connector to provide a new Socket
    while (!available) {
     try {
       wait();
     }
     catch (InterruptedException e) {
     }
   }
 
   // Notify the Connector that we have received this Socket
   Socket socket = this.socket;
   available = false;
   notifyAll();
   if ((debug >= 1) && (socket != null))
     log("  The incoming request has been awaited");
   return (socket);
}

 

    Table 4.1总结了这两个方法的程序流(program flow)。
 
Table 4.1: Summary of the await and assign method 
 
The processor thread (the await method)    The connector thread (the assign method)

while (!available) {                                        while (available) {  
    wait();                                                            wait();  
}                                                                     }  
Socket socket = this.socket;                           this.socket = socket;  
available = false;                                            available = true; 
notifyAll();                                                       notifyAll();  
return socket; // to the run  method               ...
 
    一开始,当“处理器线程”刚刚启动时,available为false,所以“处理器线程”在while循环中等待(参见Table 4.1的第1列)。它会一直等待直到另一个线程调用notify或notifyAll方法为止。这就是说,调用await方法导致“处理器线程”暂停,直 到“连接器线程”调用HttpProcessor实例的notifyAll方法。

    现在,看看第2列。当一个新套接字被分配(assign)时,“连接器线程”调用HttpProcessor的assign方法。available的值 为false,因此while循环被跳过去,套接字被赋值给HttpProcessor实例的socket变量:

this.socket = socket; 


    接着,“连接器线程”将available设置成true,并调用notifyAll方法。这会唤醒“处理器线程”,而且现在available的值为 true,“处理器线程”从而离开while循环:将实例变量socket赋值给本地变量socket,设置available为false,调用 notifyAll方法,返回本地变量socket,最终套接字将被处理。

    为什么await方法需要使用本地变量(socket),而不是返回实例变量socket呢?这样做,当前套接字被处理完之前,下一个套接字就可以赋值给 实例变量socket。

    为什么await方法需要调用notifyAll呢?就是为了解决这个问题:available值为true时另一个套接字到达。在这种情况下,“连接器 线程”将停在assign方法while循环中,直到“处理器线程”调用notifyAll。

请求对象

   org.apache.catalina.Request接口代表了默认连接器的HTTP请求对象。该接口被HttpRequest的父类 RequestBase直接继承。最终的实现是HttpRequest的子类HttpRequestImpl。就像第3章一样,这里也有几个门面 (facade)类:RequestFacade和HttpRequestFacade。Figure 4.2给出了Request接口及其实现类的UML图。注意该图不包括javax.servlet和javax.servlet.http包中的类型,前 缀org.apache.catalina被省略。

  
    如果你理解第3章中的请求对象,那么你应该能够理解上面这张图。

响应对象

    Figure 4.3 给出了Response接口及其实现类的UML图。

     

处理请求

    到这里,你已经理解了HttpConnector是如何创建请求对象和响应对象的。现在是整个处理过程的最后一步。本节我们重点关注 HttpProcess的process方法。HttpProcess得到套接字之后,其run方法就会调用process方法。process方法会执 行以下操作:

  • 解析连接
  • 解析请求
  • 解析头部


    解释完process方法后,我们会分子章节讨论上述每个操作。

    process方法使用布尔变量ok指示处理过程是否发生了错误,使用布尔变量finishResponse指示Response接口的 finishResponse是否应该被调用。
  boolean ok = true;
  boolean finishResponse = true;  
    
    另外,process方法还使用了布尔变量keepalive、stopped和http11。keepalive指示连接是否是持久化的 (persistent),stopped指示HttpProcessor实例是否被连接器停止了(这样process方法也需要停止),http11指 示来自网络客户端的HTTP请求是否支持HTTP 1.1。

 

    就像第3章一样,一个SocketInputStream实例包装了套接字的输入流。注意,来自连接器对象的缓冲区大小(buffer size)被传递给SocketInputStream的构造函数,而不是HttpProcessor的本地变量。这是因为默认连接器的用户访问不到 HttpProcessor。将缓冲区大小放到Connector接口中,可以让任何人使用连接器来设置缓冲区大小。
   SocketInputStream input = null;
   OutputStream output = null;
   // Construct and initialize the objects we will need
   try {
     input = new SocketInputStream(socket.getInputstream(),
       connector.getBufferSize());
   }
   catch (Exception e) {
     ok = false;
   } 

 

    然后,有一个while循环,不断从输入流中读取数据,直到HttpProcessor停止,或抛出异常,或连接关闭。

keepAlive = true;
   while (!stopped && ok && keepAlive) {
     ...
   } 
  
    在while循环中,process方法首先设置finishResponse为true,然后获取输出流,并初始化请求对象和响应对象。
     finishResponse = true;
     try {
       request.setStream(input);
       request.setResponse(response);
       output = socket.getOutputStream();
       response.setStream(output);
       response.setRequest(request);
       ((HttpServletResponse) response.getResponse()).setHeader
         ("Server", SERVER_INFO);
     }
     catch (Exception e) {
       log("process.create", e);  //第7章将讨论日志 
       ok = false;
     }
 
 
    接着,process方法首先依次调用parseConnection、parseRequest和parseHeaders方法来解析请求,下面每个 子章节会讨论这些方法。
     try {
       if (ok) {
         parseConnection(socket);
         parseRequest(input, output);
         if (!request.getRequest().getProtocol()
           .startsWith("HTTP/0"))
           parseHeaders(input); 

 

 

    parseConnection方法获取协议(protocol)的值,该值可能是HTTP 0.9、HTTP 1.0或HTTP 1.1。如果协议是HTTP 1.0,keepAlive的值将被设置为false,因为HTTP 1.0不支持持久化连接。如果在HTTP请求中找到Expect: 100-continue头部,parseHeaders方法将会设置sendAck为true。如果协议是HTTP 1.1,且客户端发送了Expect: 100-continue头部,parseHeaders方法就会通过调用ackRequest方法来响应该头部。同时,parseHeaders方法也 会检查连接器是否允许chunking。
     if (http11) {
       // Sending a request acknowledge back to the client if
       // requested.
       ackRequest(output);
       // If the protocol is HTTP/1.1, chunking is allowed.
       if (connector.isChunkingAllowed())
         response.setAllowChunking(true);
     } 
 
    ackRequest方法检查sendAck的值,如果为true就发送下面的字符串:

    HTTP/1.1 100 Continue\r\n\r\n

    在解析HTTP请求的过程中,可以会抛出多种异常。抛出任何异常,process方法都会设置ok或finishResponse为false。解析结束 之后,process方法将请求对象和响应对象传递给容器的invoke方法。
     try {
       ((HttpServletResponse) response).setHeader
         ("Date", FastHttpDateFormat.getCurrentDate());
       if (ok) {
         connector.getContainer().invoke(request, response);
       }
     }
 
    然后,如果finishResponse仍为true,响应对象的finishResponse方法和请求对象的finishRequest方法都会被调 用,且输出被flush。
    if (finishResponse) {
       ...
       response.finishResponse();
       ...
       request.finishRequest();
       ...
       output.flush(); 
 
    while循环的最后一部分检查响应的Connection头部是否被Servlet设置成close,或者协议是否是HTTP 1.0。如果是这样,keepAlive被设置成false。同时,请求对象和响应对象被回收再利用(recycle)。
    if ( "close".equals(response.getHeader("Connection")) ) {
       keepAlive = false;
     }
     // End of request processing
     status = Constants.PROCESSOR_IDLE;
     // Recycling the request and the response objects
     request.recycle();
     response.recycle();
    } 
   
    这时,如果keepAlive为true,且前面的解析过程和容器的invoke方法都没有发生错误,那么while循环将从头重新开始。否 则,process方法调用shutdownInput方法并关闭套接字。
   try {
     shutdownInput(input);
     socket.close();
   }
   ... 
 
    shutdownInput方法检查是否有未读的字节。如果有,就跳过这些字节。

解析连接

    parseConnection方法获得套接字的网络地址(Internet address),并将它赋值给HttpRequestImpl对象。该方法还检查是否使用代理服务器,如果使用就将代理服务器的端口赋值给请求对象。 Listing 4.2给出了parseConnection方法的代码。
 
Listing 4.2: The parseConnection method   
 
private void parseConnection(Socket socket)
   throws IOException, ServletException {
   if (debug >= 2)
     log("  parseConnection: address=" + socket.getInetAddress() +
       ", port=" + connector.getPort());
   ((HttpRequestImpl) request).setInet(socket.getInetAddress());   
   if (proxyPort != 0)
     request.setServerPort(proxyPort);
   else
     request.setServerPort(serverPort);
   request.setSocket(socket);
} 

解析请求 

    parseRequest方法是第3章中同名方法的完整版。如果你很好地理解了第3章,那么你通过阅读代码就应该能理解该方法的工作原理。

解析头部 

    默认连接器的parseHeaders方法使用了org.apache.catalina.connector.http包中HttpHeader和 DefaultHeaders这两个类。HttpHeader类表现了一个HTTP请求头部。HttpHeader使用字符数组替代了第3章中的字符串, 以避免昂贵的字符串操作。DefaultHeaders是一个final类,以字符数组形式提供了标准的HTTP请求头部名称:
  static final char[] AUTHORIZATION_NAME =
     "authorization".toCharArray();
   static final char[] ACCEPT_LANGUAGE_NAME =
     "accept-language".toCharArray();
   static final char[] COOKIE_NAME = "cookie".toCharArray();
   ... 
 
    parseHeaders方法包括一个while循环,该循环一直读取HTTP请求,直到没有头部可以读取。while循环首先调用请求对象的 allocateHeader方法来获取一个空的HttpHeader实例。该实例被传递给SocketInputStream的readHeader方 法。
     HttpHeader header = request.allocateHeader();
 
     // Read the next header
     input.readHeader(header);

     //If all headers have been read, the readHeader method will assign no name to the
     //HttpHeader instance, and this is time for the parseHeaders method to return.
     if (header.nameEnd == 0) {
       if (header.valueEnd == 0) {
         return;
       }
       else {
         throw new ServletException
           (sm.getString("httpProcessor.parseHeaders.colon"));
       }
     } 
 
    HTTP请求中,头部的名称和值必须成对出现:
String value = new String(header.value, 0, header.valueEnd); 
 
    下一步,就像第3章一样,parseHeaders方法拿头部名称和DefaultHeaders类的标准头部名称做比较。注意比较的是字符数组而不是字 符串。
     if (header.equals(DefaultHeaders.AUTHORIZATION_NAME)) {
       request.setAuthorization(value);
     }
     else if (header.equals(DefaultHeaders.ACCEPT_LANGUAGE_NAME)) {
       parseAcceptLanguage(value);
     }
     else if (header.equals(DefaultHeaders.COOKIE_NAME)) {
       // parse cookie
     }
     else if (header.equals(DefaultHeaders.CONTENT_LENGTH_NAME)) {
       // get content length
     }
     else if (header.equals(DefaultHeaders.CONTENT_TYPE_NAME)) {
         request.setContentType(value);
     }
     else if (header.equals(DefaultHeaders.HOST_NAME)) {
       // get host name
     }
     else if (header.equals(DefaultHeaders.CONNECTION_NAME)) {
       if (header.valueEquals(DefaultHeaders.CONNECTION_CLOSE_VALUE)) {
         keepAlive = false;
         response.setHeader("Connection", "close");
       }      }
     else if (header.equals(DefaultHeaders.EXPECT_NAME)) {
       if (header.valueEquals(DefaultHeaders.EXPECT_100_VALUE))
         sendAck = true;
       else
         throw new ServletException(sm.getstring
           ("httpProcessor.parseHeaders.unknownExpectation"));
     }
      else if (header.equals(DefaultHeaders.TRANSFER_ENCODING_NAME)) {
       //request.setTransferEncoding(header);
     }
 
     request.nextHeader(); 

简单容器应用

    本章应用的主要目的是展示如何使用默认连接器。该应用共包括两个类:ex04.pyrmont.core.SimpleContainer和ex04 pyrmont.startup.Bootstrap。SimpleContainer类实现了 org.apache.catalina.container接口,因此它能够和连接器关联。Bootstrap类被用来启动应用,我们已经移除第3章应 用中的连接器模块、ServletProcessor类和StaticResourceProcessor类,因此你不能向本章应用请求静态页面。 Listing 4.3给出了SimpleContainer类的代码。

Listing 4.3: The SimpleContainer class   
 
package ex04.pyrmont.core;
 
import java.beans.PropertyChangeListener;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.io.File;
import java.io.IOException;
import javax.naming.directory.DirContext;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.Cluster; import org.apache.catalina.Container;
import org.apache.catalina.ContainerListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Logger;
import org.apache.catalina.Manager;
import org.apache.catalina.Mapper;
import org.apache.catalina.Realm;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
 
public class SimpleContainer implements Container {
 
   public static final String WEB_ROOT =
     System.getProperty("user.dir") + File.separator + "webroot";
 
   public SimpleContainer() {  }
   public String getInfo() {
     return null;
   }
   public Loader getLoader() {
     return null;
   }
   public void setLoader(Loader loader) {  }
   public Logger getLogger() {
     return null;
   }
   public void setLogger(Logger logger) {  }
   public Manager getManager() {
     return null;
   }
   public void setManager(Manager manager) {  }
   public Cluster getCluster() {
     return null;
   }
   public void setCluster(Cluster cluster) {  }
 
   public String getName() {
     return null;
   }
   public void setName(String name) {  }
   public Container getParent() {
     return null;
   }
   public void setParent(Container container) {  }    public ClassLoader getParentClassLoader() {
     return null;
   }
   public void setParentClassLoader(ClassLoader parent) {  }
   public Realm getRealm() {
     return null;
   }
   public void setRealm(Realm realm) {  }
   public DirContext getResources() {
     return null;
   }
   public void setResources(DirContext resources) {  }
   public void addChild(Container child) {  }
   public void addContainerListener(ContainerListener listener) {  }
   public void addMapper(Mapper mapper) {  }
   public void addPropertyChangeListener(
     PropertyChangeListener listener) {  }
   public Container findchild(String name) {
     return null;
   }
   public Container[] findChildren() {
     return null;
   }
   public ContainerListener[] findContainerListeners() {
     return null;
   }
   public Mapper findMapper(String protocol) {
     return null;
   }
   public Mapper[] findMappers() {
     return null;
   }
   public void invoke(Request request, Response response)
     throws IoException, ServletException {
 
     string servletName = ( (Httpservletrequest)
request).getRequestURI();
     servletName = servletName.substring(servletName.lastIndexof("/") +
1);
     URLClassLoader loader = null;
     try {
       URL[] urls = new URL[1];
       URLStreamHandler streamHandler = null;
       File classpath = new File(WEB_ROOT);        string repository = (new URL("file",null,
classpath.getCanonicalpath() + File.separator)).toString();
       urls[0] = new URL(null, repository, streamHandler);
       loader = new URLClassLoader(urls);
 
     }
     catch (IOException e) {
       System.out.println(e.toString() );
     }
     Class myClass = null;
     try {
       myClass = loader.loadclass(servletName);
     }
     catch (classNotFoundException e) {
       System.out.println(e.toString());
     }
 
     servlet servlet = null;
 
     try {
       servlet = (Servlet) myClass.newInstance();
       servlet.service((HttpServletRequest) request,
(HttpServletResponse) response);
     }
     catch (Exception e) {
       System.out.println(e.toString());
     }
     catch (Throwable e) {
       System.out.println(e.toString());
     }
   }
 
   public Container map(Request request, boolean update) {
     return null;
   }
   public void removeChild(Container child) {  }
   public void removeContainerListener(ContainerListener listener) {  }
   public void removeMapper(Mapper mapper) {  }
   public void removoPropertyChangeListener(
     PropertyChangeListener listener) {
   }
}
 
    我只提供了SimpleContainer中invoke方法的实现,因为默认连接器将会调用该方法。invoke方法创建了一个类加载器,加载 servlet类,调用它(servlet)的service方法。invoke方法和第3章中ServletProcessor类的process方法 非常类似。

    Listing 4.4给出了Bootstrap类的代码。

Listing 4.4: The ex04.pyrmont.startup.Bootstrap class   
 
 
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();
 
       // make the application wait until we press any key.
       System in.read();
     }
     catch (Exception e) {
       e.printStackTrace();
     }
   }
} 
     
    Bootstrap类的main方法构造了一个org.apache.catalina.connector.http.HttpConnector实例 connector和一个SimpleContainer实例container。然后以container为参数调用connector的 setContainer方法将connector和container相关联。接着,main方法调用connector的initialize和 start方法。这会让connector在8080端口上等待处理传入的HTTP请求。

    你可以在控制台上敲入任何键来停止该应用。

运行应用

    要在Windows 运行该应用,在工作目录运行以下命令:

java -classpath ./lib/servlet.jar;./ ex04.pyrmont.startup.Bootstrap

    在Linux 上,需要使用冒号来分隔两个库。

java -classpath ./lib/servlet.jar:./ ex04.pyrmont.startup.Bootstrap

    你可以调用按照第3章的方法调用PrimitiveServlet和ModernServlet。注意你不能请求index.html,因为本章应用没有 静态资源处理器(processor)。

总结

    本章展示了如何构建一个可以和Catalina协同工作的Tomcat连接器。剖析了Tomcat 4默认连接器的代码,构建了一个使用该连接器的小应用。接下来的章节的应用都会使用该默认连接器。

 

分享到:
评论

相关推荐

    How Tomcat Works 中文版.pdf

    比如,Tomcat 5支持更高版本的Servlet和JSP规范,具有更高效的默认连接器,以及后端处理线程的共享优化,从而减少了资源消耗。此外,Tomcat 5还简化了查找子组件的映射组件,使得代码结构更加简洁。 全书共分为20章...

    How Tomcat Works【英文PDF+中文HTML+源码】.zip

    《How Tomcat Works》是一份深入探讨Apache Tomcat工作原理的重要资源,包含了英文PDF文档、中文HTML翻译以及源代码,旨在帮助读者理解Tomcat服务器的内部运作机制。这份资料是IT从业者,特别是Java Web开发者、系统...

    How Tomcat Works 中文版+例程源码

    4. **连接器(Connector)**:Tomcat使用连接器组件接收和响应HTTP请求。最常见的连接器是Coyote,它负责与客户端建立连接,解析请求并转发给相应的Servlet进行处理。 5. **容器(Container)**:Tomcat有多个级别...

    How Tomcat Works 中文版

    Tomcat5相较于Tomcat4,增加了对Servlet2.4和JSP2.0规范的支持,并且使用了更有效的默认连接器。Tomcat5的后台处理线程是共享的,相比于Tomcat4中每个组件都有自己的处理线程,Tomcat5能更高效地利用资源。此外,...

    译How Tomcat Works(第二章)

    《译How Tomcat Works(第二章)》这篇文章主要讲解了Apache Tomcat服务器的工作原理,它是一个开源的Java Servlet容器,广泛用于部署Web应用程序。在这一章中,我们将深入探讨Tomcat如何处理HTTP请求,以及其内部架构...

    HowTomcatWorks(书和源码)

    《How Tomcat Works》是一本深入解析Apache Tomcat工作原理的书籍,同时也包含了源码,为读者提供了理论与实践相结合的深入学习体验。Tomcat是一款广泛使用的开源Java Servlet容器,它是Apache软件基金会 Jakarta...

    How Tomcat Works中文

    - **连接器性能**:相比于Tomcat 4,Tomcat 5提供了更为高效的默认连接器。 - **线程模型**:Tomcat 5采用共享的后台处理线程池,而Tomcat 4的每个组件都有独立的后台线程。这种改进提高了Tomcat 5的资源利用效率和...

    How Tomcat Works 英文书及源码

    《How Tomcat Works》这本书是理解Apache Tomcat服务器工作原理的宝贵资源,它全面深入地讲解了这个流行的Java Servlet和JavaServer Pages(JSP)容器的内部机制。书中的20个章节涵盖了从基础概念到高级特性的广泛...

    How Tomcat works(PDF)

    《How Tomcat Works》这本书深入浅出地介绍了Apache Tomcat这款广泛应用的Java Servlet容器的工作原理。Tomcat作为开源软件,是许多Web应用的基础,尤其在轻量级开发和测试环境中非常常见。以下是对Tomcat核心知识点...

    HowTomcatWorks 中文版+源码.rar

    《HowTomcatWorks》是一本深入解析Apache Tomcat工作原理的书籍,中文版的发布使得更多的中国开发者能够理解和掌握这款广泛应用的开源Java Servlet容器的工作机制。Tomcat是Apache软件基金会Jakarta项目的一部分,它...

    how tomcat works中英文版

    《How Tomcat Works》是一本深入探讨Apache Tomcat工作原理的书籍,包含了中英文两个版本。这本书对于理解Java Servlet和JavaServer Pages(JSP)容器的运作方式具有极高的价值,特别是对于那些想要深入理解Web应用...

    How Tomcat Works 中文版/英文版 + 源码

    《How Tomcat Works》是一本深入解析Apache Tomcat服务器内部工作原理的重要参考资料,它提供了对Tomcat架构的全面理解,包括其设计、配置和优化。这本书的中文版和英文版都为读者提供了便利,无论你是母语为中文...

    how tomcat works

    - **Tomcat5**:支持Servlet2.4和JSP2.0规范,引入了更高效的默认连接器,支持共享后台处理线程,减少了资源消耗,并且去掉了映射组件。 ### 学习Tomcat的路径 《How Tomcat Works》是针对不同层次读者的: - **...

Global site tag (gtag.js) - Google Analytics