How Tomcat Works中文版

    

How Tomcat Works中文版

介紹

概要

    歡迎閱讀《How Tomcat Works》這本書。這本書解剖了Tomcat4.1.12和5.0.18版本,解釋了它的servlet容器的內部運行機制,那是一個免費的,開源的,最受歡迎的servlet容器,代號爲Catalina。Tomcat是一個複雜的系統,由許多不一樣的組件構成。那些想要學習Tomcat運行機制的朋友大部分知道從何入手。這本書會提供一個藍圖,而後爲每個組件構造一個簡化版本,使得能夠更加容易的理解這些組件。在這以後纔會對真實的組件進行解釋。
    你應該從這份簡介開始閱讀,由於它解釋了這本書的結構,同時給你勾畫了這個項目構造的簡潔輪廓。「準備前提軟件」這一節會給你一些指示,例如你須要下載什麼樣的軟件,如何爲你的代碼建立目錄結構等等。

本書爲誰而做

    這本書是爲任何一個使用Java技術進行工做的人而準備的。
  • 假如你是一個servlet/jsp程序員或者一個Tomcat用戶,並且對一個servlet容器是如何工做這個問題你感興趣的話,這本書就是爲你準備的。
  • 假如你想加入Tomcat的開發團隊的話,這本書就是爲你準備的,由於你首先須要學習那些已存在的代碼是如何工做的。
  • 假如你從未涉及web開發,但你對通常意義上的軟件開發感興趣的話,你能夠在這本書學到一個像Tomcat同樣的大型項目是如何進行設計和開發的。
  • 假如你想配置和自定義Tomcat,你也應該讀讀這本書。
    爲了理解書中的討論,你須要瞭解Java面向對象編程技術以及servlet編程。假如你對這些不熟悉的話,這裏有不少書籍能夠參考,包括Budi的《Java for the Web with Servlets, JSP, and EJB》。爲了讓這些材料更容易理解,每一章開始都會有便於理解所討論主題的必要的背景資料介紹。

Servlet容器是如何工做的

    servlet容器是一個複雜的系統。不過,一個servlet容器要爲一個servlet的請求提供服務,基本上有三件事要作:
  • 建立一個request對象並填充那些有可能被所引用的servlet使用的信息,如參數、頭部、cookies、查詢字符串、URI等等。一個request對象是javax.servlet.ServletRequest或javax.servlet.http.ServletRequest接口的一個實例。
  • 建立一個response對象,所引用的servlet使用它來給客戶端發送響應。一個response對象javax.servlet.ServletResponse或javax.servlet.http.ServletResponse接口的一個實例。
  • 調用servlet的service方法,並傳入request和response對象。在這裏servlet會從request對象取值,給response寫值。
    當你讀這些章節的時候,你將會找到關於catalina servlet容器的詳細討論。

Catalina架構圖

    Catalina是一個很是複雜的,並優雅的設計開發出來的軟件,同時它也是模塊化的。基於「Servlet容器是如何工做的」這一節中提到的任務,你能夠把Catalina當作是由兩個主要模塊所組成的:鏈接器(connector)和容器(container)。在Figure I.1中的架構圖,固然是簡化了。在稍後的章節裏邊,你將會一個個的揭開全部更小的組件的神祕面紗。 
    如今從新回到Figure I.1,鏈接器是用來「鏈接」容器裏邊的請求的。它的工做是爲接收到每個HTTP請求構造一個request和response對象。而後它把流程傳遞給容器。容器從鏈接器接收到requset和response對象以後調用servlet的service方法用於響應。謹記,這個描述僅僅是冰山一角而已。這裏容器作了至關多事情。例如,在它調用servlet的service方法以前,它必須加載這個servlet,驗證用戶(假如須要的話),更新用戶會話等等。一個容器爲了處理這個進程使用了不少不一樣的模塊,這也並不奇怪。例如,管理模塊是用來處理用戶會話,而加載器是用來加載servlet類等等。

Tomcat 4和5

    這本書涵蓋了Tomcat4和5.這二者有一些不一樣之處:
  • Tomcat 5支持Servlet 2.4和JSP 2.0規範,而Tomcat 4支持Servlet 2.3和JSP 1.2。
  • 比起Tomcat 4,Tomcat 5有一些更有效率的默認鏈接器。
  • Tomcat 5共享一個後臺處理線程,而Tomcat 4的組件都有屬於本身的後臺處理線程。所以,就這一點而言,Tomcat 5消耗較少的資源。
  • Tomcat 5並不須要一個映射組件(mapper component)用於查找子組件,所以簡化了代碼。

各章概述

    這本書共20章,其中前面兩章做爲導言。
    第1章說明一個HTTP服務器是如何工做的,第2章突出介紹了一個簡單的servlet容器。接下來的兩章關注鏈接器,第5章到第20章涵蓋容器裏邊的每個組件。如下是各章節的摘要。
     注意:對於每一個章節,會有一個附帶程序,相似於正在被解釋的組件。
    第1章從這本書一開始就介紹了一個簡單的HTTP服務器。要創建一個可工做的HTTP服務器,你須要知道在java.net包裏邊的2個類的內部運做:Socket和ServerSocket。這裏有關於這2個類足夠的背景資料,使得你可以理解附帶程序是如何工做的。
    第2章說明簡單的servlet容器是如何工做的。這一章帶有2個servlet容器應用,能夠處理靜態資源和簡單的servlet請求。尤爲是你將會學到如何建立request和response對象,而後把它們傳遞給被請求的servlet的service方法。在servlet容器裏邊還有一個servlet,你能夠從一個web瀏覽器中調用它。
    第3章介紹了一個簡化版本的Tomcat 4默認鏈接器。這章裏邊的程序提供了一個學習工具,用於理解第4章裏邊的討論的鏈接器。
    第4章介紹了Tomcat 4的默認鏈接器。這個鏈接器已經不推薦使用,推薦使用一個更快的鏈接器,Coyote。不過,默認的鏈接器更簡單,更易於理解。
    第5章討論container模塊。container指的是org.apache.catalina.Container接口,有4種類型的container:engine, host, context和wrapper。這章提供了兩個工做於context和wrapper的程序。
    第6章解釋了Lifecycle接口。這個接口定義了一個Catalina組件的生命週期,並提供了一個優雅的方式,用來把在該組件發生的事件通知其餘組件。另外,Lifecycle接口提供了一個優雅的機制,用於在Catalina經過單一的start/stop來啓動和中止組件
    第7章包括日誌,該組件是用來記錄錯誤信息和其餘信息的。
    第8章解釋了加載器(loader)。加載器是一個重要的Catalina模塊,負責加載servlet和一個web應用所需的其餘類。這章還展現瞭如何實現應用的從新加載。
    第9章討論了管理器(manager)。這個組件用來管理會話管理中的會話信息。它解釋了各式各樣類型的管理器,管理器是如何把會話對象持久化的。在章末,你將會學到如何建立一個的應用,該應用使用StandardManager實例來運行一個使用會話對象進行儲值的servlet。
    第10章包括web應用程序安全性的限制,用來限制進入某些內容。你將會學習與安全相關的實體,例如
主角(principals),角色(roles),登錄配置,認證等等。你也將會寫兩個程序,它們在StandardContext對象中安裝一個身份驗證閥(authenticator valve)而且使用了基本的認證來對用戶進行認證。
    第11章詳細解釋了在一個web應用中表明一個servlet的org.apache.catalina.core.StandardWrapper類。特別的是,這章解釋了過濾器(filter)和一個servlet的service方法是怎樣給調用的。這章的附帶程序使用StandardWrapper實例來表明servlet。
    第12章包括了在一個web應用中表明一個servlet的org.apache.catalina.core.StandardContext類。特別是這章討論了一個StandardContext對象是如何給配置的,對於每一個傳入的HTTP請求在它裏面會發生什麼,是怎樣支持自動從新加載的,還有就是,在一個在其相關的組件中執行按期任務的線程中,Tomcat 5是如何共享的。
    第13章介紹了另外兩個容器:host和engine。你也一樣能夠找到這兩個容器的標準實現:org.apache.catalina.core.StandardHost和org.apache.catalina.core.StandardEngine。
    第14章提供了服務器和服務組件的部分。服務器爲整個servlet容器提供了一個優雅的啓動和中止機制,而服務爲容器和一個或多個鏈接器提供了一個支架。這章附帶的程序說明了如何使用服務器和服務。
    第15章解釋了經過Digester來配置web應用。Digester是來源於Apache軟件基金會的一個使人振奮的開源項目。對那些還沒有初步瞭解的人,這章經過一節略微介紹了Digester庫以及XML文件中如何使用它來把節點轉換爲Java對象。而後解釋了用來配置一個StandardContext實例的ContextConfig對象。
    第16章解釋了shutdown鉤子,Tomcat使用它總能得到一個機會用於clean-up,而不管用戶是怎樣中止它的(即適當的發送一個shutdown命令或者不適當的簡單關閉控制檯)。
    第17章討論了經過批處理文件和shell腳本對Tomcat進行啓動和中止。
    第18章介紹了部署工具(deployer),這個組件是負責部署和安裝web應用的。
    第19章討論了一個特殊的接口,ContainerServlet,可以讓servlet訪問Catalina的內部對象。特別是,它討論了Manager應用,你能夠經過它來部署應用程序。
    第20章討論了JMX以及Tomcat是如何經過爲其內部對象建立MBeans使得這些對象可管理的。

各章的程序

    每一章附帶了一個或者多個程序,側重於Catalina的一個特定的組件。一般你能夠找到這些簡化版本,不管是正在被解釋的組件或者解釋如何使用Catalina組件的代碼。各章節的程序的全部的類和接口都放在ex[章節號].pyrmont包或者它的子包。例如第1章的程序的類就是放在ex01.pyrmont包中。

準備的前提軟件

    這本書附帶的程序運行於J2SE1.4版本。壓縮源文件能夠從做者的網站 http://www.brainysoftware.com/中下載。它包括Tomcat 4.1.12和這本書所使用的程序的源代碼。假設你已經安裝了J2SE 1.4而且你的path環境變量中已經包括了JDK的安裝目錄,請按照下列步驟:
  1. 解壓縮ZIP文件。全部的解壓縮文件將放在一個新的目錄howtomcatworks中。howtomcatworks將是你的工做目錄。在howtomcatworks目錄下面將會有數個子目錄,包括lib (包括全部所需的庫),src (包括全部的源文件),webroot (包括一個HTML文件和三個servlet樣本),和webapps (包括示例應用程序)。
  2. 改變目錄到工做目錄下並編譯java文件。加入你使用的是Windows,運行win-compile.bat文件。假如你的計算機是Linux機器,敲入如下內容:(若有必要的話不用忘記使用chmod更改文件屬性)
        ./linux-compile.sh
     注意:你能夠在ZIP文件中的Readme.txt文件找到更多信息。

第一章:一個簡單的Web服務器

    本章說明java web服務器是如何工做的。Web服務器也成爲超文本傳輸協議(HTTP)服務器,由於它使用HTTP來跟客戶端進行通訊的,這一般是個web瀏覽器。一個基於java的web服務器使用兩個重要的類:java.net.Socket和java.net.ServerSocket,並經過HTTP消息進行通訊。所以這章就天然是從HTTP和這兩個類的討論開始的。接下去,解釋這章附帶的一個簡單的web服務器。

超文本傳輸協議(HTTP)

    HTTP是一種協議,容許web服務器和瀏覽器經過互聯網進行來發送和接受數據。它是一種請求和響應協議。客戶端請求一個文件而服務器響應請求。HTTP使用可靠的TCP鏈接--TCP默認使用80端口。第一個HTTP版是HTTP/0.9,而後被HTTP/1.0所替代。正在取代HTTP/1.0的是當前版本HTTP/1.1,它定義於徵求意見文檔(RFC) 2616,能夠從 http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf下載。
     注意:本節涵蓋的HTTP 1.1只是簡略的幫助你理解web服務器應用發送的消息。假如你對更多詳細信息感興趣,請閱讀RFC 2616。
    在HTTP中,始終都是客戶端經過創建鏈接和發送一個HTTP請求從而開啓一個事務。web服務器不須要聯繫客戶端或者對客戶端作一個回調鏈接。不管是客戶端或者服務器均可以提早終止鏈接。舉例來講,當你正在使用一個web瀏覽器的時候,能夠經過點擊瀏覽器上的中止按鈕來中止一個文件的下載進程,從而有效的關閉與web服務器的HTTP鏈接。

HTTP請求

    一個HTTP請求包括三個組成部分:
  • 方法—統一資源標識符(URI)—協議/版本
  • 請求的頭部
  • 主體內容
    下面是一個HTTP請求的例子:
POST /examples/default.jsp HTTP/1.1
Accept: text/plain; text/html
Accept-Language: en-gb
Connection: Keep-Alive
Host: localhost
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
Content-Length: 33
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate

lastName=Franks&firstName=Michael
    方法—統一資源標識符(URI)—協議/版本出如今請求的第一行。 
POST /examples/default.jsp HTTP/1.1
    這裏POST是請求方法,/examples/default.jsp是URI,而HTTP/1.1是協議/版本部分。
    每一個HTTP請求可使用HTTP標準裏邊提到的多種方法之一。HTTP 1.1支持7種類型的請求:GET, POST,
HEAD, OPTIONS, PUT, DELETE和TRACE。GET和POST在互聯網應用裏邊最廣泛使用的。
    URI徹底指明瞭一個互聯網資源。URI一般是相對服務器的根目錄解釋的。所以,始終一斜線/開頭。統一資源定位器(URL)實際上是一種URI(查看 http://www.ietf.org/rfc/rfc2396.txt)來的。該協議版本表明了正在使用的HTTP協議的版本。
    請求的頭部包含了關於客戶端環境和請求的主體內容的有用信息。例如它可能包括瀏覽器設置的語言,主體內容的長度等等。每一個頭部經過一個回車換行符(CRLF)來分隔的。
    對於HTTP請求格式來講,頭部和主體內容之間有一個回車換行符(CRLF)是至關重要的。CRLF告訴HTTP服務器主體內容是在什麼地方開始的。在一些互聯網編程書籍中,CRLF還被認爲是HTTP請求的第四部分。
    在前面一個HTTP請求中,主體內容只不過是下面一行:
lastName=Franks&firstName=Michael
    實體內容在一個典型的HTTP請求中能夠很容易的變得更長。

HTTP響應

    相似於HTTP請求,一個HTTP響應也包括三個組成部分:
  • 方法—統一資源標識符(URI)—協議/版本
  • 響應的頭部
  • 主體內容
    下面是一個HTTP響應的例子:
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Date: Mon, 5 Jan 2004 13:13:33 GMT
Content-Type: text/html
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
Content-Length: 112

<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Brainy Software
</body>
</html>
    響應頭部的第一行相似於請求頭部的第一行。第一行告訴你該協議使用HTTP 1.1,請求成功(200=成功),表示一切都運行良好。
    響應頭部和請求頭部相似,也包括不少有用的信息。響應的主體內容是響應自己的HTML內容。頭部和主體內容經過CRLF分隔開來。

Socket類

    套接字是網絡鏈接的一個端點。套接字使得一個應用能夠從網絡中讀取和寫入數據。放在兩個不一樣計算機上的兩個應用能夠經過鏈接發送和接受字節流。爲了從你的應用發送一條信息到另外一個應用,你須要知道另外一個應用的IP地址和套接字端口。在Java裏邊,套接字指的是java.net.Socket類。
    要建立一個套接字,你可使用Socket類衆多構造方法中的一個。其中一個接收主機名稱和端口號:
public Socket (java.lang.String host, int port)
    在這裏主機是指遠程機器名稱或者IP地址,端口是指遠程應用的端口號。例如,要鏈接yahoo.com的80端口,你須要構造如下的Socket對象:
new Socket ("yahoo.com", 80);
    一旦你成功建立了一個Socket類的實例,你可使用它來發送和接受字節流。要發送字節流,你首先必須調用Socket類的getOutputStream方法來獲取一個java.io.OutputStream對象。要發送文本到一個遠程應用,你常常要從返回的OutputStream對象中構造一個java.io.PrintWriter對象。要從鏈接的另外一端接受字節流,你能夠調用Socket類的getInputStream方法用來返回一個java.io.InputStream對象。
    如下的代碼片斷建立了一個套接字,能夠和本地HTTP服務器(127.0.0.1是指本地主機)進行通信,發送一個HTTP請求,並從服務器接受響應。它建立了一個StringBuffer對象來保存響應並在控制檯上打印出來。
Socket socket = new Socket("127.0.0.1", "8080");
OutputStream os = socket.getOutputStream();
boolean autoflush = true;
PrintWriter out = new PrintWriter(
socket.getOutputStream(), autoflush);
BufferedReader in = new BufferedReader(
new InputStreamReader( socket.getInputstream() ));
// send an HTTP request to the web server
out.println("GET /index.jsp HTTP/1.1");
out.println("Host: localhost:8080");
out.println("Connection: Close");
out.println();
// read the response
boolean loop = true;
StringBuffer sb = new StringBuffer(8096);
while (loop) {
    if ( in.ready() ) {
        int i=0;
        while (i!=-1) {
            i = in.read();
            sb.append((char) i);
        }
    loop = false;
    }
    Thread.currentThread().sleep(50);
}
// display the response to the out console
System.out.println(sb.toString());
socket.close();
    請注意,爲了從web服務器獲取適當的響應,你須要發送一個遵照HTTP協議的HTTP請求。假如你已經閱讀了前面一節超文本傳輸協議(HTTP),你應該可以理解上面代碼提到的HTTP請求。
     注意:你能夠本書附帶的com.brainysoftware.pyrmont.util.HttpSniffer類來發送一個HTTP請求並顯示響應。要使用這個Java程序,你必須鏈接到互聯網上。雖然它有可能並不會起做用,假如你有設置防火牆的話。

ServerSocket類

    Socket類表明一個客戶端套接字,即任什麼時候候你想鏈接到一個遠程服務器應用的時候你構造的套接字,如今,假如你想實施一個服務器應用,例如一個HTTP服務器或者FTP服務器,你須要一種不一樣的作法。這是由於你的服務器必須隨時待命,由於它不知道一個客戶端應用何時會嘗試去鏈接它。爲了讓你的應用能隨時待命,你須要使用java.net.ServerSocket類。這是服務器套接字的實現。
    ServerSocket和Socket不一樣,服務器套接字的角色是等待來自客戶端的鏈接請求。一旦服務器套接字得到一個鏈接請求,它建立一個Socket實例來與客戶端進行通訊。
    要建立一個服務器套接字,你須要使用ServerSocket類提供的四個構造方法中的一個。你須要指定IP地址和服務器套接字將要進行監聽的端口號。一般,IP地址將會是127.0.0.1,也就是說,服務器套接字將會監聽本地機器。服務器套接字正在監聽的IP地址被稱爲是綁定地址。服務器套接字的另外一個重要的屬性是backlog,這是服務器套接字開始拒絕傳入的請求以前,傳入的鏈接請求的最大隊列長度。
    其中一個ServerSocket類的構造方法以下所示:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
    對於這個構造方法,綁定地址必須是java.net.InetAddress的一個實例。一種構造InetAddress對象的簡單的方法是調用它的靜態方法getByName,傳入一個包含主機名稱的字符串,就像下面的代碼同樣。
InetAddress.getByName("127.0.0.1");
    下面一行代碼構造了一個監聽的本地機器8080端口的ServerSocket,它的backlog爲1。
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
    一旦你有一個ServerSocket實例,你可讓它在綁定地址和服務器套接字正在監聽的端口上等待傳入的鏈接請求。你能夠經過調用ServerSocket類的accept方法作到這點。這個方法只會在有鏈接請求時纔會返回,而且返回值是一個Socket類的實例。Socket對象接下去能夠發送字節流並從客戶端應用中接受字節流,就像前一節"Socket類"解釋的那樣。實際上,這章附帶的程序中,accept方法是惟一用到的方法。

應用程序

    咱們的web服務器應用程序放在ex01.pyrmont包裏邊,由三個類組成:
  • HttpServer
  • Request
  • Response
    這個應用程序的入口點(靜態main方法)能夠在HttpServer類裏邊找到。main方法建立了一個HttpServer的實例並調用了它的await方法。await方法,顧名思義就是在一個指定的端口上等待HTTP請求,處理它們併發送響應返回客戶端。它一直等待直至接收到shutdown命令。
    應用程序不能作什麼,除了發送靜態資源,例如放在一個特定目錄的HTML文件和圖像文件。它也在控制檯上顯示傳入的HTTP請求的字節流。不過,它不給瀏覽器發送任何的頭部例如日期或者cookies。
    如今咱們將在如下各小節中看看這三個類。

HttpServer類

    HttpServer類表明一個web服務器並展現在Listing 1.1中。請注意,await方法放在Listing 1.2中,爲了節省空間沒有重複放在Listing 1.1中。
        Listing 1.1: HttpServer類
package ex01.pyrmont;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.File;
public class HttpServer {
    /** WEB_ROOT is the directory where our HTML and other files reside.
    * For this package, WEB_ROOT is the "webroot" directory under the
    * working directory.
    * The working directory is the location in the file system
    * from where the java command was invoked.
    */
    public static final String WEB_ROOT =
    System.getProperty("user.dir") + File.separator + "webroot";
    // shutdown command
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    // the shutdown command received
    private boolean shutdown = false;
    public static void main(String[] args) {
        HttpServer server = new HttpServer();
        server.await();
    }
    public void await() {
        ...
    }
}
        Listing 1.2: HttpServer類的await方法
public void await() {
    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);
    }
    // Loop waiting for a request
    while (!shutdown) {
        Socket socket = null;
        InputStream input = null;
        OutputStream output = null;
        try {
            socket = serverSocket.accept();
            input = socket.getInputStream();
            output = socket.getOutputStream();
            // create Request object and parse
            Request request = new Request(input);
            request.parse();
            // create Response object
            Response response = new Response(output);
            response.setRequest(request);
            response.sendStaticResource();
            // Close the socket
            socket.close();
            //check if the previous URI is a shutdown command
            shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
        }
        catch (Exception e) {
            e.printStackTrace ();
            continue;
        }
    }
}
     web服務器能提供公共靜態final變量WEB_ROOT所在的目錄和它下面全部的子目錄下的靜態資源。以下所示,WEB_ROOT被初始化:
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
    代碼列表包括一個叫webroot的目錄,包含了一些你能夠用來測試這個應用程序的靜態資源。你一樣能夠在相同的目錄下找到幾個servlet用於測試下一章的應用程序。爲了請求一個靜態資源,在你的瀏覽器的地址欄或者網址框裏邊敲入如下的URL:
http://machineName:port/staticResource
    若是你要從一個不一樣的機器上發送請求到你的應用程序正在運行的機器上,machineName應該是正在運行應用程序的機器的名稱或者IP地址。假如你的瀏覽器在同一臺機器上,你可使用localhost做爲machineName。端口是8080,staticResource是你須要請求的文件的名稱,且必須位於WEB_ROOT裏邊。
    舉例來講,假如你正在使用同一臺計算機上測試應用程序,而且你想要調用HttpServer對象去發送一個index.html文件,你可使用一下的URL:
http://localhost:8080/index.html
    要中止服務器,你能夠在web瀏覽器的地址欄或者網址框裏邊敲入預約義字符串,就在URL的host:port的後面,發送一個shutdown命令。shutdown命令是在HttpServer類的靜態final變量SHUTDOWN裏邊定義的:
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    所以,要中止服務器,使用下面的URL:
http://localhost:8080/SHUTDOWN
    如今咱們來看看Listing 1.2印出來的await方法。
    使用方法名await而不是wait是由於wait方法是與線程相關的java.lang.Object類的一個重要方法。
    await方法首先建立一個ServerSocket實例而後進入一個while循環。
serverSocket = new ServerSocket(port, 1,
    InetAddress.getByName("127.0.0.1"));
...
// Loop waiting for a request
while (!shutdown) {
    ...
}
    while循環裏邊的代碼運行到ServletSocket的accept方法停了下來,只會在8080端口接收到一個HTTP請求的時候才返回:
socket = serverSocket.accept();
    接收到請求以後,await方法從accept方法返回的Socket實例中取得java.io.InputStream和java.io.OutputStream對象。
input = socket.getInputStream();
output = socket.getOutputStream();
    await方法接下去建立一個ex01.pyrmont.Request對象而且調用它的parse方法去解析HTTP請求的原始數據。
// create Request object and parse
Request request = new Request(input);
request.parse ();
    在這以後,await方法建立一個Response對象,把Request對象設置給它,並調用它的sendStaticResource方法。
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
    最後,await關閉套接字並調用Request的getUri來檢測HTTP請求的URI是否是一個shutdown命令。假如是的話,shutdown變量將被設置爲true且程序會退出while循環。
// Close the socket
socket.close ();
//check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);

Request類

    ex01.pyrmont.Request類表明一個HTTP請求。從負責與客戶端通訊的Socket中傳遞過來InputStream對象來構造這個類的一個實例。你調用InputStream對象其中一個read方法來獲取HTTP請求的原始數據。
    Request類顯示在Listing 1.3。Request對象有parse和getUri兩個公共方法,分別在Listings 1.4和1.5列出來。
        Listing 1.3: Request類
package ex01.pyrmont;
import java.io.InputStream;
import java.io.IOException;
public class Request {
    private InputStream input;
    private String uri;
    public Request(InputStream input) {
        this.input = input;
    }
    public void parse() {
        ...
    }
    private String parseUri(String requestString) {
        ...
    }
    public String getUri() {
        return uri;
    }
}
        Listing 1.4: Request類的parse方法
public void parse() {
    // Read a set of characters from the socket
    StringBuffer request = new StringBuffer(2048);
    int i;
    byte[] buffer = new byte[2048];
    try {
        i = input.read(buffer);
    }
    catch (IOException e) {
        e.printStackTrace();
        i = -1;
    }
    for (int j=0; j<i; j++) {
        request.append((char) buffer[j]);
    }
    System.out.print(request.toString());
    uri = parseUri(request.toString());
}
        Listing 1.5: Request類的parseUri方法
private String parseUri(String requestString) {
    int index1, index2;
    index1 = requestString.indexOf(' ');
    if (index1 != -1) {
        index2 = requestString.indexOf(' ', index1 + 1);
        if (index2 > index1)
        return requestString.substring(index1 + 1, index2);
    }
    return null;
}
    parse方法解析HTTP請求裏邊的原始數據。這個方法沒有作不少事情。它惟一可用的信息是經過調用HTTP請求的私有方法parseUri得到的URI。parseUri方法在uri變量裏邊存儲URI。公共方法getUri被調用並返回HTTP請求的URI。
     注意:在第3章和下面各章的附帶程序裏邊,HTTP請求將會對原始數據進行更多的處理。
    爲了理解parse和parseUri方法是怎樣工做的,你須要知道上一節「超文本傳輸協議(HTTP)」討論的HTTP請求的結構。在這一章中,咱們僅僅關注HTTP請求的第一部分,請求行。請求行從一個方法標記開始,接下去是請求的URI和協議版本,最後是用回車換行符(CRLF)結束。請求行裏邊的元素是經過一個空格來分隔的。例如,使用GET方法來請求index.html文件的請求行以下所示。
GET /index.html HTTP/1.1
    parse方法從傳遞給Requst對象的套接字的InputStream中讀取整個字節流並在一個緩衝區中存儲字節數組。而後它使用緩衝區字節數據的字節來填入一個StringBuffer對象,而且把表明StringBuffer的字符串傳遞給parseUri方法。
    parse方法列在Listing 1.4。
    而後parseUri方法從請求行裏邊得到URI。Listing 1.5給出了parseUri方法。parseUri方法搜索請求裏邊的第一個和第二個空格並從中獲取URI。

Response類

    ex01.pyrmont.Response類表明一個HTTP響應,在Listing 1.6裏邊給出。
        Listing 1.6: Response類
