1. 程式人生 > >實戰WEB 伺服器(JAVA編寫WEB伺服器)

實戰WEB 伺服器(JAVA編寫WEB伺服器)

  一、超文字傳輸協議

    1.1 HTTP請求

    1.2 HTTP應答

  二、Socket類

  三、ServerSocket類

  四、Web伺服器例項

    4.1 HttpServer類

    4.2 Request類

    4.3 Response類

  五、編譯和執行



  ===================

  正文:

  ===================

  Web伺服器與客戶端的通訊使用HTTP協議(超文字傳輸協議),所以也叫做HTTP伺服器。用Java構造Web伺服器主要用二個類,java.net.Socket和java.net.ServerSocket,來實現HTTP通訊。因此,本文首先要討論的是HTTP協議和這兩個類,在此基礎上實現一個簡單但完整的Web伺服器。

  一、超文字傳輸協議



  Web伺服器和瀏覽器通過HTTP協議在Internet上傳送和接收訊息。HTTP協議是一種請求-應答式的協議——客戶端傳送一個請求,伺服器返回該請求的應答。HTTP協議使用可靠的TCP連線,預設埠是80。HTTP的第一個版本是HTTP/0.9,後來發展到了HTTP/1.0,現在最新的版本是HTTP/1.1。HTTP/1.1由 RFC 2616 定義(pdf格式)。

  本文只簡要介紹HTTP 1.1的相關知識,但應該足以讓你理解Web伺服器和瀏覽器傳送的訊息。如果你要了解更多的細節,請參考RFC 2616。

  在HTTP中,客戶端/伺服器之間的會話總是由客戶端通過建立連線和傳送HTTP請求的方式初始化,伺服器不會主動聯絡客戶端或要求與客戶端建立連線。瀏覽器和伺服器都可以隨時中斷連線,例如,在瀏覽網頁時你可以隨時點選“停止”按鈕中斷當前的檔案下載過程,關閉與Web伺服器的HTTP連線。

  1.1 HTTP請求


  HTTP請求由三個部分構成,分別是:方法-URI-協議/版本,請求頭,請求正文。下面是一個HTTP請求的例子:

GET /servlet/default.jsp HTTP/1.1
Accept: text/plain; text/html 
Accept-Language: en-gb 
Connection: Keep-Alive 
Host: localhost 
Referer: http://localhost/ch8/SendDetails.htm 
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 

userName=JavaJava&userID=javaID



  請求的第一行是“方法-URI-協議/版本”,其中GET就是請求方法,/servlet/default.jsp表示URI,HTTP/1.1是協議和協議的版本。根據HTTP標準,HTTP請求可以使用多種請求方法。例如,HTTP 1.1支援七種請求方法:GET,POST,HEAD,OPTIONS,PUT,DELETE,和TRACE。在Internet應用中,最常用的請求方法是GET和POST。

  URI完整地指定了要訪問的網路資源,通常認為它相對於伺服器的根目錄而言,因此總是以“/”開頭。URL實際上是URI 一種型別。最後,協議版本聲明瞭通訊過程中使用的HTTP協議的版本。

  請求頭包含許多有關客戶端環境和請求正文的有用資訊。例如,請求頭可以宣告瀏覽器所用的語言,請求正文的長度,等等,它們之間用一個回車換行符號(CRLF)分隔。

  請求頭和請求正文之間是一個空行(只有CRLF符號的行),這個行非常重要,它表示請求頭已經結束,接下來的是請求的正文。一些介紹Internet程式設計的書籍把這個CRLF視為HTTP請求的第四個組成部分。

  在前面的HTTP請求中,請求的正文只有一行內容。當然,在實際應用中,HTTP請求正文可以包含更多的內容。

  1.2 HTTP應答

  和HTTP請求相似,HTTP應答也由三個部分構成,分別是:協議-狀態程式碼-描述,應答頭,應答正文。下面是一個HTTP應答的例子:

HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Date: Mon, 3 Jan 1998 13:13:33 GMT
Content-Type: text/html
Last-Modified: Mon, 11 Jan 1998 13:23:42 GMT
Content-Length: 112

<html>
<head>
<title>HTTP應答示例</title></head><body>
Hello HTTP!
</body>
</html>



  HTTP應答的第一行類似於HTTP請求的第一行,它表示通訊所用的協議是HTTP 1.1,伺服器已經成功地處理了客戶端發出的請求(200表示成功),一切順利。

  應答頭也和請求頭一樣包含許多有用的資訊,例如伺服器型別、日期時間、內容型別和長度等。應答的正文就是伺服器返回的HTML頁面。應答頭和正文之間也用CRLF分隔。

  二、Socket類

  Socket代表著網路連線的一個端點,應用程式通過該端點向網路傳送或從網路讀取資料。位於兩臺不同機器上的應用軟體通過網路連線傳送和接收位元組流,從而實現通訊。要把訊息傳送給另一個應用,首先要知道對方的IP地址以及其通訊端點的埠號。在Java中,通訊端點由java.net.Socket類表示。

  Socket類有許多建構函式,其中一個建構函式的引數是主機名稱和埠號:

public Socket(String host, int port)



  host是遠端機器的名字或IP地址,port是遠端應用的埠號。例如,如果要連線到yahoo.com的80埠,我們可以用“new Socket("yahoo.com", 80);”語句構造一個Socket。

  成功建立了Socket類的例項之後,我們就可以用它來發送和接收位元組流形式的資料。要傳送位元組流,首先要呼叫Socket類的getOutputStream方法獲得一個java.io.OutputStream物件;為了向遠端應用傳送文字資料,我們經常要從返回的OutputStream物件構造一個java.io.PrintWriter物件。要從連線的另一端接收位元組流,首先要呼叫Socket類的getInputStream方法獲得一個java.io.InputStream物件。

  例如,下面的程式碼片斷建立一個與本地HTTP伺服器(127.0.0.1代表本地主機的IP地址)通訊的Socket,傳送一個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() ));

// 向Web伺服器傳送一個HTTP請求
out.println("GET /index.jsp HTTP/1.1");
out.println("Host: localhost:8080");
out.println("Connection: Close");
out.println();

// 讀取伺服器的應答
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);
}

// 把應答顯示到控制檯
System.out.println(sb.toString());
socket.close();



  注意,為了保證Web伺服器能夠返回正確的應答,客戶端傳送的HTTP請求應該遵從雙方約定的HTTP協議版本。

  三、ServerSocket類

  Socket類代表的是“客戶”通訊端點,它是一個連線遠端伺服器應用時臨時建立的端點。對於伺服器應用,例如HTTP伺服器或FTP伺服器,我們需要另一種端點,因為我們不知道客戶端應用什麼時候會試圖連線伺服器,伺服器必須一直處於等待連線的狀態。

  因此,對於伺服器端的通訊端點,我們要使用java.net.ServerSocker類。ServerSocket等待來自客戶端的連線請求;一旦接收到請求,ServerSocket建立一個Socket例項來處理與該客戶端的通訊。

  ServerSocket提供了四個建構函式。建立ServerSocket的例項時,我們必須指定監聽客戶端訊息的IP地址(稱為“繫結地址”,Binding Address)和埠。通常情況下,這個IP地址總是127.0.0.1,也就是說伺服器端點將在本地機器上監聽。伺服器端點的另一個重要屬性是它的backlog值,這是儲存客戶端連線請求的最大佇列長度,一旦超越這個長度,伺服器端點開始拒絕客戶端的連線請求。

  下面是ServerSocket類建構函式的其中一種形式:

public ServerSocket(int port, int backLog, InetAddress bindingAddress);



  這個建構函式要求繫結地址必須是一個java.net.InetAddress的例項。要構造一個InetAddress物件,一種簡單的辦法是呼叫它的靜態getByName方法,傳入一個表示主機名稱/地址的String。例如,下面的程式碼構造了一個在本地機器的8080埠監聽的ServerSocket,它的backlog值是1:

new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));



  建立好ServerSocket例項之後,呼叫它的accept方法,要求它等待傳入的連線請求。只有出現了連線請求時,accept方法才會返回,它的返回值是一個Socket類的例項。隨後,這個Socket物件就可以用來與客戶端應用通訊。

  四、Web伺服器例項

  本文的Web伺服器由三個類構成,分別是:HttpServer,Request ,Response。

  應用的入口點(static main方法)在HttpServer類。main方法建立一個HttpServer例項,然後呼叫await方法。從await方法的名字也可以看出,它的功能是在指定的埠上等待HTTP請求,然後處理請求,把處理的結果返回給客戶端。除非收到了關閉伺服器的命令,否則await將一直保持等待客戶端請求的狀態。(之所以用await而不是wait作為方法名,是因為wait是System.Object類中一個用來操作執行緒的重要方法)。

  本文的Web伺服器只能傳送指定目錄下的靜態資源,例如HTML和圖形檔案。它不支援頭資訊(例如日期時間、Cookie等)。

  4.1 HttpServer類

  HttpServer類代表一個Web伺服器,提供由WEB_ROOT變數指定的目錄及其子目錄下的靜態資源。WEB_ROOT用下面的語句初始化:

public static final String WEB_ROOT =
    System.getProperty("user.dir") + File.separator  + "webroot";



  本文最後的下載程式碼包中有一個webroot目錄,它裡面有一些靜態Web頁面,可用來測試本文的伺服器。要開啟webroot目錄下的靜態頁面,在瀏覽器的位址列輸入URL:http://machineName:port/staticResource。

  如果執行Web伺服器的機器和瀏覽器所在的機器不同,machineName必須是Web伺服器所在機器的IP地址或名稱;如果瀏覽器和Web伺服器在同一臺機器上執行,machineName也可以是localhost。port是8080,staticResource是要請求的資源(頁面)檔名稱。

  例如,假設我們在同一臺機器上執行Web伺服器和瀏覽器,如果要求HttpServer返回index.html檔案,則URL是:

http://localhost:8080/index.html



  要關閉Web伺服器,在瀏覽器的位址列輸入一個預定義的關閉命令,即在URL的“主機名稱:埠”之後,加上SHUTDOWN_COMMAND變數定義的字元。假設SHUTDOWN_COMMAND變數的值是“/SHUTDOWN”,我們可以在瀏覽器位址列輸入“http://localhost:8080/SHUTDOWN”關閉Web伺服器。

  下面我們來看看await方法的程式碼,程式碼的說明隨後給出。

【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);
    }

    // 迴圈,等待客戶端發來的請求
    while (!shutdown) {
        Socket socket = null;
        InputStream input = null;
        OutputStream output = null;
        try {
            socket = serverSocket.accept();
            input = socket.getInputStream();
            output = socket.getOutputStream();
            // 建立Request物件並予以解析
            Request request = new Request(input);
            request.parse();
            // 建立Response物件
            Response response = new Response(output);
            response.setRequest(request);
            response.sendStaticResource();
            // 關閉Socket
            socket.close();
            // 檢查該URI是否為關閉伺服器的命令
            shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
        }
        catch (Exception e) {
            e.printStackTrace();
            continue;
        }
    }
}



  await方法首先建立一個ServerSocket例項,然後進入while迴圈等待來自客戶端的請求。while迴圈裡面的程式碼會在執行ServerSocket的accept方法時等待,直到8080埠收到一個HTTP請求。然後,從accept返回的Socket獲得一個java.io.InputStream和一個java.io.OutputStream。接下來,await方法建立一個Request物件,呼叫parse方法解析原始的HTTP請求。接著,await方法又建立一個Response物件,把前面建立的Request物件傳遞給它,呼叫它的sendStaticRessource方法。

  最後,await方法關閉Socket,呼叫Request方法的getUri方法,查檢該HTTP請求的URI是否為一個關閉伺服器的命令。如果是,把shutdown變數的值設定為true,while迴圈結束。

  4.2 Request類

  Request類代表一個HTTP請求。建立Request類的例項時要傳入一個從負責與客戶端通訊的Socket獲得的InputStream物件。呼叫InputStream物件的其中一個read方法可獲得HTTP請求的原始資料。

  Request類有兩個公用方法parse和getUri。parse方法解析HTTP請求中的原始資料,其實它的功能並不多——它唯一提取的資訊是HTTP請求的URI,通過呼叫私有的parseUri方法獲得。parseUri把Uri儲存在uri變數中。呼叫公用的getUri方法可返回HTTP請求的URI。

  要理解parse和parseUri的工作原理,首先要理解HTTP請求的結構,參見本文前面內容以及RFC 2616。如前所述,HTTP請求包含三個部分,現在我們感興趣的是第一部分,即所謂的“請求行”,包括請求方法、URI和協議版本,最後是一個CRLF字元。請求行裡面的各個部分由空格分隔,例如,用GET方法請求index.html檔案的請求行是:

GET /index.html HTTP/1.1



  parse方法讀取傳遞給Request物件的InputStream的整個位元組流,把位元組資料儲存到緩衝區,然後利用buffer位元組陣列中的內容填寫一個稱為request的StringBuffer物件,把該StringBuffer的String描述傳遞給parseUri方法。parse方法的程式碼如下所示:

【Request類的parse方法】

public void parse() {
    // 從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());
}



  parseUri從請求行獲得URI,下面給出了parseUri方法的程式碼。parseUri方法搜尋請求中的第一、二兩個空格字元,提取出URI。

【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;
}



  4.3 Response類

  Response類代表一個HTTP應答。它的建構函式要求指定一個OutputStream物件,例如:

public Response(OutputStream output) {
    this.output = output;
}



  Response類有兩個公用方法:setRequest和sendStaticResource。setRequest方法用來把Request物件傳遞給Response物件,很簡單,如下所示:

【Response類的setRequest方法】

public void setRequest(Request request) {
    this.request = request;
}



  sendStaticResource方法用來發送靜態資源,如HTML檔案等。它的實現如下所示:

【Response類的sendStaticResource方法】

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 {
            // 找不到檔案
            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) {
        // 如不能例項化File物件,丟擲異常。
        System.out.println(e.toString() );
    }
    finally {
        if (fis != null)
            fis.close();
    }
}



  sendStaticResource方法首先建立一個java.io.File類的例項,在呼叫File類建構函式時指定了Web伺服器的根目錄和請求的目標URI。然後,sendStaticResource 檢查使用者請求的檔案是否存在,如存在,它在該File物件的基礎上建立一個java.io.FileInputStream物件,然後呼叫FileInputStream的read方法,把讀取的位元組陣列寫入到OutputStream輸出。如果使用者請求的檔案不存在,sendStaticResource方法向瀏覽器傳送一個錯誤資訊。

  五、編譯和執行

  下載本文後面提供的zip檔案,解開壓縮。解開壓縮時你指定的目標目錄稱為“工作目錄”。工作目錄下有二個子目錄:src,webroot。webroot目錄下包含一些示例頁面。在工作目錄下執行下面的命令編譯Web伺服器:

javac -d . src/*.java



  “-d .”選項表示把編譯結果儲存到當前目錄(即工作目錄),而不是儲存到src目錄。執行java HttpServer就可以啟動Web伺服器。

  假設瀏覽器和Web伺服器執行在同一臺機器上,開啟瀏覽器,輸入URL:http://localhost:8080/index.html。瀏覽器顯示出圖一所示的頁面。


  結束語:本文通過開發一個簡單的JavaWeb伺服器,介紹了Web伺服器的基本工作原理。雖然本文開發的Web伺服器不具備複雜的功能,但它足以作為一個不錯的學習工具。


完整程式碼如下: