[How Tomcat Works]第3章连接器- 对Java的喜欢,是一种爱! 对Java的 ...

??? 就像《简介》中介绍的,Catalina 中有两个主要模 块:Connector (连接器)和Container (容器)。本章,你将编写一个连接 器 来增强第2章的应用,该连接器 能够创建更好的RequestResponse 对 象。符合Servlet 2.32.4 规范的连接器 必须创建javax.servlet.http.HttpServletRequest 实例和javax.servlet.http.HttpServletResponse 实例,并将它们作 为参数传递给servletservice 方法。第2章的Servlet 容器只 能运行实现了javax.servlet.Servlet 接口的servlet ,并传递javax.servlet.ServletRequest 实 例和javax.servlet.ServletResponse 实例给servletservice 方 法。连接器 并不知道servlet 的类型(例如,是否实现了javax.servlet.Servlet 接 口, 继承了javax.servlet.GenericServlet ,或继承了javax.servlet.http.HttpServlet ), 因此它必须始终提供HttpServletRequest 实例和HttpServletResponse 实 例。

??? 本章的应用程序中,连接器 解析HTTP 请求的headers , 使得servlet 可以获得headerscookies 、参数名/值, 等等。我们也会完善第2章中Response 类的getWriter 方法,修正它的行为(译者注:第2 章中的实现是有问题的)。由于这些改进,我们可以从PrimitiveServlet 得 到完整的响应,同时也能够运行更加复杂的ModernServlet 。本章构建的连接器Tomcat 4 默认连接器 的一个简化版本,我们会在第4 章详细讨论Tomcat 4 的默认Connecotr 。虽然Tomcat 默 认连接器Tomcat 4 中已经不推荐使用了,但是它仍是一个很好的学习工具。在本章接下来的讨论中,凡是提到的连接器 ,都是指本章构建的连接器 ,而不是Tomcat 的默认连接器

??? 提示:与上一章的 应用不同,本章应用中连接器 和容器是分离的。
?
??? 本章应用的代码在ex03.pyrmont 包及其子包中。构成连接器 的类是ex03.pyrmont.connector 包 和ex03.pyrmont.connector.http 包的一部分。从本章开始, 每个应用都有一个bootstrap 类,用于启动整个应用。不过,目前还没有 停止应用的机制。应用一旦运行起来,你必须通过关闭控制台(Windows 平 台)或杀死进程(UNIX/Linux 平台)的粗鲁的方式来停止应用。

??? 在解释应用之前,请允许我先介绍org.apache.catalina.util 包 中的StringManager 类。该类负责处理本应用及Catalina 自身各模块的错误消息的国际化。然后,我们会讨论整个应用。

??? 像Tomcat 这样的大型程序,都需要仔细地处理错误消息。在Tomcat 中, 错误消息对系统管理员和Servlet 程序员都很重要。例如,通过Tomcat 的错误日志,系统管理员可以轻松定位任何异常。Tomcat 为内部抛出的每个javax.servlet.ServletException 打 印出一条特定错误日志,这样Servlet 程序员就可以知道自己写的servlet 哪里出了问题。

??? Tomcat 采用的方法是将错误消息存储在一个属性(properties )文件中,这样就可以方便地编辑错误消息。但是,Tomcat 有数百个类,如果将所有类的错误消息都存储在一个巨大的属性文件中,那么维护这些错误消息就是一个恶 梦。为了避免这个问题,Tomcat 为每个包定义了一个属性文件。例如,org.apache.catalina.connector 包中的属性文件包括了该包所有类抛 出的错误消息。每个属性文件都会被一个特定的org.apache.catalina.util.StringManager 实 例处理。Tomcat 运行的时侯,会有很多StringManager 实例,每个实例都会读取对应包中的属性文件。而且,由于Tomcat 十分流行,提供多语言版本的错误消息是很有意义的。目前,Tomcat 共支持三种语言。英语的属性文件名都是LocalStrings.properties 。其他两种语言是西班牙语和日语,其属性文件分别 为LocalStrings_es.propertiesLocalStrings_ja.properties