package ex01.pyrmont;
import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.File;
/*
HTTP Response = Status-Line
*(( general-header | response-header | entity-header ) CRLF)
CRLF
[ message-body ]
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
*/
public class Response {
    private static final int BUFFER_SIZE = 1024;
    Request request;
    OutputStream output;
    public Response(OutputStream output) {
        this.output = output;
    }
    public void setRequest(Request request) {
        this.request = request;
    }
    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;
        try {
            File file = new File(HttpServer.WEB_ROOT, request.getUri());
            if (file.exists()) {
                fis = new FileInputStream(file);
                int ch = fis.read(bytes, 0, BUFFER_SIZE);
                while (ch!=-1) {
                    output.write(bytes, 0, ch);
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
            }
            else {
            // file not found
            String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
                "Content-Type: text/html\r\n" +
                "Content-Length: 23\r\n" +
                "\r\n" +
                "<h1>File Not Found</h1>";
            output.write(errorMessage.getBytes());
            }
        }
        catch (Exception e) {
            // thrown if cannot instantiate a File object
            System.out.println(e.toString() );
        }
        finally {
            if (fis!=null)
            fis.close();
        }
    }
}
    首先注意到它的構造方法接收一個java.io.OutputStream對象,就像以下所示。
public Response(OutputStream output) {
    this.output = output;
}
    響應對象是經過傳遞由套接字得到的OutputStream對象給HttpServer類的await方法來構造的。Response類有兩個公共方法:setRequest和sendStaticResource。setRequest方法用來傳遞一個Request對象給Response對象。
    sendStaticResource方法是用來發送一個靜態資源,例如一個HTML文件。它首先經過傳遞上一級目錄的路徑和子路徑給File累的構造方法來實例化java.io.File類。
File file = new File(HttpServer.WEB_ROOT, request.getUri());
    而後它檢查該文件是否存在。假如存在的話,經過傳遞File對象讓sendStaticResource構造一個java.io.FileInputStream對象。而後,它調用FileInputStream的read方法並把字節數組寫入OutputStream對象。請注意,這種狀況下,靜態資源是做爲原始數據發送給瀏覽器的。
if (file.exists()) {
    fis = new FileInputstream(file);
    int ch = fis.read(bytes, 0, BUFFER_SIZE);
    while (ch!=-1) {
        output.write(bytes, 0, ch);
        ch = fis.read(bytes, 0, BUFFER_SIZE);
    }
}
    假如文件並不存在,sendStaticResource方法發送一個錯誤信息到瀏覽器。
String errorMessage =
    "Content-Type: text/html\r\n" +
    "Content-Length: 23\r\n" +
    "\r\n" +
    "<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());

運行應用程序

    爲了運行應用程序,能夠在工做目錄下敲入下面的命令:
java ex01.pyrmont.HttpServer
    爲了測試應用程序,能夠打開你的瀏覽器並在地址欄或網址框中敲入下面的命令:
http://localhost:8080/index.html
    正如Figure 1.1所示,你將會在你的瀏覽器裏邊看到index.html頁面。
Figure 1.1: web服務器的輸出
    在控制檯中,你能夠看到相似於下面的HTTP請求:
GET /index.html HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,
application/vnd.ms-excel, application/msword, application/vnd.ms-
powerpoint, application/x-shockwave-flash, application/pdf, */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR
1.1.4322)
Host: localhost:8080
Connection: Keep-Alive

GET /images/logo.gif HTTP/1.1
Accept: */*
Referer: http://localhost:8080/index.html
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR
1.1.4322)
Host: localhost:8080
Connection: Keep-Alive

總結

    在這章中你已經看到一個簡單的web服務器是如何工做的。這章附帶的程序僅僅由三個類組成,並非全功能的。不過,它提供了一個良好的學習工具。下一章將要討論動態內容的處理過程。

第2章:一個簡單的Servlet容器

概要

    本章經過兩個程序來講明你如何開發本身的servlet容器。第一個程序被設計得足夠簡單使得你能理解一個servlet容器是如何工做的。而後它演變爲第二個稍微複雜的servlet容器。
     注意:每個servlet容器的應用程序都是從前一章的應用程序逐漸演變過來的,直至一個全功能的Tomcat servlet容器在第17章被創建起來。
    這兩個servlet容器均可以處理簡單的servlet和靜態資源。你可使用PrimitiveServlet來測試這個容器。PrimitiveServlet在Listing 2.1中列出而且它的類文件能夠在webroot目錄下找到。更復雜的servlet就超過這些容器的能力了,可是你將會在如下各章中學到如何創建更復雜的servlet容器。
        Listing 2.1: PrimitiveServlet.java
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
public class PrimitiveServlet implements Servlet {
    public void init(ServletConfig config) throws ServletException {
    System.out.println("init");
    }
    public void service(ServletRequest request, ServletResponse response)
        throws ServletException, IOException {
        System.out.println("from service");
        PrintWriter out = response.getWriter();
        out.println("Hello. Roses are red.");
        out.print("Violets are blue.");
    }
    public void destroy() {
        System.out.println("destroy");
    }
    public String getServletInfo() {
        return null;
    }
    public ServletConfig getServletConfig() {
        return null;
    }
}
    兩個應用程序的類都放在ex02.pyrmont包裏邊。爲了理解應用程序是如何工做的,你須要熟悉javax.servlet.Servlet接口。爲了給你複習一下,將會在本章的首節討論這個接口。在這以後,你將會學習一個servlet容器作了什麼工做來爲一個servlet提供HTTP請求。

javax.servlet.Servlet接口

    Servlet編程是經過javax.servlet和javax.servlet.http這兩個包的類和接口來實現的。其中一個相當重要的就是javax.servlet.Servlet接口了。全部的servlet必須實現實現或者繼承實現該接口的類。
    Servlet接口有五個方法,其用法以下。
public void init(ServletConfig config) throws ServletException
public void service(ServletRequest request, ServletResponse response)
    throws ServletException, java.io.IOException
public void destroy()
public ServletConfig getServletConfig()
public java.lang.String getServletInfo()
    在Servlet的五個方法中,init,service和destroy是servlet的生命週期方法。在servlet類已經初始化以後,init方法將會被servlet容器所調用。servlet容器只調用一次,以此代表servlet已經被加載進服務中。init方法必須在servlet能夠接受任何請求以前成功運行完畢。一個servlet程序員能夠經過覆蓋這個方法來寫那些僅僅只要運行一次的初始化代碼,例如加載數據庫驅動,值初始化等等。在其餘狀況下,這個方法一般是留空的。
    servlet容器爲servlet請求調用它的service方法。servlet容器傳遞一個javax.servlet.ServletRequest對象和javax.servlet.ServletResponse對象。ServletRequest對象包括客戶端的HTTP請求信息,而ServletResponse對象封裝servlet的響應。在servlet的生命週期中,service方法將會給調用屢次。
    當從服務中移除一個servlet實例的時候,servlet容器調用destroy方法。這一般發生在servlet容器正在被關閉或者servlet容器須要一些空閒內存的時候。僅僅在全部servlet線程的service方法已經退出或者超時淘汰的時候,這個方法才被調用。在servlet容器已經調用完destroy方法以後,在同一個servlet裏邊將不會再調用service方法。destroy方法提供了一個機會來清理任何已經被佔用的資源,例如內存,文件句柄和線程,並確保任何持久化狀態和servlet的內存當前狀態是同步的。
    Listing 2.1介紹了一個名爲PrimitiveServlet的servlet的代碼,是一個很是簡單的的servlet,你能夠用來測試本章裏邊的servlet容器應用程序。PrimitiveServlet類實現了javax.servlet.Servlet(全部的servlet都必須這樣作),併爲Servlet的這五個方法都提供了實現。PrimitiveServlet作的事情很是簡單。在init,service或者destroy中的任何一個方法每次被調用的時候,servlet把方法名寫到標準控制檯上面去。另外,service方法從ServletResponse對象得到java.io.PrintWriter實例,併發送字符串到瀏覽器去。

應用程序1

    如今,讓咱們從一個servlet容器的角度來研究一下servlet編程。總的來講,一個全功能的servlet容器會爲servlet的每一個HTTP請求作下面一些工做:
  • 當第一次調用servlet的時候,加載該servlet類並調用servlet的init方法(僅僅一次)。
  • 對每次請求,構造一個javax.servlet.ServletRequest實例和一個javax.servlet.ServletResponse實例。
  • 調用servlet的service方法,同時傳遞ServletRequest和ServletResponse對象。
  • 當servlet類被關閉的時候,調用servlet的destroy方法並卸載servlet類。
    本章的第一個servlet容器不是全功能的。所以,她不能運行什麼除了很是簡單的servlet,並且也不調用servlet的init方法和destroy方法。相反它作了下面的事情:
  • 等待HTTP請求。
  • 構造一個ServletRequest對象和一個ServletResponse對象。
  • 假如該請求須要一個靜態資源的話,調用StaticResourceProcessor實例的process方法,同時傳遞ServletRequest和ServletResponse對象。
  • 假如該請求須要一個servlet的話,加載servlet類並調用servlet的service方法,同時傳遞ServletRequest和ServletResponse對象。
     注意:在這個servlet容器中,每一次servlet被請求的時候,servlet類都會被加載。
    第一個應用程序由6個類組成:
  • HttpServer1
  • Request
  • Response
  • StaticResourceProcessor
  • ServletProcessor1
  • Constants
    Figure 2.1顯示了第一個servlet容器的UML圖。
    Figure 2.1: 第一個servlet容器的UML圖
    這個應用程序的入口點(靜態main方法)能夠在HttpServer1類裏邊找到。main方法建立了一個HttpServer1的實例並調用了它的await方法。await方法等待HTTP請求,爲每次請求建立一個Request對象和一個Response對象,並把他們分發到一個StaticResourceProcessor實例或者一個ServletProcessor實例中去,這取決於請求一個靜態資源仍是一個servlet。
    Constants類包括涉及其餘類的靜態final變量WEB_ROOT。WEB_ROOT顯示了PrimitiveServlet和這個容器能夠提供的靜態資源的位置。
    HttpServer1實例會一直等待HTTP請求,直到接收到一個shutdown的命令。你科研用第1章的作法發送一個shutdown命令。
    應用程序裏邊的每一個類都會在如下各節中進行討論。

HttpServer1類

    這個應用程序裏邊的HttpServer1類相似於第1章裏邊的簡單服務器應用程序的HttpServer類。不過,在這個應用程序裏邊HttpServer1類能夠同時提供靜態資源和servlet。要請求一個靜態資源,你能夠在你的瀏覽器地址欄或者網址框裏邊敲入一個URL:
http://machineName:port/staticResource
    就像是在第1章提到的,你能夠請求一個靜態資源。
    爲了請求一個servlet,你可使用下面的URL:
http://machineName:port/servlet/servletClass
    所以,假如你在本地請求一個名爲PrimitiveServlet的servlet,你在瀏覽器的地址欄或者網址框中敲入:
http://localhost:8080/servlet/PrimitiveServlet
    servlet容器能夠就提供PrimitiveServlet了。不過,假如你調用其餘servlet,如ModernServlet,servlet容器將會拋出一個異常。在如下各章中,你將會創建能夠處理這兩個狀況的程序。
    HttpServer1類顯示在Listing 2.2中。
        Listing 2.2: HttpServer1類的await方法
package ex02.pyrmont;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
public class HttpServer1 {
    /** WEB_ROOT is the directory where our HTML and other files reside.
    * For this package, WEB_ROOT is the "webroot" directory under the
    * working directory.
    * The working directory is the location in the file system
    * from where the java command was invoked.
    */
    // shutdown command
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    // the shutdown command received
    private boolean shutdown = false;
    public static void main(String[] args) {
        HttpServer1 server = new HttpServer1();
        server.await();
    }
    public void await() {
        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);
        }
        // Loop waiting for a request
        while (!shutdown) {
            Socket socket = null;
            InputStream input = null;
            OutputStream output = null;
            try {
                socket = serverSocket.accept();
                input = socket.getInputstream();
                output = socket.getOutputStream();
                // create Request object and parse
                Request request = new Request(input);
                request.parse();
                // create Response object
                Response response = new Response(output);
                response.setRequest(request);
                // check if this is a request for a servlet or
                // a static resource
                // a request for a servlet begins with "/servlet/"
                if (request.getUri().startsWith("/servlet/")) {
                    ServletProcessor1 processor = new ServletProcessor1();
                    processor.process(request, response);
                }
                else {
                    StaticResoureProcessor processor =
                    new StaticResourceProcessor();
                    processor.process(request, response);
                }
                // Close the socket
                socket.close();
                //check if the previous URI is a shutdown command
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
            }
            catch (Exception e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
    }
}
    類的await方法等待HTTP請求直到一個shutdown命令給發出,讓你想起第1章的await方法。Listing 2.2的await方法和第1章的區別是,在Listing 2.2裏邊,請求能夠分發給一個StaticResourceProcessor或者一個ServletProcessor。假如URI包括字符串/servlet/的話,請求將會轉發到後面去。
    否則的話,請求將會傳遞給StaticResourceProcessor實例 instance. 請注意,這部分在Listing 2.2中灰暗顯示。

Request類

    servlet的service方法從servlet容器中接收一個javax.servlet.ServletRequest實例和一個javax.servlet.ServletResponse實例。這就是說對於每個HTTP請求,servlet容器必須構造一個ServletRequest對象和一個ServletResponse對象並把它們傳遞給正在服務的servlet的service方法。
    ex02.pyrmont.Request類表明一個request對象並被傳遞給servlet的service方法。就自己而言,它必須實現javax.servlet.ServletRequest接口。這個類必須提供這個接口全部方法的實現。不過,咱們想要讓它很是簡單而且僅僅提供實現其中一些方法,咱們在如下各章中再實現所有的方法。要編譯Request類,你須要把這些方法的實現留空。假如你看過Listing 2.3中的Request類,你將會看到那些須要返回一個對象的方法返回了null
        Listing 2.3: Request類
