欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《深入剖析Tomcat》一3.2 应用程序

程序员文章站 2022-07-14 10:30:09
...

3.2 应用程序

从本章开始,每章的应用程序都会按照模块进行划分。本章的应用程序包含3个模块:连接器模块、启动模块和核心模块。
启动模块只有一个类(Bootstrap),后者负责启动应用程序。连接器模块中的类可分为以下5个类型:
连接器及其支持类(HttpConnector和HttpProcessor);
表示HTTP请求的类(HttpRequest)及其支持类;
表示HTTP响应的类(HttpResponse)及其支持类;
外观类(HttpRequestFacade和HttpResponseFacade);
常量类。
核心模块包含两个类,servletProcessor类和StaticResourceProcessor类。
图3-1展示了本章中应用程序的UML类图。为了使类图更容易理解,一些与HttpRequest类和HttpResponse类相关的类被省略掉。在讨论Request对象和Response对象时,你会看到它们各自的UML类图。

《深入剖析Tomcat》一3.2 应用程序

该图相比于图2-1中的图,第2章中的HttpServer类分成HttpConnector和HttpProcessor两个类,Request类和Response类分别被HttpRequest和HttpResponse代替。此外,本章的应用程序中还使用了一些其他的类。
在第2章中,HttpServer类负责等待HTTP请求,并创建Request和Response对象。在本章的应用程序中,等待HTTP请求的工作由HttpConnector实例完成,创建Request和Response对象的工作由HttpProcessor实例完成。
在本章中,HTTP请求对象使用HttpRequest类表示,HttpRequest类实现javax.servlet.http.HttpServletRequest接口。HttpRequest对象会被转型为HttpServletRequest对象,然后作为参数传递给被调用的servlet实例的service()方法。因此,必须正确地设置每个HttpRequest实例的成员变量供servlet实例使用。需要设置的值包括;URI、查询字符串、参数、Cookie和其他一些请求头信息等。因为连接器并不知道被调用的servlet会使用哪些变量,所以连接器必须解析从HTTP请求中获取的所有信息。但是,解析HTTP请求涉及一些系统开销大的字符串操作以及一些其他操作。若是连接器仅仅解析会被servlet实例用到的值它就会节省很多CPU周期。比如,如果servlet实例不会使用任何请求参数(即它不会调用javax.servlet.http.HttpServletRequest类的getParameter()、getParameterMap()、getParameterNames()或getParameterValues()方法),连接器就不需要从查询字符串和/或HTTP请求体中解析这些参数。在这些参数被servlet实例真正调用前,Tomcat的默认连接器(包括本章应用程序中的连接器)是不会解析它们的,这样就可以使程序执行得更有效率。
Tomcat中的默认连接器和本章应用程序中的连接器使用SocketInputStream类从套接字的InputStream对象中读取字节流。SocketInputStream实例是java.io.InputStream实例的包装类,SocketInputStream实例可以通过调用套接字的getInputStream()方法获得。SocketInputStream类提供了两个重要的方法,分别是readRequestLine()和readHeader()。readRequestLine()方法会返回一个HTTP请求中第1行的内容,即包含了URI、请求方法和HTTP版本信息的内容。由于从套接字的输入流中处理字节流是指读取从第1个字节到最后1个字节(无法从后向前读取)的内容,因此readRequestLine()方法必须在readHeader()方法之前调用。每次调用readerHeader()方法都会返回一个名/值对,所以应重复调用readHeader()方法,直到读取了所有的请求头信息。readRequestLine()方法的返回值是一个HttpRequestLine实例,readHeader()方法的返回值是一个HttpHeader对象。接下来的章节将会对HttpRequestLine类和HttpHeader类进行讨论。
HttpProcessor对象负责创建HttpRequest的实例,并填充它的成员变量。HttpProcessor类使用其parse()方法解析HTTP请求中的请求行和请求头信息,并将其填充到HttpRequest对象的成员变量中。但是,parse()方法并不会解析请求体或查询字符串中的参数。这个任务由各个HttpRequest对象自己完成。这样,只有当servlet实例需要使用某个参数时,才会由HttpRequest对象去解析请求体或查询字符串。
相比与前面章节中的应用程序,本章中的另一个功能点是使用启动类ex03.pyrmont.startup.Bootstrap来启动应用程序。
下面的几节将会介绍应用程序的细节。
启动应用程序
连接器
创建HttpRequest对象
创建HttpResponse对象
静态资源处理器和servlet处理器
运行应用程序