??? 当类需要在属性文件中查找错误消息时,它首先获取一个StringManager 实 例。但是,同一个包中很多类都可能需要一个StringManager 实例,如果为每 个需要错误消息的类对象创建一个StringManager 实例,则是对资源的浪费。 因此StringManager 类被设计成,同一个包中所有类对象可以共享一个StringManager 实例。如果熟悉设计模式,你可能会猜到StringManager 是一个单例类(singleton class )。StringManager 类 {wy}的构造函数是私有的(private ),因此你不能使用new 关键字在类外部创建该类的实例。以包名为参数,调用StringManager 类的公开静态方法getManager , 就可以获得一个StringManager 实例。每个实例被存储在一个Hashtable 中,key 就是包的名称。

private static Hashtable managers = new Hashtable();
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;
} 

? ? 提示:在附带的zip 文件中,可以找到一篇题为“The Singleton Pattern ”、关于单例模式的文章。

??? 举个例子,为了使用ex03.pyrmont.connector.http 包中的StringManager 类,传递包名给StringManagergetManager 方法:

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

??? ex03.pyrmont.connector.http 包 中,你可以找到三个属性文件:LocalStrings.propertiesLocalStrings_es.propertiesLocalStrings_ja.propertiesStringManager 实例根据应用程序运行时所在机器的区域(local ) 来决定使用哪个文件。如果你打开LocalStrings.properties ,非 注释的{dy}行应该是这样的:

httpConnector.alreadyInitialized=HTTP connector has already been initialized

??? 要得到一条错误消息,你需要以错误码(error code )为参数调用StringManager 类的getString 方 法。下面是该方法的多个重载之一:

public String getString(String key)
?
?? 以“httpConnector.alreadyInitialized ” 为参数调用getString 方法,就会返回“HTTP connector has already been initialized ”。

??? 从本章开始,每章附带的应用程序被化分成模块。本章的应用包括三个模块:connectorstartupcore

??? startup 模块只包括一个类:Bootstrap , 其作用是启动整个应用。connector 模块的类可以分成5个类别:

  • connector 和它的支持(supporting )类(HttpConnectorHttpProcessor
  • 代表HTTP 请求的类(HttpRequest )及 其支持类
  • 代表HTTP 响应的类(HttpResponse )及其支持类
  • 门面(Facade )类(HttpRequestFacadeHttpResponseFacade )
  • Constant

?
??? core 模块包括两个类:ServletProcessorStaticResourceProcessor


??? Figure 3.1 是本应用的类图。为了让类图更具可读性,HttpRequestHttpResponse 相关的类都被省略了。我们后面讨论RequestResponse 对 象时,会给出更加详细的类图。
?
??? 我们把Figure 3.1Figure 2.1 做个比较。第2 章的HttpServer 类被拆分成两个类:HttpConnectorHttpProcessorRequest 类被HttpRequest 类 替换,Response 类被HttpResponse 类 替换。而且,本章的应用使用了更多其他的类。

??? 第2章中的HttpServer 类 负责等待HTTP 请求,创建请求对象和响应对象。本章应用中,等待HTTP 请求的任务交给了HttpConnector 实 例,创建请求对象和响应对象的任务分配给了HttpProcessor 实例。

??? 本章中,HTTP 请求对象由实现了javax.servlet.http.HttpServletRequest 接口的HttpRequest 类来代表。HttpRequest 对 象被转型为HttpServletRequest 实例,并传递给servletservice 方 法。因此,每个HttpRequest 实例必须拥有适当的域,以便servlet 使用它们。需要赋给HttpRequest 对 象的值包括URIquery string 、参数、cookies 和 其他headers 等等。因为连接器 不知道servlet 需要哪些值,所以 必须解析所有能够从HTTP 请 求获得的值。但是,解析HTTP 请求会带来昂贵(开销巨大)的字符串操作和其 他操作。如果只解析servlet 需要的值,那么就可能节省大量的CPU 周期。例如,如果servlet 不 需要任何请求(也就是,不调用javax.servlet.http.HttpServletRequestgetParametergetParameterMap 、 getParameterNames或getParameterValues 方法),连接器 就不需要从query stringHTTP request body 中解析出 请求参数。Tomcat 的默认连接器 (包括本章应用中的连接器 )尝试通过“直 到真正需要时才解析请求参数”的方式来提高效率。Tomcat 的默认连接器 和我们的连接器 使 用SocketInputStream 类从SocketInputStream 中读取字节流。SocketInputStream 实例包装了SocketgetInputStream 返回的java.io.InputStream 实例。SocketInputStream 类提供了两个重要方法:readRequestLinereadHeaderreadRequestLine 返 回HTTP 请求的{dy}行,即包括URIHTTP 方法(method )和HTTP 版 本的那一行。处理套接字输入流中的字节流就意味着,从{dy}个字节读取到{zh1}一个字节(从不回退),因此readRequestLine 必 须只能被调用一次,而且必须在readHeader 方法之前调用。每调用一次readHeader 就可以读取一个header 名 /值对,而且应该重复调用直到所有的headers 都被读取。readRequestLine 的返回值是一个HttpRequestLine 实 例,readHeader 的返回值是一个HttpHeader 对象。我们将在下面讨论HttpRequestLineHttpHeader

??? HttpProcessor 对象负责创建HttpRequest 实 例,因此必须填充HttpRequest 实例的每个成员变量。HttpProcess 类使用它的parse 方 法来解析HTTP 请求的request lineheadersparse 方法的返回被赋值给HttpProcessor 对 象的成员变量。但是,parse 方法并不解析query stringrequest body 中 的请求参数。这个任务留给了HttpRequest 对象自己(译者注:这就是延迟解 析)。只有servlet 需要一个参数时,query stirngrequest body 才会被解析。

??? 在前一章基础上的另一个改进,就是引入了启动类ex03.pyrmont.startup.Bootstrap 来启动整个应用。

??? 我们将在下面这些小节中,详细解释本章的应用:

  • 启动应用
  • 连接器
  • 创建HttpRequest 对象
  • 创建HttpResponse 对象
  • 静态资源处理器和serlvet 处 理器
  • 运行应用

启动应用

?? 我们从ex03.pyrmont.startup.Bootstrap 类启动整个应用。Listing 3.1 列出了该类的代码。

Listing 3.1: The Bootstrap class? ?
?
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();
?? }
}
?
??? Bootstrap 类的main 方 法创建了一个HttpConnector 实例,并调用了它的start 方法。Listing 3.2 列出了HttpConnector 类的代码。