package ex02.pyrmont;
import java.io.InputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
public class Request implements ServletRequest {
    private InputStream input;
    private String uri;
    public Request(InputStream input){
        this.input = input;
    }
    public String getUri() {
        return uri;
    }
    private String parseUri(String requestString) {
        int index1, index2;
        index1 = requestString.indexOf(' ');
        if (index1 != -1) {
            index2 = requestString.indexOf(' ', index1 + 1);
            if (index2 > index1)
                return requestString.substring(index1 + 1, index2);
        }
        return null;
    }
    public void parse() {
        // Read a set of characters from the socket
        StringBuffer request = new StringBuffer(2048);
        int i;
        byte[] buffer = new byte[2048];
        try {
            i = input.read(buffer);
        }
        catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
        for (int j=0; j<i; j++) {
            request.append((char) buffer(j));
        }
        System.out.print(request.toString());
        uri = parseUri(request.toString());
    }
    /* implementation of ServletRequest */
    public Object getAttribute(String attribute) {
        return null;
    }
    public Enumeration getAttributeNames() {
        return null;
    }
    public String getRealPath(String path) {
        return null;
    }
    public RequestDispatcher getRequestDispatcher(String path) {
        return null;
    }
    public boolean isSecure() {
        return false;
    }
    public String getCharacterEncoding() {
        return null;
    }
    public int getContentLength() {
        return 0;
    }
    public String getContentType() {
        return null;
    }
    public ServletInputStream getInputStream() throws IOException {
        return null;
    }
    public Locale getLocale() {
        return null;
    }
    public Enumeration getLocales() {
        return null;
    }
    public String getParameter(String name) {
        return null;
    }
    public Map getParameterMap() {
        return null;
    }
    public Enumeration getParameterNames() {
        return null;
    }
    public String[] getParameterValues(String parameter) {
        return null;
    }
    public String getProtocol() {
        return null;
    }
    public BufferedReader getReader() throws IOException {
        return null;
    }
    public String getRemoteAddr() {
        return null;
    }
    public String getRemoteHost() {
        return null;
    }
    public String getScheme() {
        return null;
    }
    public String getServerName() {
        return null;
    }
    public int getServerPort() {
        return 0;
    }
    public void removeAttribute(String attribute) { }
    public void setAttribute(String key, Object value) { }
    public void setCharacterEncoding(String encoding)
        throws UnsupportedEncodingException { }
}
    另外,Request類仍然有在第1章中討論的parse和getUri方法。

Response類

    在Listing 2.4列出的ex02.pyrmont.Response類,實現了javax.servlet.ServletResponse。就自己而言,這個類必須提供接口裏邊的全部方法的實現。相似於Request類,咱們把除了getWriter以外的全部方法的實現留空。
        Listing 2.4: Response類
package ex02.pyrmont;
import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.File;
import java.io.PrintWriter;
import java.util.Locale;
import javax.servlet.ServletResponse;
import javax.servlet.ServletOutputStream;
public class Response implements ServletResponse {
    private static final int BUFFER_SIZE = 1024;
    Request request;
    OutputStream output;
    PrintWriter writer;
    public Response(OutputStream output) {
        this.output = output;
    }
    public void setRequest(Request request) {
        this.request = request;
    }
    /* This method is used to serve static pages */
    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputstream fis = null;
        try {
            /* request.getUri has been replaced by request.getRequestURI */
            File file = new File(Constants.WEB_ROOT, request.getUri());
            fis = new FileInputstream(file);
            /*
            HTTP Response = Status-Line
            *(( general-header | response-header | entity-header ) CRLF)
            CRLF
            [ message-body ]
            Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
            */
            int ch = fis.read(bytes, 0, BUFFER_SIZE);
            while (ch!=-1) {
                output.write(bytes, 0, ch);
                ch = fis.read(bytes, 0, BUFFER_SIZE);
            }
        }
        catch (FileNotFoundException e) {
            String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
                "Content-Type: text/html\r\n" +
                "Content-Length: 23\r\n" +
                "\r\n" +
                "<h1>File Not Found</h1>";
            output.write(errorMessage.getBytes());
        }
        finally {
            if (fis!=null)
            fis.close();
        }
    }
    /** implementation of ServletResponse */
    public void flushBuffer() throws IOException ( }
    public int getBufferSize() {
        return 0;
    }
    public String getCharacterEncoding() {
        return null;
    }
    public Locale getLocale() {
        return null;
    }
    public ServletOutputStream getOutputStream() throws IOException {
        return null;
    }
    public PrintWriter getWriter() throws IOException {
        // autoflush is true, println() will flush,
        // but print() will not.
        writer = new PrintWriter(output, true);
        return writer;
    }
    public boolean isCommitted() {
        return false;
    }
    public void reset() { }
    public void resetBuffer() { }
    public void setBufferSize(int size) { }
    public void setContentLength(int length) { }
    public void setContentType(String type) { }
    public void setLocale(Locale locale) { }
}
    在getWriter方法中,PrintWriter類的構造方法的第二個參數是一個布爾值代表是否容許自動刷新。傳遞true做爲第二個參數將會使任何println方法的調用都會刷新輸出(output)。不過,print方法不會刷新輸出。
    所以,任何print方法的調用都會發生在servlet的service方法的最後一行,輸出將不會被髮送到瀏覽器。這個缺點將會在下一個應用程序中修復。
    Response類還擁有在第1章中談到的sendStaticResource方法。

StaticResourceProcessor類

    ex02.pyrmont.StaticResourceProcessor類用來提供靜態資源請求。惟一的方法是process方法。Listing 2.5給出了StaticResourceProcessor類。
        Listing 2.5: StaticResourceProcessor類
package ex02.pyrmont;
import java.io.IOException;
public class StaticResourceProcessor {
    public void process(Request request, Response response) {
    try {
        response.sendStaticResource();
    }
    catch (IOException e) {
        e.printStackTrace();
    }
    }
}
    process方法接收兩個參數:一個ex02.pyrmont.Request實例和一個ex02.pyrmont.Response實例。這個方法只是簡單的呼叫Response對象的sendStaticResource方法。

ServletProcessor1類

    Listing 2.6中的ex02.pyrmont.ServletProcessor1類用於處理servlet的HTTP請求。
        Listing 2.6: ServletProcessor1類
package ex02.pyrmont;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.io.File;
import java.io.IOException;
import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class ServletProcessor1 {
    public void process(Request request, Response response) {
        String uri = request.getUri();
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);
        URLClassLoader loader = null;
        try {
            // create a URLClassLoader
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            File classPath = new File(Constants.WEB_ROOT);
            // the forming of repository is taken from the
            // createClassLoader method in
            // org.apache.catalina.startup.ClassLoaderFactory
            String repository =(new URL("file", null, classPath.getCanonicalPath() +
                File.separator)).toString() ;
            // the code for forming the URL is taken from
            // the addRepository method in
            // org.apache.catalina.loader.StandardClassLoader.
            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((ServletRequest) request,
            (ServletResponse) response);
        }
        catch (Exception e) {
            System.out.println(e.toString());
        }
        catch (Throwable e) {
            System.out.println(e.toString());
        }
    }
}
    ServletProcessor1類出奇的簡單,僅僅由一個方法組成:process。這個方法接受兩個參數:一個
javax.servlet.ServletRequest實例和一個javax.servlet.ServletResponse實例。該方法從ServletRequest中經過調用getRequestUri方法得到URI:
String uri = request.getUri();
    請記住URI是如下形式的:
/servlet/servletName
    在這裏servletName是servlet類的名字。
    要加載servlet類,咱們須要從URI中知道servlet的名稱。咱們可使用process方法的下一行來得到servlet的名字:
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
    接下去,process方法加載servlet。要完成這個,你須要建立一個類加載器並告訴這個類加載器要加載的類的位置。對於這個servlet容器,類加載器直接在Constants指向的目錄裏邊查找。WEB_ROOT就是指向工做目錄下面的webroot目錄。
     注意: 類加載器將在第8章詳細討論。
    要加載servlet,你可使用java.net.URLClassLoader類,它是java.lang.ClassLoader類的一個直接子類。一旦你擁有一個URLClassLoader實例,你使用它的loadClass方法去加載一個servlet類。如今舉例說明URLClassLoader類是straightforward直接轉發的。這個類有三個構造方法,其中最簡單的是:
public URLClassLoader(URL[] urls);
    這裏urls是一個java.net.URL的對象數組,這些對象指向了加載類時候查找的位置。任何以/結尾的URL都假設是一個目錄。不然,URL會Otherwise, the URL假定是一個將被下載並在須要的時候打開的JAR文件。
     注意:在一個servlet容器裏邊,一個類加載器能夠找到servlet的地方被稱爲資源庫(repository)。
    在咱們的應用程序裏邊,類加載器必須查找的地方只有一個,如工做目錄下面的webroot目錄。所以,咱們首先建立一個單個URL組成的數組。URL類提供了一系列的構造方法,因此有不少中構造一個URL對象的方式。對於這個應用程序來講,咱們使用Tomcat中的另外一個類的相同的構造方法。這個構造方法以下所示。
public URL(URL context, java.lang.String spec, URLStreamHandler hander)
throws MalformedURLException
    你可使用這個構造方法,併爲第二個參數傳遞一個說明,爲第一個和第三個參數都傳遞null。不過,這裏有另一個接受三個參數的構造方法:
public URL(java.lang.String protocol, java.lang.String host,
    java.lang.String file) throws MalformedURLException
    所以,假如你使用下面的代碼時,編譯器將不會知道你指的是那個構造方法:
new URL(null, aString, null);
    你能夠經過告訴編譯器第三個參數的類型來避開這個問題,例如。
URLStreamHandler streamHandler = null;
new URL(null, aString, streamHandler);
    你可使用下面的代碼在組成一個包含資源庫(servlet類能夠被找到的地方)的字符串,並做爲第二個參數,
String repository = (new URL("file", null,
classPath.getCanonicalPath() + File.separator)).toString() ;
    把全部的片斷組合在一塊兒,這就是用來構造適當的URLClassLoader實例的process方法中的一部分:
// create a URLClassLoader
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(Constants.WEB_ROOT);
String repository = (new URL("file", null,
classPath.getCanonicalPath() + File.separator)).toString() ;
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
     注意: 用來生成資源庫的代碼是從org.apache.catalina.startup.ClassLoaderFactory的createClassLoader方法來的,而生成URL的代碼是從org.apache.catalina.loader.StandardClassLoader的addRepository方法來的。不過,在如下各章以前你不須要擔憂這些類。
    當有了一個類加載器,你可使用loadClass方法加載一個servlet:
Class myClass = null;
try {
    myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
    System.out.println(e.toString());
}
    而後,process方法建立一個servlet類加載器的實例, 把它向下轉換(downcast)爲javax.servlet.Servlet, 並調用servlet的service方法:
Servlet servlet = null;
try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) request,(ServletResponse) response);
}
catch (Exception e) {
    System.out.println(e.toString());
}
catch (Throwable e) {
    System.out.println(e.toString());
}

運行應用程序

    要在Windows上運行該應用程序,在工做目錄下面敲入如下命令:
java -classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer1
    在Linux下,你使用一個冒號來分隔兩個庫:
java -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer1
    要測試該應用程序,在瀏覽器的地址欄或者網址框中敲入:
http://localhost:8080/index.html
    或者
http://localhost:8080/servlet/PrimitiveServlet
    當調用PrimitiveServlet的時候,你將會在你的瀏覽器看到下面的文本:
Hello. Roses are red.
    請注意,由於只是第一個字符串被刷新到瀏覽器,因此你不能看到第二個字符串Violets are blue。咱們將在第3章修復這個問題。

應用程序2

    第一個應用程序有一個嚴重的問題。在ServletProcessor1類的process方法,你向上轉換ex02.pyrmont.Request實例爲javax.servlet.ServletRequest,並做爲第一個參數傳遞給servlet的service方法。你也向下轉換ex02.pyrmont.Response實例爲javax.servlet.ServletResponse,並做爲第二個參數傳遞給servlet的service方法。
try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) request,(ServletResponse) response);
}
    這會危害安全性。知道這個servlet容器的內部運做的Servlet程序員能夠分別把ServletRequest和ServletResponse實例向下轉換爲ex02.pyrmont.Request和ex02.pyrmont.Response,並調用他們的公共方法。擁有一個Request實例,它們就能夠調用parse方法。擁有一個Response實例,就能夠調用sendStaticResource方法。
    你不能夠把parse和sendStaticResource方法設置爲私有的,由於它們將會被其餘的類調用。不過,這兩個方法是在個servlet內部是不可見的。其中一個解決辦法就是讓Request和Response類擁有默認訪問修飾,因此它們不能在ex02.pyrmont包的外部使用。不過,這裏有一個更優雅的解決辦法:經過使用facade類。請看Figure 2.2中的UML圖。
                            Figure 2.2: Façade classes
    在這第二個應用程序中,咱們增長了兩個façade類: RequestFacade和ResponseFacade。RequestFacade實現了ServletRequest接口並經過在構造方法中傳遞一個引用了ServletRequest對象的Request實例做爲參數來實例化。ServletRequest接口中每一個方法的實現都調用了Request對象的相應方法。然而ServletRequest對象自己是私有的,並不能在類的外部訪問。咱們構造了一個RequestFacade對象並把它傳遞給service方法,而不是向下轉換Request對象爲ServletRequest對象並傳遞給service方法。Servlet程序員仍然能夠向下轉換ServletRequest實例爲RequestFacade,不過它們只能夠訪問ServletRequest接口裏邊的公共方法。如今parseUri方法就是安全的了。
    Listing 2.7 顯示了一個不完整的RequestFacade類
        Listing 2.7: RequestFacade類
package ex02.pyrmont;
public class RequestFacade implements ServletRequest {
    private ServleLRequest request = null;
    public RequestFacade(Request request) {
        this.request = request;
    }
    /* implementation of the ServletRequest*/
    public Object getAttribute(String attribute) {
        return request.getAttribute(attribute);
    }
    public Enumeration getAttributeNames() {
        return request.getAttributeNames();
    }
    ...
}
    請注意RequestFacade的構造方法。它接受一個Request對象並立刻賦值給私有的servletRequest對象。還請注意,RequestFacade類的每一個方法調用ServletRequest對象的相應的方法。
    這一樣使用於ResponseFacade類。
    這裏是應用程序2中使用的類:
  • HttpServer2
  • Request
  • Response
  • StaticResourceProcessor
  • ServletProcessor2
  • Constants
    HttpServer2類相似於HttpServer1,除了它在await方法中使用ServletProcessor2而不是ServletProcessor1:
if (request.getUri().startWith("/servlet/")) {
    servletProcessor2 processor = new ServletProcessor2();
    processor.process(request, response);
}
else {
    ...
}
    ServletProcessor2類相似於ServletProcessor1,除了process方法中的如下部分:
Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) requestFacade,(ServletResponse)responseFacade);
}

運行應用程序

    要在Windows上運行該應用程序,在工做目錄下面敲入如下命令:
java -classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer2
    在Linux下,你使用一個冒號來分隔兩個庫:
java -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer2
    你可使用與應用程序1同樣的地址,並獲得相同的結果。

總結

    本章討論了兩個簡單的能夠用來提供靜態資源和處理像PrimitiveServlet這麼簡單的servlet的servlet容器。一樣也提供了關於javax.servlet.Servlet接口和相關類型的背景信息。

第3章:鏈接器

概要

    在介紹中提到,Catalina中有兩個主要的模塊:鏈接器和容器。本章中你將會寫一個能夠建立更好的請求和響應對象的鏈接器,用來改進第2章中的程序。一個符合Servlet 2.3和2.4規範的鏈接器必須建立javax.servlet.http.HttpServletRequest和javax.servlet.http.HttpServletResponse,並傳遞給被調用的servlet的service方法。在第2章 中,servlet容器只能夠運行實現了javax.servlet.Servlet的servlet,並傳遞 javax.servlet.ServletRequest和javax.servlet.ServletResponse實例給service方法。由於鏈接器並不知道servlet的類型(例如它是否實現了javax.servlet.Servlet,繼承了javax.servlet.GenericServlet,或者繼承了javax.servlet.http.HttpServlet),因此鏈接器必須始終提供HttpServletRequest和HttpServletResponse的實例。
    在本章的應用程序中,鏈接器解析HTTP請求頭部並讓servlet能夠得到頭部, cookies, 參數名/值等等。你將會完善第2章中Response類的getWriter方法,讓它可以正確運行。因爲這些改進,你將會從 PrimitiveServlet中獲取一個完整的響應,並可以運行更加複雜的ModernServlet。
    本章你創建的鏈接器是將在第4章詳細討論的Tomcat4的默認鏈接器的一個簡化版本。Tomcat的默認鏈接器在Tomcat4中是不推薦使用的,但它仍然能夠做爲一個很是棒的學習工具。在這章的剩餘部分,"connector"指的是內置在咱們應用程序的模塊。
     注意:和上一章的應用程序不一樣的是,本章的應用程序中,鏈接器和容器是分離的。
    本章的應用程序能夠在包ex03.pyrmont和它的子包中找到。組成鏈接器的這些類是包
ex03.pyrmont.connector 和ex03.pyrmont.connector.http的一部分。在本章的開頭,每一個附帶的程序都有個bootstrap類用來啓動應用程序。不過,在這個階段,還沒有有一個機制來中止這個應用程序。一旦運行,你必須經過關閉控制檯(Windows)或者殺死進程(UNIX/Linux)的方法來魯 莽的關閉應用程序。
    在咱們解釋該應用程序以前,讓咱們先來講說包org.apache.catalina.util裏邊的StringManager類。這個類用來處理這個程序中不一樣模塊和Catalina自身的錯誤信息的國際化。以後會討論附帶的應用程序。

StringManager類

    一個像Tomcat這樣的大型應用須要仔細的處理錯誤信息。在Tomcat中,錯誤信息對於系統管理員和servlet程序員都是有用的。例 如,Tomcat記錄錯誤信息,讓系統管理員能夠定位發生的任何異常。對servlet程序員來講,Tomcat會在拋出的任何一個 javax.servlet.ServletException中發送一個錯誤信息,這樣程序員能夠知道他/她的servlet究竟發送什麼錯誤了。
    Tomcat所採用的方法是在一個屬性文件裏邊存儲錯誤信息,這樣,能夠容易的修改這些信息。不過,Tomcat中有數以百計的類。把全部類使用的錯誤信 息存儲到一個大的屬性文件裏邊將會容易產生維護的噩夢。爲了不這一狀況,Tomcat爲每一個包都分配一個屬性文件。例如,在包 org.apache.catalina.connector裏邊的屬性文件包含了該包全部的類拋出的全部錯誤信息。每一個屬性文件都會被一個 org.apache.catalina.util.StringManager類的實例所處理。當Tomcat運行時,將會有許多 StringManager實例,每一個實例會讀取包對應的一個屬性文件。此外,因爲Tomcat的受歡迎程度,提供多種語言的錯誤信息也是有意義的。目前,有三種語言是被支持的。英語的錯誤信息屬性文件名爲LocalStrings.properties。另外兩個是西班牙語和日語,分別放在 LocalStrings_es.properties和LocalStrings_ja.properties裏邊。
    當包裏邊的一個類須要查找放在該包屬性文件的一個錯誤信息時,它首先會得到一個StringManager實例。不過,相同包裏邊的許多類可能也須要 StringManager,爲每一個對象建立一個StringManager實例是一種資源浪費。所以,StringManager類被設計成一個StringManager實例能夠被包裏邊的全部類共享。假如你熟悉設計模式,你將會正確的猜到StringManager是一個單例 (singleton)類。僅有的一個構造方法是私有的,全部你不能在類的外部使用new關鍵字來實例化。你經過傳遞一個包名來調用它的公共靜態方法 getManager來得到一個實例。每一個實例存儲在一個以包名爲鍵(key)的Hashtable中。
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;
}
     注意:一篇關於單例模式的題爲"The Singleton Pattern"的文章能夠在附帶的ZIP文件中找到。
    例如,要在包ex03.pyrmont.connector.http的一個類中使用StringManager,能夠傳遞包名給StringManager類的getManager方法:
StringManager sm =
    StringManager.getManager("ex03.pyrmont.connector.http");
    在包ex03.pyrmont.connector.http中,你會找到三個屬性文件:LocalStrings.properties, LocalStrings_es.properties和LocalStrings_ja.properties。StringManager實例是根據運行程序的服務器的區域設置來決定使用哪一個文件的。假如你打開LocalStrings.properties,非註釋的第一行是這樣的:
httpConnector.alreadyInitialized=HTTP connector has already been initialized
    要得到一個錯誤信息,可使用StringManager類的getString,並傳遞一個錯誤代號。這是其中一個重載方法:
public String getString(String key)
    經過傳遞httpConnector.alreadyInitialized做爲getString的參數,將會返回"HTTP connector has already been initialized"。

應用程序

    從本章開始,每章附帶的應用程序都會分紅模塊。這章的應用程序由三個模塊組成:connector,
startup和core。
    startup模塊只有一個類,Bootstrap,用來啓動應用的。connector模塊的類能夠分爲五組:
  • 鏈接器和它的支撐類(HttpConnector和HttpProcessor)。
  • 指代HTTP請求的類(HttpRequest)和它的輔助類。
  • 指代HTTP響應的類(HttpResponse)和它的輔助類。
  • Facade類(HttpRequestFacade和HttpResponseFacade)。
  • Constant類
    core模塊由兩個類組成:ServletProcessor和StaticResourceProcessor。
    Figure 3.1顯示了這個應用的類的UML圖。爲了讓圖更具可讀性,HttpRequest和HttpResponse相關的類給省略了。你能夠在咱們討論Request和Response對象的時候分別找到UML圖。
            Figure 3.1: 應用程序的UML圖
    和Figure 2.1的UML圖相比,第2章中的HttpServer類被分離爲兩個類:HttpConnector和HttpProcessor,Request被 HttpRequest所取代,而Response被HttpResponse所取代。一樣,本章的應用使用了更多的類。
    第2章中的HttpServer類的職責是等待HTTP請求並建立請求和響應對象。在本章的應用中,等待HTTP請求的工做交給HttpConnector實例,而建立請求和響應對象的工做交給了HttpProcessor實例。
    本章中,HTTP請求對象由實現了javax.servlet.http.HttpServletRequest的HttpRequest類來表明。一個 HttpRequest對象將會給轉換爲一個HttpServletRequest實例並傳遞給被調用的servlet的service方法。所以,每一個 HttpRequest實例必須適當增長字段,以便servlet可使用它們。值須要賦給HttpRequest對象,包括URI,查詢字符串,參數,cookies和其餘的頭部等等。由於鏈接器並不知道被調用的servlet須要哪一個值,因此鏈接器必須從HTTP請求中解析全部可得到的值。不過,解析一個HTTP請求牽涉昂貴的字符串和其餘操做,假如只是解析servlet須要的值的話,鏈接器就能節省許多CPU週期。例如,假如servlet不 解析任何一個請求參數(例如不調用javax.servlet.http.HttpServletRequest的getParameter, getParameterMap,getParameterNames或者getParameterValues方法),鏈接器就不須要從查詢字符串或者 HTTP請求內容中解析這些參數。Tomcat的默認鏈接器(和本章應用程序的鏈接器)試圖不解析參數直到servlet真正須要它的時候,經過這樣來得到更高效率。
    Tomcat的默認鏈接器和咱們的鏈接器使用SocketInputStream類來從套接字的InputStream中讀取字節流。一個 SocketInputStream實例對從套接字的getInputStream方法中返回的java.io.InputStream實例進行包裝。 SocketInputStream類提供了兩個重要的方法:readRequestLine和readHeader。readRequestLine返回一個HTTP請求的第一行。例如,這行包括了URI,方法和HTTP版本。由於從套接字的輸入流中處理字節流意味着只讀取一次,從第一個字節到最後一個字節(而且不回退),所以readHeader被調用以前,readRequestLine必須只被調用一次。readHeader每次被調用來得到一個頭部的名/值對,而且應該被重複的調用知道全部的頭部被讀取到。readRequestLine的返回值是一個HttpRequestLine的實例,而 readHeader的返回值是一個HttpHeader對象。咱們將在下節中討論類HttpRequestLine和HttpHeader。
    HttpProcessor對象建立了HttpRequest的實例,所以必須在它們當中增長字段。HttpProcessor類使用它的parse方法 來解析一個HTTP請求中的請求行和頭部。解析出來並把值賦給HttpProcessor對象的這些字段。不過,parse方法並不解析請求內容或者請求 字符串裏邊的參數。這個任務留給了HttpRequest對象它們。只是當servlet須要一個參數時,查詢字符串或者請求內容纔會被解析。
    另外一個跟上一個應用程序比較的改進是用來啓動應用程序的bootstrap類ex03.pyrmont.startup.Bootstrap的出現。
    咱們將會在下面的子節裏邊詳細說明該應用程序:
  • 啓動應用程序
  • 鏈接器
  • 建立一個HttpRequest對象
  • 建立一個HttpResponse對象
  • 靜態資源處理器和servlet處理器
  • 運行應用程序

啓動應用程序

    你能夠從ex03.pyrmont.startup.Bootstrap類來啓動應用程序。這個類在Listing 3.1中給出。
        Listing 3.1: Bootstrap類
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方法。HttpConnector類在Listing 3.2給出。
        Listing 3.2: HttpConnector類的start方法
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實例
  • 調用HttpProcessor的process方法
      注意:run方法相似於第2章中HttpServer1類的await方法。
    立刻你就會看到HttpConnector類和ex02.pyrmont.HttpServer1類很是相像,除了從 java.net.ServerSocket類的accept方法中得到一個套接字以後,一個HttpProcessor實例會被建立,而且經過傳遞該套 接字給它的process方法調用。
     注意:HttpConnector類有另外一個方法叫getScheme,用來返回一個scheme(HTTP)。
    HttpProcessor類的process方法接受前來的HTTP請求的套接字,會作下面的事情:
1. 建立一個HttpRequest對象。
2. 建立一個HttpResponse對象。
3. 解析HTTP請求的第一行和頭部,並放到HttpRequest對象。
4. 解析HttpRequest和HttpResponse對象到一個ServletProcessor或者 StaticResourceProcessor。像第2章裏邊說的,ServletProcessor調用被請求的servlet的service方 法,而StaticResourceProcessor發送一個靜態資源的內容。
    process方法在Listing 3.3給出.
Listing 3.3: HttpProcessor類process方法
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首先得到套接字的輸入流和輸出流。請注意,在這個方法中,咱們適合繼承了java.io.InputStream的SocketInputStream類。
SocketInputStream input = null;
OutputStream output = null;
try {
    input = new SocketInputStream(socket.getInputStream(), 2048);
    output = socket.getOutputStream();
    而後,它建立一個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類複雜得多。舉例來講,你能夠經過調用他的setHeader方法來發送頭部到一個客戶端。
response.setHeader("Server", "Pyrmont Servlet Container");
    接下去,process方法調用HttpProcessor類中的兩個私有方法來解析請求。
parseRequest(input, output);
parseHeaders (input);
    而後,它根據請求URI的形式把HttpRequest和HttpResponse對象傳給ServletProcessor或者StaticResourceProcessor進行處理。
if (request.getRequestURI().startsWith("/servlet/")) {
    ServletProcessor processor = new ServletProcessor();
    processor.process(request, response);
}
else {
    StaticResourceProcessor processor =
        new StaticResourceProcessor();
    processor.process(request, response);
}
    最後,它關閉套接字。
socket.close();
    也要注意的是,HttpProcessor類使用org.apache.catalina.util.StringManager類來發送錯誤信息:
protected StringManager sm =
    StringManager.getManager("ex03.pyrmont.connector.http");
    HttpProcessor類中的私有方法--parseRequest,parseHeaders和normalize,是用來幫助填充HttpRequest的。這些方法將會在下節"建立一個HttpRequest對象"中進行討論。

建立一個HttpRequest對象

    HttpRequest類實現了javax.servlet.http.HttpServletRequest。跟隨它的是一個叫作 HttpRequestFacade的facade類。Figure 3.2顯示了HttpRequest類和它的相關類的UML圖。
        Figure 3.2: HttpRequest類和它的相關類
    HttpRequest類的不少方法都留空(你須要等到第4章纔會徹底實現),可是servlet程序員已經能夠從到來的HTTP請求中得到頭部,cookies和參數。這三種類型的值被存儲在下面幾個引用變量中:
protected HashMap headers = new HashMap();
protected ArrayList cookies = new ArrayList();
protected ParameterMap parameters = null;
     注意:ParameterMap類將會在「獲取參數」這節中解釋。
    所以,一個servlet程序員能夠從javax.servlet.http.HttpServletRequest中的下列方法中取得正確的返回 值:getCookies,getDateHeader,getHeader, getHeaderNames, getHeaders, getParameter, getPrameterMap,getParameterNames和getParameterValues。就像你在HttpRequest類中看到的同樣,一旦你取得了頭部,cookies和填充了正確的值的參數,相關的方法的實現是很簡單的。
    不用說,這裏主要的挑戰是解析HTTP請求和填充HttpRequest類。對於頭部和cookies,HttpRequest類提供了addHeader和addCookie方法用於HttpProcessor的parseHeaders方法調用。當須要的時候,會使用 HttpRequest類的parseParameters方法來解析參數。在本節中全部的方法都會被討論。
    由於HTTP請求的解析是一項至關複雜的任務,因此本節會分爲如下幾個小節:
  • 讀取套接字的輸入流
  • 解析請求行
  • 解析頭部
  • 解析cookies
  • 獲取參數

讀取套接字的輸入流

    在第1章和第2章中,你在ex01.pyrmont.HttpRequest和ex02.pyrmont.HttpRequest類中作了一點請求解析。 你經過調用java.io.InputStream類的read方法獲取了請求行,包括方法,URI和HTTP版本:
byte[] buffer = new byte [2048];
try {
    // input is the InputStream from the socket.
    i = input.read(buffer);
}
    你沒有試圖爲那兩個應用程序去進一步解析請求。不過,在本章的應用程序中,你擁有 ex03.pyrmont.connector.http.SocketInputStream類,這是 org.apache.catalina.connector.http.SocketInputStream的一個拷貝。這個類提供了方法不只用來獲取請求行,還有請求頭部。
    你經過傳遞一個InputStream和一個指代實例使用的緩衝區大小的整數,來構建一個SocketInputStream實例。在本章中,你在 ex03.pyrmont.connector.http.HttpProcessor的process方法中建立了一個 SocketInputStream對象,就像下面的代碼片斷同樣:
SocketInputStream input = null;
OutputStream output = null;
try {
    input = new SocketInputStream(socket.getInputStream(), 2048);
    ...
    就像前面提到的同樣,擁有一個SocketInputStream是爲了兩個重要方法:readRequestLine和readHeader。請繼續往下閱讀。

解析請求行

    HttpProcessor的process方法調用私有方法parseRequest用來解析請求行,例如一個HTTP請求的第一行。這裏是一個請求行的例子:
GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1
    請求行的第二部分是URI加上一個查詢字符串。在上面的例子中,URI是這樣的:
/myApp/ModernServlet
    另外,在問好後面的任何東西都是查詢字符串。所以,查詢字符串是這樣的:
userName=tarzan&password=pwd
    查詢字符串能夠包括零個或多個參數。在上面的例子中,有兩個參數名/值對,userName/tarzan和password/pwd。在servlet/JSP編程中,參數名jsessionid是用來攜帶一個會話標識符。會話標識符常常被做爲cookie來嵌入,可是程序員能夠選擇把它嵌入到查詢字符串去,例如,當瀏覽器的cookie被禁用的時候。
    當parseRequest方法被HttpProcessor類的process方法調用的時候,request變量指向一個HttpRequest實例。parseRequest方法解析請求行用來得到幾個值並把這些值賦給HttpRequest對象。如今,讓咱們來關注一下在Listing 3.4中的parseRequest方法。
        Listing 3.4:HttpProcessor類中的parseRequest方法
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 = "";
        }
        request.setRequestedSessionURL(true);
        uri = uri.substring(0, semicolon) + rest;
    }
    else {
        request.setRequestedSessionId(null);
        request.setRequestedSessionURL(false);
    }
    // Normalize URI (using String operations at the moment)
    String normalizedUri = normalize(uri);
    // Set the corresponding request properties
    ((HttpRequest) request).setMethod(method);
    request.setProtocol(protocol);
    if (normalizedUri != null) {
        ((HttpRequest) request).setRequestURI(normalizedUri);
    }
    else {
        ((HttpRequest) request).setRequestURI(uri);
    }
    if (normalizedUri == null) {
        throw new ServletException("Invalid URI: " + uri + "'");
    }
}
    parseRequest方法首先調用SocketInputStream類的readRequestLine方法:
input.readRequestLine(requestLine);
    在這裏requestLine是HttpProcessor裏邊的HttpRequestLine的一個實例:
private HttpRequestLine requestLine = new HttpRequestLine();
    調用它的readRequestLine方法來告訴SocketInputStream去填入HttpRequestLine實例。
    接下去,parseRequest方法得到請求行的方法,URI和協議:
String method =
    new String(requestLine.method, 0, requestLine.methodEnd);
String uri = null;
String protocol = new String(requestLine.protocol, 0, requestLine.protocolEnd);
    不過,在URI後面能夠有查詢字符串,假如存在的話,查詢字符串會被一個問好分隔開來。所以,parseRequest方法試圖首先獲取查詢字符串。並調用setQueryString方法來填充HttpRequest對象:
// Parse any query parameters out of the request URI
int question = requestLine.indexOf("?");
if (question >= 0) { // there is a query string.
    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);
}
    不過,大多數狀況下,URI指向一個相對資源,URI還能夠是一個絕對值,就像下面所示:
http://www.brainysoftware.com/index.html?name=Tarzan
    parseRequest方法一樣也檢查這種狀況:
// Checking for an absolute URI (with the HTTP protocol)
if (!uri.startsWith("/")) {
    // not starting with /, this is an absolute URI
    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);
        }
    }
}
    而後,查詢字符串也能夠包含一個會話標識符,用jsessionid參數名來指代。所以,parseRequest方法也檢查一個會話標識符。假如在查詢字符串裏邊找到jessionid,方法就取得會話標識符,並經過調用setRequestedSessionId方法把值交給HttpRequest實例:
// 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 = "";
    }
    request.setRequestedSessionURL (true);
    uri = uri.substring(0, semicolon) + rest;
}
else {
    request.setRequestedSessionId(null);
    request.setRequestedSessionURL(false);
}
    當jsessionid被找到,也意味着會話標識符是攜帶在查詢字符串裏邊,而不是在cookie裏邊。所以,傳遞true給request的 setRequestSessionURL方法。不然,傳遞false給setRequestSessionURL方法並傳遞null給 setRequestedSessionURL方法。
    到這個時候,uri的值已經被去掉了jsessionid。
    接下去,parseRequest方法傳遞uri給normalize方法,用於糾正「異常」的URI。例如,任何\的出現都會給/替代。假如uri是正確的格式或者異常能夠給糾正的話,normalize將會返回相同的或者被糾正後的URI。假如URI不能糾正的話,它將會給認爲是非法的而且一般會返回null。在這種狀況下(一般返回null),parseRequest將會在方法的最後拋出一個異常。
    最後,parseRequest方法設置了HttpRequest的一些屬性:
((HttpRequest) request).setMethod(method);
request.setProtocol(protocol);
if (normalizedUri != null) {
    ((HttpRequest) request).setRequestURI(normalizedUri);
}
else {
    ((HttpRequest) request).setRequestURI(uri);
}
    還有,假如normalize方法的返回值是null的話,方法將會拋出一個異常:
if (normalizedUri == null) {
    throw new ServletException("Invalid URI: " + uri + "'");
}

解析頭部

    一個HTTP頭部是用類HttpHeader來表明的。這個類將會在第4章詳細解釋,而如今知道下面的內容就足夠了:
  • 你能夠經過使用類的無參數構造方法構造一個HttpHeader實例。
  • 一旦你擁有一個HttpHeader實例,你能夠把它傳遞給SocketInputStream的readHeader方法。假如這裏有頭部須要讀取,readHeader方法將會相應的填充HttpHeader對象。假如再也沒有頭部須要讀取了,HttpHeader實例的nameEnd和valueEnd字段將會置零。
  • 爲了獲取頭部的名稱和值,使用下面的方法:
  • String name = new String(header.name, 0, header.nameEnd);
  • String value = new String(header.value, 0, header.valueEnd);
    parseHeaders方法包括一個while循環用於持續的從SocketInputStream中讀取頭部,直到再也沒有頭部出現爲止。循環從構建一個HttpHeader對象開始,並把它傳遞給類SocketInputStream的readHeader方法:
HttpHeader header = new HttpHeader();
// Read the next header
input.readHeader(header);
    而後,你能夠經過檢測HttpHeader實例的nameEnd和valueEnd字段來測試是否能夠從輸入流中讀取下一個頭部信息:
if (header.nameEnd == 0) {
    if (header.valueEnd == 0) {
        return;
    }
    else {
        throw new ServletException             (sm.getString("httpProcessor.parseHeaders.colon"));
    }
}
      假如存在下一個頭部,那麼頭部的名稱和值能夠經過下面方法進行檢索:
String name = new String(header.name, 0, header.nameEnd);
String value = new String(header.value, 0, header.valueEnd);
    一旦你獲取到頭部的名稱和值,你經過調用HttpRequest對象的addHeader方法來把它加入headers這個HashMap中:
request.addHeader(name, value);
    一些頭部也須要某些屬性的設置。例如,當servlet調用javax.servlet.ServletRequest的getContentLength方法的時候,content-length頭部的值將被返回。而包含cookies的cookie頭部將會給添加到cookie集合中。就這樣,下面是其中一些過程:
if (name.equals("cookie")) {
    ... // process cookies here
}
else if (name.equals("content-length")) {
    int n = -1;
    try {
        n = Integer.parseInt (value);
    }
    catch (Exception e) {
        throw new ServletException(sm.getString(
            "httpProcessor.parseHeaders.contentLength"));
    }
    request.setContentLength(n);
}
else if (name.equals("content-type")) {
    request.setContentType(value);
}
    Cookie的解析將會在下一節「解析Cookies」中討論。

解析Cookies

    Cookies是做爲一個Http請求頭部經過瀏覽器來發送的。這樣一個頭部名爲"cookie"而且它的值是一些cookie名/值對。這裏是一個包括兩個cookie:username和password的cookie頭部的例子。
Cookie: userName=budi; password=pwd;
    Cookie的解析是經過類org.apache.catalina.util.RequestUtil的parseCookieHeader方法來處理的。這個方法接受cookie頭部並返回一個javax.servlet.http.Cookie數組。數組內的元素數量和頭部裏邊的cookie名/值對個數是同樣的。parseCookieHeader方法在Listing 3.5中列出。
Listing 3.5: The org.apache.catalina.util.RequestUtil class's parseCookieHeader method
public static Cookie[] parseCookieHeader(String header) {
    if ((header == null) || (header.length 0 < 1) )
        return (new Cookie[0]);
    ArrayList cookies = new ArrayList();
    while (header.length() > 0) {
        int semicolon = header.indexOf(';');
        if (semicolon < 0)
            semicolon = header.length();
        if (semicolon == 0)
            break;
        String token = header.substring(0, semicolon);
        if (semicolon < header.length())
            header = header.substring(semicolon + 1);
        else
            header = "";
        try {
            int equals = token.indexOf('=');
            if (equals > 0) {
                String name = token.substring(0, equals).trim();
                String value = token.substring(equals+1).trim();
                cookies.add(new Cookie(name, value));
            }
        }
        catch (Throwable e) {
            ;
        }
    }
    return ((Cookie[]) cookies.toArray (new Cookie [cookies.size ()]));
}
    還有,這裏是HttpProcessor類的parseHeader方法中用於處理cookie的部分代碼:
else if (header.equals(DefaultHeaders.COOKIE_NAME)) {
    Cookie cookies[] = RequestUtil.ParseCookieHeader (value);
    for (int i = 0; i < cookies.length; i++) {
        if (cookies[i].getName().equals("jsessionid")) {
            // Override anything requested in the URL
            if (!request.isRequestedSessionIdFromCookie()) {
                // Accept only the first session id cookie
                request.setRequestedSessionId(cookies[i].getValue());
                request.setRequestedSessionCookie(true);
                request.setRequestedSessionURL(false);
            }
        }
        request.addCookie(cookies[i]);
    }
}

獲取參數

    你不須要立刻解析查詢字符串或者HTTP請求內容,直到servlet須要經過調用javax.servlet.http.HttpServletRequest的getParameter,
getParameterMap, getParameterNames或者getParameterValues方法來讀取參數。所以,HttpRequest的這四個方法開頭調用了parseParameter方法。
    這些參數只須要解析一次就夠了,由於假如參數在請求內容裏邊被找到的話,參數解析將會使得SocketInputStream到達字節流的尾部。類HttpRequest使用一個布爾變量parsed來指示是否已經解析過了。
    參數能夠在查詢字符串或者請求內容裏邊找到。假如用戶使用GET方法來請求servlet的話,全部的參數將在查詢字符串裏邊出現。假如使用POST方法的話,你也能夠在請求內容中找到一些。全部的名/值對將會存儲在一個HashMap裏邊。Servlet程序員能夠以Map的形式得到參數(經過調用HttpServletRequest的getParameterMap方法)和參數名/值。There is a catch, though. Servlet程序員不被容許修改參數值。所以,將使用一個特殊的HashMap:org.apache.catalina.util.ParameterMap。
    類ParameterMap繼承java.util.HashMap,並使用了一個布爾變量locked。當locked是false的時候,名/值對僅僅能夠添加,更新或者移除。不然,異常IllegalStateException會拋出。而隨時均可以讀取參數值。
類ParameterMap將會在Listing 3.6中列出。它覆蓋了方法用於增長,更新和移除值。那些方法僅僅在locked爲false的時候能夠調用。
Listing 3.6: The org.apache.Catalina.util.ParameterMap class.
package org.apache.catalina.util;
import java.util.HashMap;
import java.util.Map;
public final class ParameterMap extends HashMap {
    public ParameterMap() {
        super ();
    }
    public ParameterMap(int initialCapacity) {
        super(initialCapacity);
    }
    public ParameterMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
    }
    public ParameterMap(Map map) {
        super(map);
    }
    private boolean locked = false;
    public boolean isLocked() {
        return (this.locked);
    }
    public void setLocked(boolean locked) {
        this.locked = locked;
    }
    private static final StringManager sm =
        StringManager.getManager("org.apache.catalina.util");
    public void clear() {
        if (locked)
            throw new IllegalStateException
                (sm.getString("parameterMap.locked"));
        super.clear();
    }
    public Object put(Object key, Object value) {
        if (locked)
            throw new IllegalStateException
                (sm.getString("parameterMap.locked"));
        return (super.put(key, value));
    }
    public void putAll(Map map) {
        if (locked)
            throw new IllegalStateException
                (sm.getString("parameterMap.locked"));
        super.putAll(map);
    }
    public Object remove(Object key) {
        if (locked)
            throw new IllegalStateException
                (sm.getString("parameterMap.locked"));
        return (super.remove(key));
    }
}
    如今,讓咱們來看parseParameters方法是怎麼工做的。
    由於參數能夠存在於查詢字符串或者HTTP請求內容中,因此parseParameters方法會檢查查詢字符串和請求內容。一旦解析事後,參數將會在對象變量parameters中找到,因此方法的開頭會檢查parsed布爾變量,假如已經解析過的話,parsed將會返回true。
if (parsed)
return;
    而後,parseParameters方法建立一個名爲results的ParameterMap變量,並指向parameters。假如
parameters爲null的話,它將建立一個新的ParameterMap。
ParameterMap results = parameters;
if (results == null)
    results = new ParameterMap();
    而後,parseParameters方法打開parameterMap的鎖以便寫值。
results.setLocked(false);
    下一步,parseParameters方法檢查字符編碼,並在字符編碼爲null的時候賦予默認字符編碼。
String encoding = getCharacterEncoding();
if (encoding == null)
    encoding = "ISO-8859-1";
    而後,parseParameters方法嘗試解析查詢字符串。解析參數是使用org.apache.Catalina.util.RequestUtil的parseParameters方法來處理的。
// Parse any parameters specified in the query string
String queryString = getQueryString();
try {
    RequestUtil.parseParameters(results, queryString, encoding);
}
catch (UnsupportedEncodingException e) {
    ;
}
    接下來,方法嘗試查看HTTP請求內容是否包含參數。這種狀況發生在當用戶使用POST方法發送請求的時候,內容長度大於零,而且內容類型是application/x-www-form-urlencoded的時候。因此,這裏是解析請求內容的代碼:
// Parse any parameters specified in the input stream
String contentType = getContentType();
if (contentType == null)
    contentType = "";
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
    contentType = contentType.substring (0, semicolon).trim();
}
else {
    contentType = contentType.trim();
}
if ("POST".equals(getMethod()) && (getContentLength() > 0)
    && "application/x-www-form-urlencoded".equals(contentType)) {
    try {
        int max = getContentLength();
        int len = 0;
        byte buf[] = new byte[getContentLength()];
        ServletInputStream is = getInputStream();
        while (len < max) {
            int next = is.read(buf, len, max - len);
            if (next < 0 ) {
                break;
            }
            len += next;
        }
        is.close();
        if (len < max) {
            throw new RuntimeException("Content length mismatch");
        }
        RequestUtil.parseParameters(results, buf, encoding);
    }
    catch (UnsupportedEncodingException ue) {
        ;
    }
    catch (IOException e) {
        throw new RuntimeException("Content read fail");
    }
}
    最後,parseParameters方法鎖定ParameterMap,設置parsed爲true,並把results賦予parameters。
// Store the final results
results.setLocked(true);
parsed = true;
parameters = results;

建立一個HttpResponse對象

    HttpResponse類實現了javax.servlet.http.HttpServletResponse。跟隨它的是一個叫作HttpResponseFacade的façade類。Figure 3.3顯示了HttpResponse類和它的相關類的UML圖。
    在第2章中,你使用的是一個部分實現的HttpResponse類。例如,它的getWriter方法,在它的其中一個print方法被調用的時候,返回一個不會自動清除的java.io.PrintWriter對象。在本章中應用程序將會修復這個問題。爲了理解它是如何修復的,你須要知道Writer是什麼東西來的。
    在一個servlet裏邊,你使用PrintWriter來寫字節。你可使用任何你但願的編碼,可是這些字節將會以字節流的形式發送到瀏覽器去。所以,第2章中ex02.pyrmont.HttpResponse類的getWriter方法就不奇怪了:
public PrintWriter getWriter() {
// if autoflush is true, println() will flush,
// but print() will not.
// the output argument is an OutputStream
    writer = new PrintWriter(output, true);
    return writer;
}
    請看,咱們是如何構造一個PrintWriter對象的?就是經過傳遞一個java.io.OutputStream實例來實現的。你傳遞給PrintWriter的print或println方法的任何東西都是經過底下的OutputStream進行發送的。
    在本章中,你爲PrintWriter使用ex03.pyrmont.connector.ResponseStream類的一個實例來替代
OutputStream。須要注意的是,類ResponseStream是間接的從類java.io.OutputStream傳遞過去的。
    一樣的你使用了繼承於PrintWriter的類ex03.pyrmont.connector.ResponseWriter。
類ResponseWriter覆蓋了全部的print和println方法,而且讓這些方法的任何調用把輸出自動清除到底下的
OutputStream去。所以,咱們使用一個帶底層ResponseStream對象的ResponseWriter實例。
    咱們能夠經過傳遞一個ResponseStream對象實例來初始化類ResponseWriter。然而,咱們使用一個java.io.OutputStreamWriter對象充當ResponseWriter對象和ResponseStream對象之間的橋樑。
    經過OutputStreamWriter,寫進去的字符經過一種特定的字符集被編碼成字節。這種字符集可使用名字來設定,或者明確給出,或者使用平臺可接受的默認字符集。write方法的每次調用都會致使在給定的字符上編碼轉換器的調用。在寫入底層的輸出流以前,生成的字節都會累積到一個緩衝區中。緩衝區的大小能夠本身設定,可是對大多數場景來講,默認的就足夠大了。注意的是,傳遞給write方法的字符是沒有被緩衝的。
    所以,getWriter方法以下所示:
public PrintWriter getWriter() throws IOException {
    ResponseStream newStream = new ResponseStream(this);
    newStream.setCommit(false);
    OutputStreamWriter osr =
        new OutputStreamWriter(newStream, getCharacterEncoding());
    writer = new ResponseWriter(osr);
    return writer;
}

靜態資源處理器和Servlet處理器

    類ServletProcessor相似於第2章中的類ex02.pyrmont.ServletProcessor。它們都只有一個方法:process。然而ex03.pyrmont.connector.ServletProcessor中的process方法接受一個HttpRequest和
