Chapter 3: Connector(连接器)

一、概述

Tomcat或者称之为Catalina(开发名称),可以简化为两个主要的模块,如下图:

Chapter 3: Connector(连接器)

多个Connector关联一个Container。之所以需要多个Connector,是为了处理多种协议,如HTTP(细分为1.1版本和1.0版本),HTTPS,AJP等。后面会学习到,Container也是包含多层级的。

要满足Servlet2.3和Servlet2.4的规范,connector必须创建实现HttpServletRequest和HttpServletReponse接口的实例,用于传递到处理该请求的servlet的service方法中。

这一章介绍的connector是Tomcat4中默认的Connector的简化版本。虽然Tomcat4默认的Connector已经不推荐使用了,推荐使用一个称为Coyote的Connector,但是,是一个很好的学习资料。本章,"connector"代指我们程序中的模块。

这一章的包结构如下:

Chapter 3: Connector(连接器)

connector包及其子包是重点,就是该章要介绍的connector模块 ,startup包是启动应用程序的类的集合,剩下的类ServletProcessor和StaticResourceProcessor类归为core,也就是核心处理响应的类。

 

在我们开始介绍这一章的示例程序之前,我们先来看一下org.apache.catalina.util包下面的StringManager这个类。这个类用于处理Tocmat的错误消息的国际化。

 

二、StringManager类

 一个大型的应用程序,比如说tomcat,必须仔细处理好错误消息。对于Tomcat,错误消息对于系统管理员,还是servlet开发人员来说,都是很有价值的。举例来说,Tomcat的错误日志消息可以帮助系统管理员快速定位到异常出现的地方。对于servlet程序员,对于每一个抛出的javax.servlet.ServletException包含一个特定的错误消息,他/她能判断该servlet出了什么样的问题。

Tomcat中用于存储错误消息的方法是使用properties文件,这样便于编辑。但是,Tomcat有数百个类组成,如果把所有的类的出错消息都放在一个配置文件中,将难以维护。为了避免这样的问题,Tomcat采用的方法是对于每一个包分配一个配置文件。举例来说,org.apache.catalina.connector这个包下面的配置文件包含所有这个包下面的类可能会用到的错误信息。当Tomcat运行的时候,可能会有多个StringManager的示例在运行,每一个都和特定的包名相关联。并且,随着Tomcat的流行,提供多种语言的错误信息是有意义的。Tomcat4.1.12支持三种语言。对于英语,属性文件名称为LocalStrings.properties,对于西班牙语,文件名称为LocalStrings_es.properties,对于日语,名称为LocalStrings_ja.properties。

当一个类需要在这个包下面查找一条错误信息的时候,它会首先创建一个StringManager的实例。但是,对于同一个包下面的所有类,在需要查找错误消息的时候,都创建StringManager的实例,是一种资源的浪费。所以,StringManager被设计为如下:所有同一个包下面的类共享一个StringManager实例。如果熟悉设计模式的话,就是说StringManager是一个单例:构造方法私有使得不能从类的外面创建新的实例。通过传入一个包名,调用静态的getManager方法来获得StringManager实例,对应于多个包名的多个实例存储在一个Hashtable的数据结构中。

私有构造方法:

Chapter 3: Connector(连接器)
    private StringManager(String packageName) {
        String bundleName = packageName + ".LocalStrings";
        bundle = ResourceBundle.getBundle(bundleName);
    }
View Code

每一个StringManager实例关联一个bundle对象,用于从配置文件中读取对应的消息:

Chapter 3: Connector(连接器)
private ResourceBundle bundle;
View Code

通过调用getManager方法,获取该类所在的包对应的StringManager实例:

Chapter 3: Connector(连接器)
    /**
     * Get the StringManager for a particular package. If a manager for
     * a package already exists, it will be reused, else a new
     * StringManager will be created and returned.
     *
     * @param packageName
     */

    public synchronized static StringManager getManager(String packageName) {
        StringManager mgr = (StringManager)managers.get(packageName);
        if (mgr == null) {
            mgr = new StringManager(packageName);
            managers.put(packageName, mgr);
        }
        return mgr;
    }
View Code

通过调用getString方法,传入该条消息对应的key,来读取该条消息对应的value:

为了处理各种各样的情况,重载了很多getString方法,这里看个最简单的:

Chapter 3: Connector(连接器)
    /**
     * Get a string from the underlying resource bundle.
     *
     * @param key
     */

    public String getString(String key) {
        if (key == null) {
            String msg = "key is null";

            throw new NullPointerException(msg);
        }

        String str = null;

        try {
            str = bundle.getString(key);
        } catch (MissingResourceException mre) {
            str = "Cannot find message associated with key ‘" + key + "‘";
        }

        return str;
    }
View Code

举例来说,一个类A在包ex03.pyrmont.connector.http下面要使用StringManager来获取key为"httpConnector.alreadyInitialized"对应的错误消息,并且假设ex03.pyrmont.connector.http包下面要一个属性文件名为LocalStrings.properties,里面内容如下:

Chapter 3: Connector(连接器)
httpConnector.alreadyInitialized=HTTP connector has already been initialized
View Code

可以这样做,

StringManager sm = StringManager.getManager("ex03.pyrmont.connector.http");

getString("httpConnector.alreadyInitialized");

三、模拟Tomcat4.1.12的程序

从这一章起,程序会分为3个部分,connector,core,startup。

startup部分包含一个类:Bootstrap

connector部分包含的类可以划分为以下5类:

  • connector类本身HttpConnector以及它的支持类(HttpProcessor)
  • 代表Http请求的类HttpRequest以及它的支持类(HttpRequestFacade,RequestStream,HttpRequestLine,HttpHeader)
  • 代表Http响应的类HttpResponse以及它的支持类(HttpResponseFacade,ResponseStream,ResponseWriter)
  • Facade类,可以单独划分为一类或者也可以认为它是支持类
  • Constant类

core部分包含两个类:StaticResourceProcessor and ServletProcessor

该程序的类图如下所示,为了简化便于阅读,和HttpRequest以及HttpResponse相关的类被忽略。关于Request和Reponse的详细类图在介绍Request和Reponse部分可见。

Chapter 3: Connector(连接器)

从pdf上截取的图不是很清晰,但是结合代码来看,程序的入口HttpConnector等待请求的到来,HttpConnector和HttpProcessor关联,HttpProcessor创建(create)HttpRequest对象和HttpResponse对象。HttpProcessor在process请求的过程中,还使用(uses)到StringManager/SocketInputStream/HttpHeader/HttpRequestLine类。HttpProcessor根据请求的类型会分别调用ServletProcessor或者StaticResourceProcessor。ServletProcessor和StaticResourceProcessor使用(users)到HttpConnector创建的HttpRequest和HttpResponse对象。

和第二章的UML类图比较起来,第二章的HttpServer类被拆分为两个类:HttpConnector,HttpProcessor,功能分别为等待请求以及处理请求。Request类被替换为HttpRequest,Response类被替换为HttpResponse。当然,在这一章中使用到了更多的类。

在这一章中,用HttpRequest代表Http请求对象,它实现了javax.servlet.HttpServletRequest接口。HttpRequest中封装的数据包括了请求URI,查询字符串,请求参数,cookies,以及其他的请求头信息。因为connector并不知道哪些值会被要调用的servlet(the invoked servlet)使用到,connector要从http请求中解析得到所有这些值。但是,解析http请求包含了大量的字符串操作等,如果只解析会被请求servlet使用到的值(比如请求头)而不解析哪些用不到的值(比如请求参数)则会节省大量的cpu周期。举例来说,如果servlet不需要知道请求参数的值(i.e. it does not call the getParameter, getParameterMap, getParameterNames, or getParameterValues methods of javax.servlet.http.HttpServletRequest),则connector就不需要从query string或者request body中解析参数。Tomcat4默认的Connector(虽然已经不推荐使用了)和本章模拟的connector组件都是在servlet需要parameter值时才会从查询字符串或者请求实体中解析得到参数值。(^_^可以分析下代码,看看是如何实现的)。

我们使用SocketInputStream读取socket输入的字节流。SocketInputStream实例wrap了java.io.InputSteam(从socket中获取)并提供了两个重要的方法:readRequestLine和readHeader。readRequestLine returns the first line in an HTTP request, i.e. the line containing the URI, method and HTTP version. Because processing byte stream from the socket‘s input stream means reading from the first byte to the last (and never moves backwards), readRequestLine must be called only once and must be called before readHeader is called. readHeader is called to obtain a header name/value pair each time it is called and should be called repeatedly until all headers are read. The return value of readRequestLine is an instance of HttpRequestLine and the return value of readHeader is an HttpHeader object. We will discuss the HttpRequestLine and HttpHeader classes in the sections to come.

接下来,HttpProcessor创建HttpRequest和HttpResponse对象并填充字段,通过调用parse方法,解析请求行和请求头。但是,parse方法并不解析在request body and query string中的参数。这个任务留给HttpRequest对象本身,只有在servlet需要一个参数值时request body or query string才会被解析。

我们将分为以下小节来讨论具体的代码。

  • Starting the Application
  • The Connector
  • Creating an HttpRequest Object
  • Creating an HttpResponse Object
  • Static resource processor and servlet processor
  • Running the Application

1、如何启动

通过ex03.pyrmont.startup.Bootstrap启动程序,这个类很简单:

Chapter 3: Connector(连接器)
package ex03.pyrmont.startup;

import ex03.pyrmont.connector.http.HttpConnector;

public final class Bootstrap {
  public static void main(String[] args) {
    HttpConnector connector = new HttpConnector();
    connector.start();
  }
}
View Code

启动了HttpConnector线程。

Chapter 3: Connector(连接器)
package ex03.pyrmont.connector.http;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class HttpConnector implements Runnable {

  boolean stopped;
  private String scheme = "http";

  public String getScheme() {
    return scheme;
  }

  public void run() {
    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);
    }
    while (!stopped) {
      // Accept the next incoming connection from the server socket
      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);
    }
  }

  public void start() {
    Thread thread = new Thread(this);
    thread.start();
  }
}
View Code

 

2、Connector

从上面的代码可以看到,HttpConnector实现了Runnable接口,所以可以启动为单独的一个线程。run方法中while循环做了以下的事情:等待Http请求/对于每个请求创建HttpProcessor实例/调用HttpProcessor的process方法。

HttpProcessor的process方法得到代表请求的socket对象后,做了以下的事情:创建HttpRequest/创建HttpResponse/解析请求行和请求头并填充HttpRequest对象(这一步比较复杂,下面会分析到)/根据请求资源类型调用ServletProcessor或者StaticResourceProcessor的process方法,关闭socket。

代码如下:

Chapter 3: Connector(连接器)
  public void process(Socket socket) {
    SocketInputStream input = null;
    OutputStream output = null;
    try {
      input = new SocketInputStream(socket.getInputStream(), 2048);
      output = socket.getOutputStream();

      // create HttpRequest object and parse
      request = new HttpRequest(input);

      // create HttpResponse object
      response = new HttpResponse(output);
      response.setRequest(request);

      response.setHeader("Server", "Pyrmont Servlet Container");

      parseRequest(input, output);
      parseHeaders(input);

      //check if this is a request for a servlet or a static resource
      //a request for a servlet begins with "/servlet/"
      if (request.getRequestURI().startsWith("/servlet/")) {
        ServletProcessor processor = new ServletProcessor();
        processor.process(request, response);
      }
      else {
        StaticResourceProcessor processor = new StaticResourceProcessor();
        processor.process(request, response);
      }

      // Close the socket
      socket.close();
      // no shutdown for this application
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
View Code

 

3、创建HttpRequest对象

首先HttpRequest的详细UML类图如下:

Chapter 3: Connector(连接器)

从图中可以看出,HttpRequestFacade和HttpRequest都实现了javax.servlet.http.HttpServletRequest接口,所以根据servlet2.3规范,可以作为参数之一传入要调用的servlet的service方法。另外,HttpRequest组合(Composite)了RequestStream。这个很容易理解,只有得到了socket的输入流,才能得到uri/method/parameters/headers/cookies这些数据,才能填充HttpRequest对象。这些数据分别保存在以下的引用变量中:

protected HashMap headers = new HashMap();

protected ArrayList cookies = new ArrayList();

protected ParameterMap parameters = null;

所以,一个servlet程序员才能从javax.servlet.http.HttpServletRequest中通过调用getCookeis/getHeader/getHeaderNames/getParameter/getParameterMap/getParameterNames这些方法得到正确的值。(^_^这个解释的好,以前写servlet时,在doGet方法中调用request的对象的方法得到参数值时,并没有主要到这个request的类型是什么,现在想来应该是HttpRequestFacade这个类。已通过request.getClass().getName()验证)。

不用说,这里的主要挑战是解析http请求,并填充HttpRequest对象。For headers and cookies, the HttpRequest class provides the addHeader and addCookie methods that are called from the parseHeaders method of HttpProcessor. Parameters are parsed when they are needed, using the HttpRequest class‘s parseParameters method. All methods are discussed in this section.

因为解析http请求是一个比较复杂的工作,这小节分为以下子小节:

  • Reading the socket‘s input stream
  • Parsing the request line
  • Parsing headers
  • Parsing cookies
  • Obtaining Parameters

 

  1、读取socket输入流

在chapter 1以及chapter 2我们做了一点request的解析来获取请求行信息,是通过直接调用java.io.InputStream的read方法,如下:

Chapter 3: Connector(连接器)
byte[] buffer = new byte [2048]; try { // input is the InputStream from the socket. i = input.read(buffer); }
View Code

在前2章节,并没有更深地解析request。这一章,however, you have the ex03.pyrmont.connector.http.SocketInputStream class, a copy of org.apache.catalina.connector.http.SocketInputStream. This class provides methods for obtaining not only the request line, but also the request headers.我们通过下面的代码片段来构建SocketInputStream。

Chapter 3: Connector(连接器)
SocketInputStream input = null; OutputStream output = null; try { input = new SocketInputStream(socket.getInputStream(), 2048); ...
View Code

我们要使用SocketInputStream类,主要是因为这两个方法,public void readRequestLine(HttpRequestLine requestLine)和public void readHeader(HttpHeader header),这两个方法做的事情很简单,但是实现起来挺复杂的啊。Read On.

  2、解析请求行

  3、解析请求头

  4、解析cookies信息

  5、获取参数值(在需要的时候)

 

4、创建HttpResponse对象

5、StaticResourceProcessor和ServletProcessor这两个类

6、运行

四、总结

Chapter 3: Connector(连接器)

上一篇:用Canvas写桌球游戏!!!


下一篇:CFURLSetResourcePropertyForKey failed because it was passed this URL which has no scheme