Listing 3.2: The HttpConnector class's start method? ?
?
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 ();
?? }
}

连接器

??? ex03.pyrmont.connector.http.HttpConnector 类 代表了连接器 ,其职责是创建等待HTTP 请求的服务器套接字。Listing 3.2 给 出了该类的代码。

??? HttpConnector 类实现了java.lang.Runnable 接口,因此它可以被自己的线程使用。当启动应用时,HttpConnector 的一个实例被创建,并执行其run 方法。

??? 提示:你可以阅读文章“ Working with Threads ”来回忆如何创建 Java 线程。

??? run 方法包括了一个while 循 环,用来做下面的事情:

  • 等待HTTP 请求
  • 为 每个请求创建HttpProcessor 实例
  • 调用HttpProcessorprocess 方 法

?

??? 提示: run 方法和第 2 章中 HttpServer1 类的 await 方法是相同的。

??? 马上你就能看到,HttpConnector 类和ex02.pyrmont.HttpServer1 类非常相似。从java.net.ServerSocket 类的accept 方法获得一个socket 之后发生了变 化,HttpConnector 类创建了一个HttpProcessor 实例,并以socket 为 参数调用其process 方法。

??? 提示: HttpConnector 类拥有另一个名为 getSchema 的方法,该方法返回网络请求的 schema HTTP )。

??? HttpProcessor 类 的process 方法接受HTTP 请 求的socket 为参数。对于每个HTTP 请求,process 方法会做如下处 理:
  1. 创建一个HttpRequest 对象
  2. 创 建一个HttpResponse 对象
  3. 解析HTTP 请求的{dy}行和headers , 并填充HttpRequest 对象
  4. 传递HttpRequest 对象和HttpResponse 对 象给ServletProcessorStaticResourceProcessor

? ? 就像第2 章里那样,ServletProcessor 调 用了被请求的servletservice 方法,StaticResourceProcessor 发 送静态资源的内容(给客户端)。

??? Listing 3.3 列 出了process 方法的代码。

Listing 3.3: The HttpProcessor class's process method.? ?
?
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 ();
?? }
}
?
??? process 方法首先从获取socket 的输入流和输出流。注意,该方法使用的SocketInputStream 继 承自java.io.InputStream