HttpResponse,代替了Requese和Response實例。下面是本章中process的方法簽名:
public void process(HttpRequest request, HttpResponse response) {
    另外,process方法使用HttpRequestFacade和HttpResponseFacade做爲
request和response的facade類。另外,在調用了servlet的service方法以後,它調用了類HttpResponse的
finishResponse方法。
servlet = (Servlet) myClass.newInstance();
HttpRequestFacade requestPacade = new HttpRequestFacade(request);
HttpResponseFacade responseFacade = new HttpResponseFacade(response);
servlet.service(requestFacade, responseFacade);
((HttpResponse) response).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不會看到第二行。
    你也能夠調用ModernServet,在第2章中它不能運行在servlet容器中。下面是相應的URL:
http://localhost:8080/servlet/ModernServlet
     注意:ModernServlet的源代碼在工做目錄的webroot文件夾能夠找到。
    你能夠加上一個查詢字符串到URL中去測試servlet。加入你使用下面的URL來運行ModernServlet的話,將顯示Figure 3.4中的運行結果。
http://localhost:8080/servlet/ModernServlet?userName=tarzan&password=pwd
Figure 3.4: Running ModernServlet

總結

    在本章中,你已經知道了鏈接器是如何工做的。創建起來的鏈接器是Tomcat4的默認鏈接器的簡化版本。正如你所知道的,由於默認鏈接器並不高效,因此已經被棄用了。例如,全部的HTTP請求頭部都被解析了,即便它們沒有在servlet中使用過。所以,默認鏈接器很慢,而且已經被Coyote所代替了。Coyote是一個更快的鏈接器,它的源代碼能夠在Apache軟件基金會的網站中下載。無論怎樣,默認鏈接器做爲一個優秀的學習工具,將會在第4章中詳細討論。

第四章:Tomcat的默認鏈接器

概要

    第3章的鏈接器運行良好,能夠完善以得到更好的性能。可是,它只是做爲一個教育工具,設計來介紹Tomcat4的默認鏈接器用的。理解第3章中的鏈接器是理解Tomcat4的默認鏈接器的關鍵所在。如今,在第4章中將經過剖析Tomcat4的默認鏈接器的代碼,討論須要什麼來建立一個真實的Tomcat鏈接器。
注意:本章中說起的「默認鏈接器」是指Tomcat4的默認鏈接器。即便默認的連機器已經被棄用,被更快的,代號爲Coyote的鏈接器所代替,它仍然是一個很好的學習工具。
    Tomcat鏈接器是一個能夠插入servlet容器的獨立模塊,已經存在至關多的鏈接器了,包括Coyote, mod_jk, mod_jk2和mod_webapp。一個Tomcat鏈接器必須符合如下條件:
1. 必須實現接口org.apache.catalina.Connector。
2. 必須建立請求對象,該請求對象的類必須實現接口org.apache.catalina.Request。
3. 必須建立響應對象,該響應對象的類必須實現接口org.apache.catalina.Response。
    Tomcat4的默認鏈接器相似於第3章的簡單鏈接器。它等待前來的HTTP請求,建立request和response對象,而後把request和response對象傳遞給容器。鏈接器是經過調用接口org.apache.catalina.Container的invoke方法來傳遞request和response對象的。invoke的方法簽名以下所示:
public void invoke(
    org.apache.catalina.Request request,
    org.apache.catalina.Response response);
    在invoke方法裏邊,容器加載servlet,調用它的service方法,管理會話,記錄出錯日誌等等。
    默認鏈接器一樣使用了一些第3章中的鏈接器未使用的優化。首先就是提供一個各類各樣對象的對象池用於避免昂貴對象的建立。接着,在不少地方使用字節數組來代替字符串。
    本章中的應用程序是一個和默認鏈接器管理的簡單容器。然而,本章的焦點不是簡單容器而是默認鏈接器。咱們將會在第5章中討論容器。無論怎樣,爲了展現如何使用默認鏈接器,將會在接近本章末尾的「簡單容器的應用程序」一節中討論簡單容器。
    另外一個須要注意的是默認鏈接器除了提供HTTP0.9和HTTP1.0的支持外,還實現了HTTP1.1的全部新特性。爲了理解HTTP1.1中的新特性,你首先須要理解本章首節解釋的這些新特性。在這以後,咱們將會討論接口
org.apache.catalina.Connector和如何建立請求和響應對象。假如你理解第3章中鏈接器如何工做的話,那麼在理解默認鏈接器的時候你應該不會遇到任何問題。
    本章首先討論HTTP1.1的三個新特性。理解它們是理解默認鏈接器內部工做機制的關鍵所在。而後,介紹全部鏈接器都會實現的接口 org.apache.catalina.Connector。你會發現第3章中遇到的那些類,例如HttpConnector, HttpProcessor等等。不過,這個時候,它們比第3章那些相似的要高級些。

HTTP 1.1新特性

    本節解釋了HTTP1.1的三個新特性。理解它們是理解默認鏈接器如何處理HTTP請求的關鍵。

持久鏈接

    在HTTP1.1以前,不管何時瀏覽器鏈接到一個web服務器,當請求的資源被髮送以後,鏈接就被服務器關閉了。然而,一個互聯網網頁包括其餘資源,例如圖片文件,applet等等。所以,當一個頁面被請求的時候,瀏覽器一樣須要下載頁面所引用到的資源。加入頁面和它所引用到的所有資源使用不一樣鏈接來下載的話,進程將會很是慢。那就是爲何HTTP1.1引入持久鏈接的緣由了。使用持久鏈接的時候,當頁面下載的時候,服務器並不直接關閉鏈接。相反,它等待web客戶端請求頁面所引用的所有資源。這種狀況下,頁面和所引用的資源使用同一個鏈接來下載。考慮創建和解除HTTP鏈接的寶貴操做的話,這就爲 web服務器,客戶端和網絡節省了許多工做和時間。
    持久鏈接是HTTP1.1的默認鏈接方式。一樣,爲了明確這一點,瀏覽器能夠發送一個值爲keep-alive的請求頭部connection:
connection: keep-alive

塊編碼

    創建持續鏈接的結果就是,使用同一個鏈接,服務器能夠從不一樣的資源發送字節流,而客戶端可使用發送多個請求。結果就是,發送方必須爲每一個請求或響應發送內容長度的頭部,以便接收方知道如何解釋這些字節。然而,大部分的狀況是發送方並不知道將要發送多少個字節。例如,在開頭一些字節已經準備好的時候,servlet容器就能夠開始發送響應了,而不會等到全部都準備好。這意味着,在content-length頭部不能提早知道的狀況下,必須有一種方式來告訴接收方如何解釋字節流。
    即便不須要發送多個請求或者響應,服務器或者客戶端也不須要知道將會發送多少數據。在HTTP1.0中,服務器能夠僅僅省略content-length 頭部,並保持寫入鏈接。當寫入完成的時候,它將簡單的關閉鏈接。在這種狀況下,客戶端將會保持讀取狀態,直到獲取到-1,表示已經到達文件的尾部。
    HTTP1.1使用一個特別的頭部transfer-encoding來表示有多少以塊形式的字節流將會被髮送。對每塊來講,在數據以前,長度(十六進制)後面接着CR/LF將被髮送。整個事務經過一個零長度的塊來標識。假設你想用2個塊發送如下38個字節,第一個長度是29,第二個長度是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
    1D,是29的十六進制,指示第一塊由29個字節組成。0\r\n標識這個事務的結束。

狀態100(持續狀態)的使用

    在發送請求內容以前,HTTP 1.1客戶端能夠發送Expect: 100-continue頭部到服務器,並等待服務器的確認。這個通常發生在當客戶端須要發送一份長的請求內容而未能確保服務器願意接受它的時候。若是你發送一份長的請求內容僅僅發現服務器拒絕了它,那將是一種浪費來的。
    當接受到Expect: 100-continue頭部的時候,假如樂意或者能夠處理請求的話,服務器響應100-continue頭部,後邊跟着兩對CRLF字符。
HTTP/1.1 100 Continue
    接着,服務器應該會繼續讀取輸入流。

Connector接口

    Tomcat鏈接器必須實現org.apache.catalina.Connector接口。在這個接口的衆多方法中,最重要的是getContainer,setContainer, createRequest和createResponse。
    setContainer是用來關聯鏈接器和容器用的。getContainer返回關聯的容器。createRequest爲前來的HTTP請求構造一個請求對象,而createResponse建立一個響應對象。
    類org.apache.catalina.connector.http.HttpConnector是Connector接口的一個實現,將會在下一節「HttpConnector類」中討論。如今,仔細看一下Figure 4.1中的默認鏈接器的UML類圖。注意的是,爲了保持圖的簡單化,Request和Response接口的實現被省略了。除了 SimpleContainer類,org.apache.catalina前綴也一樣從類型名中被省略了。
Figure 4.1: The default connector class diagram
    所以,Connector須要被org.apache.catalina.Connector,util.StringManager
org.apache.catalina.util.StringManager等等訪問到。
    一個Connector和Container是一對一的關係。箭頭的方向顯示出Connector知道Container但反過來就不成立了。一樣須要注意的是,不像第3章的是,HttpConnector和HttpProcessor是一對多的關係。

HttpConnector類

    因爲在第3章中org.apache.catalina.connector.http.HttpConnector的簡化版本已經被解釋過了,因此你已經知道這個類是怎樣的了。它實現了org.apache.catalina.Connector (爲了和Catalina協調),
java.lang.Runnable (所以它的實例能夠運行在本身的線程上)和org.apache.catalina.Lifecycle。接口Lifecycle用來維護每一個已經實現它的Catalina組件的生命週期。
    Lifecycle將在第6章中解釋,如今你不須要擔憂它,只要明白經過實現Lifecycle,在你建立HttpConnector實例以後,你應該調用它的initialize和start方法。這兩個方法在組件的生命週期裏必須只調用一次。咱們將看看和第3章的HttpConnector類的那些不一樣方面:HttpConnector如何建立一個服務器套接字,它如何維護一個HttpProcessor對象池,還有它如何處理HTTP請求。

建立一個服務器套接字

    HttpConnector的initialize方法調用open這個私有方法,返回一個java.net.ServerSocket實例,並把它賦予 serverSocket。然而,不是調用java.net.ServerSocket的構造方法,open方法是從一個服務端套接字工廠中得到一個 ServerSocket實例。若是你想知道這工廠的詳細信息,能夠閱讀包org.apache.catalina.net裏邊的接口 ServerSocketFactory和類DefaultServerSocketFactory。它們是很容易理解的。

維護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實例數量是有兩個變量決定的:minProcessors和 maxProcessors。默認狀況下,minProcessors爲5而maxProcessors爲20,可是你能夠經過 setMinProcessors和setMaxProcessors方法來改變他們的值。
protected int minProcessors = 5;
private int maxProcessors = 20;
    開始的時候,HttpConnector對象建立minProcessors個HttpProcessor實例。若是一次有比HtppProcessor 實例更多的請求須要處理時,HttpConnector建立更多的HttpProcessor實例,直到實例數量達到maxProcessors個。在到達這點以後,仍不夠HttpProcessor實例的話,請來的請求將會給忽略掉。若是你想讓HttpConnector繼續建立 HttpProcessor實例的話,把maxProcessors設置爲一個負數。還有就是變量curProcessors保存了 HttpProcessor實例的當前數量。
    下面是類HttpConnector的start方法裏邊關於建立初始數量的HttpProcessor實例的代碼:
while (curProcessors < minProcessors) {
    if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
        break;
    HttpProcessor processor = newProcessor();
    recycle(processor);
}
    newProcessor方法構造一個HttpProcessor對象並增長curProcessors。recycle方法把HttpProcessor隊會棧。
    每一個HttpProcessor實例負責解析HTTP請求行和頭部,並填充請求對象。所以,每一個實例關聯着一個請求對象和響應對象。類 HttpProcessor的構造方法包括了類HttpConnector的createRequest和createResponse方法的調用。

爲HTTP請求服務

    就像第3章同樣,HttpConnector類在它的run方法中有其主要的邏輯。run方法在一個服務端套接字等待HTTP請求的地方存在一個while循環,一直運行直至HttpConnector被關閉了。
while (!stopped) {
    Socket socket = null;
    try {
        socket = serverSocket.accept();
        ...
    對每一個前來的HTTP請求,會經過調用私有方法createProcessor得到一個HttpProcessor實例。
HttpProcessor processor = createProcessor();
    然而,大部分時候createProcessor方法並不建立一個新的HttpProcessor對象。相反,它從池子中獲取一個。若是在棧中已經存在一個HttpProcessor實例,createProcessor將彈出一個。若是棧是空的而且沒有超過HttpProcessor實例的最大數量,createProcessor將會建立一個。然而,若是已經達到最大數量的話,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請求能夠被處理。每一個HttpProcessor實例有本身的線程用於解析,因此這點不是很難作到。你將會在下節「HttpProcessor類」中看到是怎麼作的。

HttpProcessor類

    默認鏈接器中的HttpProcessor類是第3章中有着相似名字的類的全功能版本。你已經學習了它是如何工做的,在本章中,咱們頗有興趣知道 HttpProcessor類怎樣讓assign方法異步化,這樣HttpProcessor實例就能夠同時間爲不少HTTP請求服務了。
    注意: HttpProcessor類的另外一個重要方法是私有方法process,它是用於解析HTTP請求和調用容器的invoke方法的。咱們將會在本章稍後部分的「處理請求」一節中看到它。
    在第3章中,HttpConnector在它自身的線程中運行。可是,在處理下一個請求以前,它必須等待當前處理的HTTP請求結束。下面是第3章中HttpProcessor類的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方法將被調用,有效的啓動了 HttpProcessor實例的處理線程。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循環按照這樣的循序進行:獲取一個套接字,處理它,調用鏈接器的recycle方法把當前的HttpProcessor實例推回棧。這裏是HttpConenctor類的recycle方法:
void recycle(HttpProcessor processor) {
    processors.push(processor);
}
    須要注意的是,run中的while循環在await方法中結束。await方法持有處理線程的控制流,直到從HttpConnector中獲取到一個新的套接字。用另一種說法就是,直到HttpConnector調用HttpProcessor實例的assign方法。可是,await方法和assign方法運行在不一樣的線程上。assign方法從HttpConnector的run方法中調用。咱們就說這個線程是HttpConnector實例的run方法運行的處理線程。assign方法是如何通知已經被調用的await方法的?就是經過一個布爾變量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中總結。
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。這就是說,調用wait方法讓處理器線程暫停,直到鏈接器線程調用HttpProcessor實例的notifyAll方法。
    如今,看看第2列,當一個新的套接字被分配的時候,鏈接器線程調用HttpProcessor的assign方法。available的值是false,因此while循環給跳過,而且套接字給賦值給HttpProcessor實例的socket變量:
this.socket = socket;
    鏈接器線程把available設置爲true並調用notifyAll。這就喚醒了處理器線程,由於available爲true,因此程序控制跳出while循環:把實例的socket賦值給一個本地變量,並把available設置爲false,調用notifyAll,返回最後須要進行處理的socket。
    爲何await須要使用一個本地變量(socket)而不是返回實例的socket變量呢?由於這樣一來,在當前socket被徹底處理以前,實例的socket變量能夠賦給下一個前來的socket。
    爲何await方法須要調用notifyAll呢? 這是爲了防止在available爲true的時候另外一個socket到來。在這種狀況下,鏈接器線程將會在assign方法的while循環中中止,直到接收處處理器線程的notifyAll調用。

請求對象

    默認鏈接器哩變得HTTP請求對象指代org.apache.catalina.Request接口。這個接口被類RequestBase直接實現了,也是HttpRequest的父接口。最終的實現是繼承於HttpRequest的HttpRequestImpl。像第3章同樣,有幾個facade類:RequestFacade和HttpRequestFacade。Request接口和它的實現類的UML圖在Figure 4.2中給出。注意的是,除了屬於javax.servlet和javax.servlet.http包的類,前綴org.apache.catalina已經被省略了。
Figure 4.2: The Request interface and related types
    若是你理解第3章的請求對象,理解這個結構圖你應該不會遇到什麼困難。

響應對象

    Response接口和它的實現類的UML圖在Figure 4.3中給出。
    Figure 4.3: The Response interface and its implementation classes

處理請求

    到這個時候,你已經理解了請求和響應對象,而且知道HttpConnector對象是如何建立它們的。如今是這個過程的最後一點東西了。在這節中咱們關注HttpProcessor類的process方法,它是一個套接字賦給它以後,在HttpProcessor類的run方法中調用的。process方法會作下面這些工做:
  • 解析鏈接
  • 解析請求
  • 解析頭部
    在解釋完process方法以後,在本節的各個小節中將討論每一個操做。
    process方法使用布爾變量ok來指代在處理過程當中是否發現錯誤,並使用布爾變量finishResponse來指代Response接口中的finishResponse方法是否應該被調用。
boolean ok = true;
boolean finishResponse = true;
    另外,process方法也使用了布爾變量keepAlive,stopped和http11。keepAlive表示鏈接是不是持久的,stopped表示HttpProcessor實例是否已經被鏈接器終止來確認process是否也應該中止,http11表示 從web客戶端過來的HTTP請求是否支持HTTP 1.1。
    像第3章那樣,有一個SocketInputStream實例用來包裝套接字的輸入流。注意的是,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); //logging is discussed in Chapter 7
    ok = false;
}
    接着,process方法經過調用parseConnection,parseRequest和parseHeaders方法開始解析前來的HTTP請求,這些方法將在這節的小節中討論。
try {
    if (ok) {
        parseConnection(socket);
        parseRequest(input, output);
        if (!request.getRequest().getProtocol().startsWith("HTTP/0"))
            parseHeaders(input);
    parseConnection方法得到協議的值,像HTTP0.9, HTTP1.0或HTTP1.1。若是協議是HTTP1.0,keepAlive設置爲false,由於HTTP1.0不支持持久鏈接。若是在HTTP請求裏邊找到Expect: 100-continue的頭部信息,則parseHeaders方法將把sendAck設置爲true。
    若是協議是HTTP1.1,而且web客戶端發送頭部Expect: 100-continue的話,經過調用ackRequest方法它將響應這個頭部。它將會測試組塊是不是容許的。
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的值,並在sendAck爲true的時候發送下面的字符串:
HTTP/1.1 100 Continue\r\n\r\n
    在解析HTTP請求的過程當中,有可能會拋出異常。任何異常將會把ok或者finishResponse設置爲false。在解析事後,process方法把請求和響應對象傳遞給容器的invoke方法:
try {
    ((HttpServletResponse) response).setHeader("Date", FastHttpDateFormat.getCurrentDate());
    if (ok) {
        connector.getContainer().invoke(request, response);
    }
}
    接着,若是finishResponse仍然是true,響應對象的finishResponse方法和請求對象的finishRequest方法將被調用,而且結束輸出。
if (finishResponse) {
    ...
    response.finishResponse();
    ...
    request.finishRequest();
    ...
    output.flush();
    while循環的最後一部分檢查響應的Connection頭部是否已經在servlet內部設爲close,或者協議是HTTP1.0.若是是這種狀況的話,keepAlive設置爲false。一樣,請求和響應對象接着會被回收利用。
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的話,while循環將會在開頭就啓動。由於在前面的解析過程當中和容器的invoke方法中沒有出現錯誤,或者HttpProcessor實例沒有被中止。不然,shutdownInput方法將會調用,而套接字將被關閉。
try {
    shutdownInput(input);
    socket.close();
}
...
    shutdownInput方法檢查是否有未讀取的字節。若是有的話,跳過那些字節。

解析鏈接

    parseConnection方法從套接字中獲取到網絡地址並把它賦予HttpRequestImpl對象。它也檢查是否使用代理並把套接字賦予請求對象。parseConnection方法在Listing4.2中列出。
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請求頭部:
standard HTTP request headers in character arrays:
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方法來獲取一個空的HttpHead實例。這個實例被傳遞給
SocketInputStream的readHeader方法。
HttpHeader header = request.allocateHeader();
// Read the next header
input.readHeader(header);
    假如全部的頭部都被已經被讀取的話,readHeader方法將不會賦值給HttpHeader實例,這個時候parseHeaders方法將會返回。
if (header.nameEnd == 0) {
    if (header.valueEnd == 0) {
        return;
    }
    else {
        throw new         ServletException(sm.getString("httpProcessor.parseHeaders.colon"));
    }
}
    若是存在一個頭部的名稱的話,這裏必須一樣會有一個頭部的值:
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,因此你不能請求一個靜態頁面。
    類SimpleContainer展現在Listing 4.3.
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類,並調用它的service方法。這個方法和第3章的ServletProcessor類在哦個的process方法很是相似。
    Bootstrap類在Listing 4.4在列出.
    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實例和一個 SimpleContainer實例。它接下去調用conncetor的setContainer方法傳遞container,讓connector和container關聯起來。下一步,它調用connector的initialize和start方法。這將會使得connector爲處理8080端口上的任何請求作好了準備。
    你能夠經過在控制檯中輸入一個按鍵來終止這個應用程序。

運行應用程序

    要在Windows中運行這個程序的話,在工做目錄下輸入如下內容: html

java -classpath ./lib/servlet.jar;./ ex04.pyrmont.startup.Bootstrap
    在Linux的話,你可使用分號來分隔兩個庫。
java -classpath ./lib/servlet.jar:./ ex04.pyrmont.startup.Bootstrap
    你能夠和第三章那樣調用PrimitiveServlet和ModernServlet。
    注意的是你不能請求index.html,由於沒有靜態資源的處理器。

總結

    本章展現瞭如何構建一個能和Catalina工做的Tomcat鏈接器。剖析了Tomcat4的默認鏈接器的代碼並用這個鏈接器構建了一個小應用程序。接下來的章節的全部應用程序都會使用默認鏈接器。
相關文章
相關標籤/搜索