3.2.1 启动应用程序

本章中的应用程序是通过ex03.pyrmont.startup.Bootstrap类来启动的。该类的定义在代码清单3-1中给出。
《深入剖析Tomcat》一3.2 应用程序

Bootstrap类的main()方法通过实例化HttpConnector类,并调用其start()方法就可以启动应用程序。HttpConnector类的定义在代码清单3-2中给出。
《深入剖析Tomcat》一3.2 应用程序
《深入剖析Tomcat》一3.2 应用程序

3.2.2 HttpConnector类

连接器是ex03.pyrmont.connector.http.HttpConnector类的实例,负责创建一个服务器套接字,该套接字会等待传入的HTTP请求。HttpConnector类已在代码清单3-2中给出。
HttpConnector类实现了java.lang.Runnable接口。当你启动应用程序时,会创建一个HttpConnector实例,该实例另起一个线程来运行。
HttpConnector类实现 java.lang.Runnable ,这样它可以专用于自己的线程。当启动应用程序时,创建HttpConnector的一个实例并执行它的run ()方法。
注意 你可以阅读文章“Working with Threads”来学习如何创建Java线程的相关知识。
run()方法包含一个while循环,在循环中会执行如下三个操作:
等待HTTP请求;
为每个请求创建一个HttpPorcessor实例;
调用HttpProcessor对象的process()方法。
注意 HttpConnector类的run()方法其实与第2章中HttpServer1类的await()方法实现了类似的功能。
你可以看到,实际上,HttpConnector类与ex02.pyrmont.HttpServer1类非常相似,区别在于从java.net.ServerSocket类的accept()方法中获取一个套接字,创建一个HttpProcessor实例并传入该套接字,调用其process()方法。
注意 HttpConnector类有一个getScheme()方法,后者会返回请求协议(如HTTP协议)。
HttpProcessor类的process()方法接收来自传入的HTTP请求的套接字。对每个传入的HTTP请求,它要完成4个操作:
创建一个HttpRequest对象;
创建一个HttpResponse对象;
解析HTTP请求的第1行内容和请求头信息,填充HttpRequest对象;
将HttpRequest对象和HttpResponse对象传递给servletProcessor或StaticResourceProcessor的process()方法。与第2章中相同,servletProcessor类会调用请求的servlet实例的service()方法,StaticResourceProcessor类会将请求的静态资源发送给客户端。
HttpProcessor类的process()方法的实现在代码清单3-3中给出。
《深入剖析Tomcat》一3.2 应用程序

process()方法首先会获取套接字的输入流和输出流。但请注意,在这种方法中,使用了继承自java.io.InputStream的SocketInputStream类:
《深入剖析Tomcat》一3.2 应用程序

然后,它会创建一个HttpRequest实例和一个HttpResponse实例,然后将HttpRequest实例赋值给HttpResponse实例:
《深入剖析Tomcat》一3.2 应用程序

在本章的应用程序中,HttpResponse类比第2章中的Response类稍微复杂一点。首先,可以调用HttpResponse类的setHeader()方法向客户端发送响应头信息:
《深入剖析Tomcat》一3.2 应用程序

其次,process()方法会调用HttpProcessor类的两个私有方法来解析请求:
《深入剖析Tomcat》一3.2 应用程序

然后,它会根据请求的URI模式来判断,将HttpRequest对象和HttpResponse对象发送给ServletProcessor类或StaticResourceProcessor类来处理:
《深入剖析Tomcat》一3.2 应用程序

最后,它关闭套接字:
《深入剖析Tomcat》一3.2 应用程序

注意,HttpProcessor类使用了org.apache.catalina.util.StringManager类来发送错误消息:
《深入剖析Tomcat》一3.2 应用程序

调用HttpProcessor类的私有方法—parseRequest()、parseHeaders()和normalize()—来帮助填充HttpRequest对象。这些方法将在3.3.3节中进行讨论。

3.2.3 创建HttpRequest对象

HttpRequest类实现了javax.servlet.http.HttpservletRequest接口。其外观类是HttpRequestFacade类。图3-2展示了HttpRequest类及其相关类的UML类图。

《深入剖析Tomcat》一3.2 应用程序

其中HttpRequest类的很多方法都是空方法(在第4章中会提供完整实现),但是servlet程序员已经可以从中获取引入的HTTP请求的请求头、Cookie和请求参数等信息了。这三类数据分别存储在如下引用变量中:
《深入剖析Tomcat》一3.2 应用程序

注意 ParameterMap类将会在本节下面的“5.获取参数”节中介绍。
因此,servlet程序员就可以通过调用javax.servlet.http.HttpservletRequest类的一些方法获取HTTP请求信息,这些方法包括getCookies()、getDateHeader()、getHeader()、getHeaderNames()、getHeaders()、getParameter()、getPrameterMap()、getParameterNames()和getParameterValues()。当使用正确的值填充请求头、Cookie和请求参数后,相关方法的实现就比较简单了,就像在HttpRequest类中看到的那样。
当然,这里最主要的工作是解析HTTP请求并填充HttpRequest对象。对每个请求头和Cookie,HttpRequest类都提供了addHeader()方法和addCookie()方法来填充相关信息,这两个方法会由HttpProcessor类的parseHeaders()方法进行调用。当真正需要用到请求参数时,才会使用HttpRequest类的parseParameters()方法解析请求参数。相关方法将会在本节中介绍。
解析HTTP请求相对来说比较复杂,所以下面将分成5个小节来进行说明:
读取套接字的输入流
解析请求行
解析请求头
解析Cookie
获取参数

  1. 读取套接字的输入流
    在第1章和第2章中,ex01.pyrmont.HttpRequest类和ex02.pyrmont.HttpRequest类对HTTP请求进行过初步的解析。通过调用java.io.InputStream类的read()方法,从请求行中获取到了方法、URI、HTTP协议版本等信息:

《深入剖析Tomcat》一3.2 应用程序

不需要试图进一步解析这两个应用程序的请求。在本章的应用程序中,将会使用ex03.pyrmont.connector.http.SocketInputStream类来进行解析,该类实际上就是org.apache.catalina.connector.http.SocketInputStream类的一个副本。该类提供了一些方法来获取请求行和请求头信息。
需要传入一个InputStream对象和一个指明缓冲区大小的整数来创建一个SocketInputStream实例。在本章的应用程序中,在ex03.pyrmont.connector.http.HttpProcessor类的process()方法中会创建一个SocketInputStream对象,如以下代码段所示:
《深入剖析Tomcat》一3.2 应用程序

如前所述,之所以使用SocketInputStream类就是为了调用其readRequestLine()方法和readHeader()方法。
2.解析请求行
HttpProcessor类的process()方法会调用私有方法parseRequest()来解析请求行,即HTTP请求的第1行内容。下面是一个HTTP请求行的示例:
GET /myApp/Modernservlet?userName=tarzan&password=pwd HTTP/1.1
请求行的第2部分是URI加上一个可选的查询字符串。在这个例子中,URI是:

/myApp/Modernservlet

问号后面的部分就是查询字符串,如下所示:

userName=tarzan&password=pwd

查询字符串可以包含0个或多个请求参数。在上面的例子中,包含了两个名/值对,userName/tarzan和password/pwd。在servlet/JSP编程中,参数名jsessionid用于携带一个会话标识符。会话标识符通常是作为Cookie嵌入的,但是当浏览器禁用了Cookie时,程序员也可以将会话标识符嵌入到查询字符串中。
当从HttpProcessor类的process()方法调用parseRequest()方法时,request变量会指向一个HttpRequest实例。parseRequest()方法解析请求行,从而获取一些值,并将其赋给HttpRequest对象。代码清单3-4展示了parseRequest()方法的实现。
《深入剖析Tomcat》一3.2 应用程序
《深入剖析Tomcat》一3.2 应用程序
《深入剖析Tomcat》一3.2 应用程序

parseRequest()方法首先会调用SocketInputStream类的readRequestLine()方法:

input.readRequestLine(requestLine);

其中,变量requestLine是HttpProcessor中的一个HttpRequestLine实例:

private HttpRequestLine requestLine = new HttpRequestLine();