???? SocketInputStream input = null;
???? OutputStream output = null;
???? try {
?????? input = new SocketInputStream(socket.getInputStream(), 2048);
?????? output = socket.getOutputStream();
?????? // Then, it creates an HttpRequest instance and an HttpResponse instance and assigns
?????? // the HttpRequest to the HttpResponse.
?????? // create HttpRequest object and parse
?????? request = new HttpRequest(input);
?????? // create HttpResponse object
?????? response = new HttpResponse(output);
?????? response.setRequest(request);

??? 本章应用的HttpResponse 类比第2 章 的Response 类要复杂很多。举例来说,你可以通过调用HttpResponse 类的setHeader 方 法向客户端发送headers

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

??? 接下来,process 方 法调用HttpProcessor 类的两个私有方法来解析请求。

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

??? 然后,process方法根据请求URI的模式(pattern),将HttpRequest对象和HttpResponse对像甩给(hand off ... to)一个
ServletProcessor对象和一个 StaticResourceProcessor对象。

?????? if (request.getRequestURI().startsWith("/servlet/")) {
???????? ServletProcessor processor = new ServletProcessor();
???????? processor.process(request, response);
?????? }
?????? else {
???????? StaticResourceProcessor processor =
??????????? new StaticResourceProcessor();
???????? processor.process(request, response);
?????? }

??? {zh1},process 方法关闭socket

????? socket.close();
?
??? 同样注意,HttpProcessor 类 使用org.apache.catalina.util.StringManager 类 来发送错误消息:

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

??? HttpProcessor
类的私有方法——parseRequestparseHeadersnormalize —— 被调用来帮助填充HttpRequest 对象。在下一节“创建HttpRequest 对象”,我们将讨论这些方法。

创建HttpRequest 对象

??? HttpRequest 类 实现了javax.servlet.http.HttpServletRequest 接 口。附带还有一个叫做HttpRequestFacade 的门面类。Figure 3.2 展现了HttpRequest 和 相关类的类图。
??? HttpRequest 类 的许多方法都是留空的(等待第4章才会全部实现),但是servlet 程序员 已经可以从HTTP 请求中获得headerscookies 和请求参数。这 三种值被存储在下面的引用变量中:

?? protected HashMap headers = new HashMap();
?? protected ArrayList cookies = new ArrayList();
?? protected ParameterMap parameters = null;
?
??? 提示:我们会在“获取参数”小节解释 ParameterMap 类。

??? 因此,servlet 程序员可以从javax.servlet.http.HttpServletRequest 下面这些方法中获取正确的值:getCookiesgetDateHeadergetHeadergetHeaderNamesgetHeadersgetParametergetPrameterMapgetParameterNamesgetParameterValues 。正如你在HttpRequest类中看到的,一旦获得headerscookies 和 请求参数,相关方法的实现就很简单了。

??? 不用说,这里主要的挑战就是解析HTTP请求和填充HttpRequest 对象。对于headerscookiesHttpRequest 类 提供了addHeaderaddCookie 方 法,HttpProcessor 类的prseHeaders 就调用了这两个方法。 请求参数是在需要时才被HttpRequest 类 的parseParameters 方法解析的。本节所有的方法都会被讨论到。

??? 由于解析HTTP 请求是一个非常复杂的任务,因此本节被分成下面几个小节:
  • 读 取套接字 的输入流
  • 解析请求行(request line
  • 解析headers
  • 解析cookies
  • 获 取请求参数

读取套接字的输入流

??? 在第12 章中,ex01.pyrmont.HttpRequest 类和ex02.pyrmont.HttpRequest 类已经做了一部分解析HTTP 请求的工作。通过调用java.io.InputStream 类 的read 方法,我们可以从请求行获得HTTP 方法、URIHTTP 版本:

???? byte[] buffer = new byte [2048];
???? try {
?????? // input is the InputStream from the socket.
?????? i = input.read(buffer);
???? }

??? 第1、2章的应用中,我们没有尝试进一步解析HTTP请求。但是在本章的应用中,我们有了 ex03.pyrmont.connector.http.SocketInputStream
类——org.apache.catalina.connector.http.SocketInputStream 类 的一个拷贝。该类提供了一些方法,这些方法不但可以获得请求行,还可以获得headers

??? 要构造SocketInputStream 的实例,我们需要传递两个参数:InputStream 对象,指定SocketInputStream 实 例缓冲区大小的整数。在本应用中,我们在ex03.pyrmont.connector.http.HttpProcessor 类 的process 方法中创建了一个SocketInputStream 实 例,代码片段如下所示:

? SocketInputStream input = null;
???? OutputStream output = null;
???? try {
?????? input = new SocketInputStream(socket.getInputStream(), 2048);
?????? ...

??? 正如前面提到的,使用SocketInputStream 类的原因是为了使用它的两 个重要方法:readRequestLinereadHeader 。继续往下读。

解析请求行

??? HttpProcessor 类的process 方 法调用私有方法parseRequest 来解析请求行,即HTTP 请求的{dy}行。这里给出请求行的一个例子:

GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1

?? 请求行
的第二部分是URI 和可选的query string 。在上面的例子中,URI是:

/myApp/ModernServlet

??? 然后,问号之后的部分都是query stirng 。因此,query string 就是:

userName=tarzan&password=pwd

??? query string
可 以包含0 或多个参数。在上面的例子中,有两个参数名/值对:username/tarzanpassword/pwd 。在Servlet/JSP 编 程中,jsessionid 参数用来携带会话标识(session identity)。会话标识通常嵌入在cookies 中,但是程序员可以选 择将会话标识嵌入在query string 中,例如在浏览器禁止cookie 的情况下。
??
??? 当parseRequest 方法被HttpProcessor 类 的process 方法调用时,变量request 已 经指向了一个HttpRequest 实例。parseRequest 方法解析了请求行,获得了几个值,并将它们赋给HttpRequest 对 象。现在,我们来看看Listing 3.4parseRequest 方法的代码。

Listing 3.4: The parseRequest method in the HttpProcessor class? ?
?
private void parseRequest(SocketInputStream input, OutputStream output)
?? throws IOException, ServletException {
?
?? // Parse the incoming request line
?? input.readRequestLine(requestLine);
?? String method =
???? new String(requestLine.method, 0, requestLine.methodEnd);
?? String uri = null;
?? String protocol = new String(requestLine.protocol, 0,
???? requestLine.protocolEnd);
??? // Validate the incoming request line
?? if (method, length () < 1) {
???? throw new ServletException("Missing HTTP request method");
?? }
?? else if (requestLine.uriEnd < 1) {
???? throw new ServletException("Missing HTTP request URI");
?? }
?? // Parse any query parameters out of the request URI
?? int question = requestLine.indexOf("?");
?? if (question >= 0) {
???? request.setQueryString(new String(requestLine.uri, question + 1,
?????? requestLine.uriEnd - question - 1));
???? uri = new String(requestLine.uri, 0, question);
?? }
?? else {
???? request.setQueryString(null);
???? uri = new String(requestLine.uri, 0, requestLine.uriEnd);
?? }
?
?? // Checking for an absolute URI (with the HTTP protocol)
?? if (!uri.startsWith("/")) {
???? int pos = uri.indexOf("://");
???? // Parsing out protocol and host name
???? if (pos != -1) {
?????? pos = uri.indexOf('/', pos + 3);
?????? if (pos == -1) {
???????? uri = "";
?????? }
?????? else {
???????? uri = uri.substring(pos);
?????? }
???? }
?? }
?
?? // Parse any requested session ID out of the request URI
?? String match = ";jsessionid=";
?? int semicolon = uri.indexOf(match);
?? if (semicolon >= 0) {
???? String rest = uri.substring(semicolon + match,length());
???? int semicolon2 = rest.indexOf(';');
???? if (semicolon2 >= 0) {
?????? request.setRequestedSessionId(rest.substring(0, semicolon2));
?????? rest = rest.substring(semicolon2);
????? }
???? else {
?????? request.setRequestedSessionId(rest);
?????? rest = "";

郑重声明:资讯 【[How Tomcat Works]第3章连接器- 对Java的喜欢,是一种爱! 对Java的 ...】由 发布,版权归原作者及其所在单位,其原创性以及文中陈述文字和内容未经(企业库qiyeku.com)证实,请读者仅作参考,并请自行核实相关内容。若本文有侵犯到您的版权, 请你提供相关证明及申请并与我们联系(qiyeku # qq.com)或【在线投诉】,我们审核后将会尽快处理。
—— 相关资讯 ——