从本章开始,每章的应用程序都会按照模块进行划分。本章的应用程序包含3个模块:连接器模块、启动模块和核心模块。启动模块只有一个类(Bootstrap),后者负责启动应用程序。连接器模块中的类可分为以下5个类型: 连接器及其支持类(HttpConnector和HttpProcessor); 表示HTTP请求的类(HttpRequest)及其支持类; 表示HTTP响应的类(HttpResponse)及其支持类; 外观类(HttpRequestFacade和HttpResponseFacade); 常量类。核心模块包含两个类,servletProcessor类和StaticResourceProcessor类。图3-1展示了本章中应用程序的UML类图。为了使类图更容易理解,一些与HttpRequest类和HttpResponse类相关的类被省略掉。在讨论Request对象和Response对象时,你会看到它们各自的UML类图。
该图相比于图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处理器 运行应用程序
本章中的应用程序是通过ex03.pyrmont.startup.Bootstrap类来启动的。该类的定义在代码清单3-1中给出。
Bootstrap类的main()方法通过实例化HttpConnector类,并调用其start()方法就可以启动应用程序。HttpConnector类的定义在代码清单3-2中给出。
连接器是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中给出。
process()方法首先会获取套接字的输入流和输出流。但请注意,在这种方法中,使用了继承自java.io.InputStream的SocketInputStream类:
然后,它会创建一个HttpRequest实例和一个HttpResponse实例,然后将HttpRequest实例赋值给HttpResponse实例:
在本章的应用程序中,HttpResponse类比第2章中的Response类稍微复杂一点。首先,可以调用HttpResponse类的setHeader()方法向客户端发送响应头信息:
其次,process()方法会调用HttpProcessor类的两个私有方法来解析请求:
然后,它会根据请求的URI模式来判断,将HttpRequest对象和HttpResponse对象发送给ServletProcessor类或StaticResourceProcessor类来处理:
最后,它关闭套接字:
注意,HttpProcessor类使用了org.apache.catalina.util.StringManager类来发送错误消息:
调用HttpProcessor类的私有方法—parseRequest()、parseHeaders()和normalize()—来帮助填充HttpRequest对象。这些方法将在3.3.3节中进行讨论。
HttpRequest类实现了javax.servlet.http.HttpservletRequest接口。其外观类是HttpRequestFacade类。图3-2展示了HttpRequest类及其相关类的UML类图。
其中HttpRequest类的很多方法都是空方法(在第4章中会提供完整实现),但是servlet程序员已经可以从中获取引入的HTTP请求的请求头、Cookie和请求参数等信息了。这三类数据分别存储在如下引用变量中:
注意 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章和第2章中,ex01.pyrmont.HttpRequest类和ex02.pyrmont.HttpRequest类对HTTP请求进行过初步的解析。通过调用java.io.InputStream类的read()方法,从请求行中获取到了方法、URI、HTTP协议版本等信息:不需要试图进一步解析这两个应用程序的请求。在本章的应用程序中,将会使用ex03.pyrmont.connector.http.SocketInputStream类来进行解析,该类实际上就是org.apache.catalina.connector.http.SocketInputStream类的一个副本。该类提供了一些方法来获取请求行和请求头信息。需要传入一个InputStream对象和一个指明缓冲区大小的整数来创建一个SocketInputStream实例。在本章的应用程序中,在ex03.pyrmont.connector.http.HttpProcessor类的process()方法中会创建一个SocketInputStream对象,如以下代码段所示:
如前所述,之所以使用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()方法的实现。
parseRequest()方法首先会调用SocketInputStream类的readRequestLine()方法:
input.readRequestLine(requestLine);其中,变量requestLine是HttpProcessor中的一个HttpRequestLine实例:
private HttpRequestLine requestLine = new HttpRequestLine();调用其readRequestLine()方法,使用SocketInputStream对象中的信息填充HttpRequestLine实例。接着,parseRequest()方法从请求行中获取请求方法、URI和请求协议的版本信息:
但是,在URI后面可能会有一个查询字符串。若有,则查询字符串与URI是用一个问号分隔的。因此,parseRequest()方法会首先调用HttpRequest类的setQueryString()方法来获取查询字符串,并填充HttpRequest对象:
但是,大多数的URI都指向一个相对路径中的资源,当然URI也可以是一个绝对路径中的值,例如:
http://www.brainysoftware.com/index.html?name=TarzanparseRequest()方法会进行如下检查:
然后,查询字符串可能也会包含一个会话标识符,参数名为jsessionid。因此,parseRequest()方法还要检查是否包含会话标识符。若在查询字符串中包含jsessionid,则parseRequest()方法要获取jsessionid的值,并调用HttpRequest类的setRequestedSessionId()方法填充HttpRequest实例:
若存在参数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对象的一些属性:
另外,若normalize()方法返回null,该方法会抛出异常:
3.解析请求头请求头信息由一个HttpHeader类表示。该类将在第4章中详细讨论,但在这里,有以下5件事需要了解: 可以通过其类的无参构造函数来创建一个HttpHeader实例; 创建了HttpHeader实例后,可以将其传给SocketInputStream类的readHeader()方法。若有请求头信息可以读取,readHeader()方法会相应地填充HttpHeader对象。若没有请求头信息可以读取,则HttpHeader实例的nameEnd和valueEnd字段都会是0; 要获取请求头的名字和值,可以使用如下的方法:
parseHeaders()方法中有一个while循环,后者不断地从SocketInputStream中读取请求头信息,直到全部读完。在循环开始时,会先创建一个HttpHeader实例,然后将其传给SocketInputStream类的readHeader()方法:然后,可以通过检查HttpHeader实例的nameEnd和valueEnd字段来判断是否已经从输入流中读取了所有的请求头信息:
若还有请求头没有读取,可以使用下面的方法获取请求头的名称和值:
当获取了请求头的名称和值之后,就可以调用HttpRequest对象的addHeader()方法,将其添加到HttpRequest对象的HashMap请求头中:
request.addHeader(name, value);某些请求头包含一些属性设置信息,例如,当调用javax.servlet.ServletRequest类的getContentLength()方法时,会返回请求头“content-length”的值,而请求头“cookies”中是一些Cookie的集合。下面是相关的处理过程:
对Cookie的解析将在下一小节中详细讨论。
解析CookieCookie是由浏览器作为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()方法的实现。
下面是HttpProcessor类的parseHeader()方法的一部分,后者处理Cookie的实现:
获取参数在调用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时,才能调用添加、更新和删除值的方法。
下面,来看一下parseParameters()方法到底是如何工作的。由于参数可以存在于查询字符串或HTTP请求体中,因此parseParameters()方法必须对这两者都进行检查。当解析完成时,参数会存储到对象变量parameters中,所以parseParameters()方法首先会检查布尔变量parsed,若该变量值为true,则方法直接返回:
然后,parseParameters()方法会创建一个名为results的ParameterMap类型的变量,将其指向变量parameters。若变量parameters为null,则parseParameters()方法会新创建一个ParameterMap对象:
然后,parseParameters()方法打开parameterMap对象的锁,使其可写:
results.setLocked(false);接下来,parseParameters()方法检查字符串encoding,若encoding为null,则使用默认编码:
然后,parseParameters()方法会对参数进行解析,解析工作会调用org.apache.Catalina.util.RequestUtil类的parseParameters()方法完成:
接着,parseParameters()方法会检查HTTP请求体是否包含请求参数。若用户使用POST方法提交请求时,请求体会包含参数,则请求头“content-length”的值会大于0,“content-type”的值为“application/x-www-form-urlencoded”。下面的代码用于解析请求体:
最后,parseParameters()方法会锁定ParameterMap对象,将布尔变量parsed设置为true,将变量results赋值给变量parameters:
HttpResponse类实现javax.servlet.http.HttpServletResponse接口,与其对应的外观类是HttpResponseFacade。图3-3展示了HttpResponse类及其相关类的UML类图。
在第2章中,HttpResponse类只实现了部分功能。例如,当调用它的其中一个print()方法时,它的getWriter()方法返回的java.io.PrintWriter对象并不会自动将结果发送到客户端。本章的应用程序将解决此问题。在此之前,为了便于理解,先说明一下什么是Writer。在servlet中,可以使用PrintWriter对象向输出流中写字符。可以使用任意编码格式,但在向浏览器发送字符的时候,实际上都是字节流。因此,对于第2章,在ex02.pyrmont.HttpResponse类中使用了如下的getWriter()方法,也就没什么好奇怪的了:
那么,如何创建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()方法的实现如下所示:
本章的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()方法:
本章应用程序中的StaticResourceProcessor类几乎与ex02.pyrmont.StaticResourceProcessor类完全相同。
要在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(中文版 英文版)》.rar