调用其readRequestLine()方法,使用SocketInputStream对象中的信息填充HttpRequestLine实例。
接着,parseRequest()方法从请求行中获取请求方法、URI和请求协议的版本信息:
《深入剖析Tomcat》一3.2 应用程序

但是,在URI后面可能会有一个查询字符串。若有,则查询字符串与URI是用一个问号分隔的。因此,parseRequest()方法会首先调用HttpRequest类的setQueryString()方法来获取查询字符串,并填充HttpRequest对象:
《深入剖析Tomcat》一3.2 应用程序

但是,大多数的URI都指向一个相对路径中的资源,当然URI也可以是一个绝对路径中的值,例如:

http://www.brainysoftware.com/index.html?name=Tarzan

parseRequest()方法会进行如下检查:
《深入剖析Tomcat》一3.2 应用程序

然后,查询字符串可能也会包含一个会话标识符,参数名为jsessionid。因此,parseRequest()方法还要检查是否包含会话标识符。若在查询字符串中包含jsessionid,则parseRequest()方法要获取jsessionid的值,并调用HttpRequest类的setRequestedSessionId()方法填充HttpRequest实例:
《深入剖析Tomcat》一3.2 应用程序

若存在参数jsessionid,则表明会话标识符在查询字符串中,而不在Cookie中。因此,需要调用该请求的setRequestSessionURL()方法并传入true值。否则,调用setRequestSessionURL()方法并传入false值,同时调用setRequestedSessionURL()方法并传入null。
此刻,jsessionid已经不包含uri的值。
然后,parseRequest()方法会将URI传入到normalize()方法中,对非正常的URL进行修正。例如,出现“”的地方会被替换为“/”。若URI本身是正常的,或不正常的地方可以修正,则normalize()方法会返回相同的URI或修正过的URI。若URI无法修正,则会认为它是无效的,normalize()方法返回null。在这种情况下(normalize()方法返回null),parseRequest()方法会在方法的末尾抛出异常。
最后,parseRequest()方法会设置HttpRequest对象的一些属性:
《深入剖析Tomcat》一3.2 应用程序

另外,若normalize()方法返回null,该方法会抛出异常:
《深入剖析Tomcat》一3.2 应用程序

3.解析请求头
请求头信息由一个HttpHeader类表示。该类将在第4章中详细讨论,但在这里,有以下5件事需要了解:
可以通过其类的无参构造函数来创建一个HttpHeader实例;
创建了HttpHeader实例后,可以将其传给SocketInputStream类的readHeader()方法。若有请求头信息可以读取,readHeader()方法会相应地填充HttpHeader对象。若没有请求头信息可以读取,则HttpHeader实例的nameEnd和valueEnd字段都会是0;
要获取请求头的名字和值,可以使用如下的方法:
《深入剖析Tomcat》一3.2 应用程序

parseHeaders()方法中有一个while循环,后者不断地从SocketInputStream中读取请求头信息,直到全部读完。在循环开始时,会先创建一个HttpHeader实例,然后将其传给SocketInputStream类的readHeader()方法:
《深入剖析Tomcat》一3.2 应用程序
然后,可以通过检查HttpHeader实例的nameEnd和valueEnd字段来判断是否已经从输入流中读取了所有的请求头信息:
《深入剖析Tomcat》一3.2 应用程序

若还有请求头没有读取,可以使用下面的方法获取请求头的名称和值:
《深入剖析Tomcat》一3.2 应用程序

当获取了请求头的名称和值之后,就可以调用HttpRequest对象的addHeader()方法,将其添加到HttpRequest对象的HashMap请求头中:

request.addHeader(name, value);

某些请求头包含一些属性设置信息,例如,当调用javax.servlet.ServletRequest类的getContentLength()方法时,会返回请求头“content-length”的值,而请求头“cookies”中是一些Cookie的集合。下面是相关的处理过程:
《深入剖析Tomcat》一3.2 应用程序
《深入剖析Tomcat》一3.2 应用程序

对Cookie的解析将在下一小节中详细讨论。

  1. 解析Cookie
    Cookie是由浏览器作为HTTP请求头的一部分发送的。这样的请求头的名称是“cookie”,其对应值是一些名/值对。下面是一个Cookie请求头的例子,其中包含两个Cookie:userName和password。

Cookie: userName=budi; password=pwd;
对Cookie的解析是通过org.apache.catalina.util.RequestUtil类的parseCookieHeader()方法完成的。该方法接受Cookie请求头,返回javax.servlet.http.Cookie类型的一个数组。数组中元素的个数与Cookie请求头中名/值对的数目相同。代码清单3-5给出了parseCookieHeader()方法的实现。
《深入剖析Tomcat》一3.2 应用程序

下面是HttpProcessor类的parseHeader()方法的一部分,后者处理Cookie的实现:
《深入剖析Tomcat》一3.2 应用程序

  1. 获取参数
    在调用javax.servlet.http.HttpservletRequest的getParameter()、getParameterMap()、getParameter-Names()或getParameterValues()方法之前,都不需要解析查询字符串或HTTP请求体来获得参数。因此,在HttpRequest类中,这4个方法的实现都会先调用parseParameter()方法。

参数只需要解析一次即可,而且也只会解析一次,因为,在请求体中包含参数,解析参数的工作会使SocketInputStream类读完整个字节流。HttpRequest类使用一个名为parsed的布尔变量来标识是否已经完成对参数的解析。
参数可以出现在查询字符串或请求体中。若用户使用GET方法请求servlet,则所有的参数都会在查询字符串中;若用户使用POST方法请求servlet,则请求体中也可能会有参数。所有的名/值对都会存储在一个HashMap对象中。servlet程序员可以将其作为一个Map对象获取参数(通过调用HttpservletRequest类的getParameterMap()方法)和参数名/值。但有一点需要注意,servlet程序员不可以修改参数值。因此,这里使用了一个特殊的HashMap类:org.apache.catalina.util.ParameterMap。
ParameterMap类继承自java.util.HashMap,其中有一个名为locked的布尔变量。只有当变量locked值为false时,才可以对ParameterMap对象中的名/值对进行添加、更新或者删除操作。否则,会抛出IllegalStateException异常。然而,可以在任何时候读取该值。ParameterMap类的定义在代码清单3-6中给出。它重写了对值进行添加、更新和删除的方法。对值的读取可在任何时候执行,但只有当变量locked的值为false时,才能调用添加、更新和删除值的方法。
《深入剖析Tomcat》一3.2 应用程序
《深入剖析Tomcat》一3.2 应用程序

下面,来看一下parseParameters()方法到底是如何工作的。
由于参数可以存在于查询字符串或HTTP请求体中,因此parseParameters()方法必须对这两者都进行检查。当解析完成时,参数会存储到对象变量parameters中,所以parseParameters()方法首先会检查布尔变量parsed,若该变量值为true,则方法直接返回:
《深入剖析Tomcat》一3.2 应用程序

然后,parseParameters()方法会创建一个名为results的ParameterMap类型的变量,将其指向变量parameters。若变量parameters为null,则parseParameters()方法会新创建一个ParameterMap对象:
《深入剖析Tomcat》一3.2 应用程序

然后,parseParameters()方法打开parameterMap对象的锁,使其可写:

results.setLocked(false);

接下来,parseParameters()方法检查字符串encoding,若encoding为null,则使用默认编码:
《深入剖析Tomcat》一3.2 应用程序

然后,parseParameters()方法会对参数进行解析,解析工作会调用org.apache.Catalina.util.RequestUtil类的parseParameters()方法完成:
《深入剖析Tomcat》一3.2 应用程序

接着,parseParameters()方法会检查HTTP请求体是否包含请求参数。若用户使用POST方法提交请求时,请求体会包含参数,则请求头“content-length”的值会大于0,“content-type”的值为“application/x-www-form-urlencoded”。下面的代码用于解析请求体:
《深入剖析Tomcat》一3.2 应用程序
《深入剖析Tomcat》一3.2 应用程序

最后,parseParameters()方法会锁定ParameterMap对象,将布尔变量parsed设置为true,将变量results赋值给变量parameters:
《深入剖析Tomcat》一3.2 应用程序

3.2.4 创建HttpResponse对象

HttpResponse类实现javax.servlet.http.HttpServletResponse接口,与其对应的外观类是HttpResponseFacade。图3-3展示了HttpResponse类及其相关类的UML类图。

《深入剖析Tomcat》一3.2 应用程序

在第2章中,HttpResponse类只实现了部分功能。例如,当调用它的其中一个print()方法时,它的getWriter()方法返回的java.io.PrintWriter对象并不会自动将结果发送到客户端。本章的应用程序将解决此问题。在此之前,为了便于理解,先说明一下什么是Writer。
在servlet中,可以使用PrintWriter对象向输出流中写字符。可以使用任意编码格式,但在向浏览器发送字符的时候,实际上都是字节流。因此,对于第2章,在ex02.pyrmont.HttpResponse类中使用了如下的getWriter()方法,也就没什么好奇怪的了:
《深入剖析Tomcat》一3.2 应用程序

那么,如何创建PrintWriter对象呢?可以通过传入一个java.io.OutputStream实例来创建PrintWriter对象。所传给PrintWriter类的print()方法或println()方法的任何字符串都会被转换为字节流,使用基本的输出流发送到客户端。
在第3章中,会使用ex03.pyrmont.connector.ResponseStream类的一个实例作为PrintWriter类的输出流对象。注意,ResponseStream类java.io.OutputStream类的直接子类。
也可以使用ex03.pyrmont.connector.ResponseWriter类来向客户端发送信息,该类继承自PrintWriter类。ResponseWriter类重写了所有的print()方法和println()方法,这样对这些方法进行调用时,会自动将信息发送客户端。因此使用一个ResponseWriter实例和一个基本ResponseStream对象。
可以通过传入ResponseStream对象的一个实例来实例化ResponseWriter类。但是,这里使用了一个java.io.OutputStreamWriter对象作为ResponseWriter对象和ResponseStream对象之间的桥梁。
使用OutputStreamWriter类,传入的字符会被转换为使用指定字符集的字节数组。其中所使用的字符集可以通过名称显式指定,也可以使用平台的默认字符集。每次调用写方法时,都会先使用编码转换器对给定字符进行编码转换。在被写入基本输出流之前,返回的字节数组会先存储在缓冲区中。缓冲区的大小是固定的,对于大多数应用来说,其默认值是足够大的。注意,传递给写方法的字符是没有缓冲的。
因此,getWriter()方法的实现如下所示:
《深入剖析Tomcat》一3.2 应用程序

3.2.5 静态资源处理器和servlet处理器

本章的ServletProcessor类与第2章的ex02.pyrmont.ServletProcessor类相似,都只有一个方法:process()方法。但是,ex03.pyrmont.connector.ServletProcessor类中的process()方法会接受一个HttpRequest对象和一个HttpResponse对象,而不是Request或Response的实例。下面是本章应用程序中process()方法的签名:

public void process(HttpRequest request, HttpResponse response);

此外,process()方法使用HttpRequestFacade类和HttpResponseFacade类作为request和response对象的外观类。当调用完servlet的service()方法后,它还会调用一次HttpResponse类的finishResponse()方法:
《深入剖析Tomcat》一3.2 应用程序

本章应用程序中的StaticResourceProcessor类几乎与ex02.pyrmont.StaticResourceProcessor类完全相同。

3.2.6 运行应用程序

要在Windows平台下运行该应用程序,需要在工作目录下执行如下命令:

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

要在Linux平台下运行,需要使用冒号来代替库文件之间的分号:

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

要显示index.html文件,可以使用如下的URL:

http://localhost:808O/index.html

要调用Primitiveservlet类,可以在浏览器中使用如下的URL:

http://localhost:8080/servlet/Primitiveservlet

可以在浏览器中看到如下的输入:

Hello. Roses are red. 
Violets are blue.

注意 在第2章运行PrimitiveServlet 不会显示第2行内容。
现在,可以通过如下的URL调用ModernServet类,该servlet在第2章的servlet容器中是无法运行的:

http://localhost:8080/servlet/Modernservlet

注意 ModernServlet类的源代码并不在工作目录的webroot目录下。
可以在测试servlet的URL后面添加查询字符串。例如使用如下的URL:

http://localhost:8080/servlet/ModernServlet?userName=tarzan&password=pwd

图3-4给出了使用上述URL运行ModernServlet的返回结果:

《深入剖析Tomcat》一3.2 应用程序