WebServer

Programming Assignment 1: Building a Multi-Threaded Web Server

一、什么是Web服務(wù)器

網(wǎng)頁服務(wù)器(Web server)一詞有兩個(gè)意思:一臺負(fù)責(zé)提供網(wǎng)頁的電腦,主要是各種編程語言構(gòu)建而成,通過HTTP協(xié)議傳給客戶端(一般是指網(wǎng)頁瀏覽器)。一個(gè)提供網(wǎng)頁的服務(wù)器程序。
  雖然每個(gè)網(wǎng)頁服務(wù)器程序有很多不同,但有一些共同的特點(diǎn):每一個(gè)網(wǎng)頁服務(wù)器程序都需要從網(wǎng)絡(luò)接受HTTP request,然后提供HTTP response給請求者。HTTP回復(fù)一般包含一個(gè)HTML文件,有時(shí)也可以包含純文本文件、圖像或其他類型的文件。

二、HTTP協(xié)議

簡介
本試驗(yàn)中我們將通過兩個(gè)階段來開發(fā)一個(gè)web服務(wù)器,最后完成一個(gè)能夠并行服務(wù)與多個(gè)請求的多線程Web服務(wù)器。
我們將實(shí)現(xiàn)在RFC 1945定義的HTTP1.0。根據(jù)定義,每個(gè)Web page中的對象將通過單獨(dú)的HTTP消息來獲取。所實(shí)現(xiàn)Web服務(wù)器將能夠并發(fā)地服務(wù)于多個(gè)請求,這意味著Web服務(wù)器是多線程的。 Web服務(wù)器的主線程負(fù)責(zé)偵聽某個(gè)端口,當(dāng)收到TCP連接請求時(shí),將創(chuàng)建一個(gè)新的socket負(fù)責(zé)與該TCP連接,并創(chuàng)建新的線程具體負(fù)責(zé)通過該連接的消息傳遞。為了簡化程序設(shè)計(jì)任務(wù),我們分兩階段來設(shè)計(jì)Web服務(wù)器。

第一階段:編寫僅僅顯示所收到HTTP Request消息所有頭部行的一個(gè)多線程Web服務(wù)器。當(dāng)該程序運(yùn)行正確后,將添加適當(dāng)?shù)拇a以實(shí)現(xiàn)對Request消息的適當(dāng)響應(yīng)。

開發(fā)Web服務(wù)器時(shí),可以通過Web瀏覽器來測試它。不過,所編寫的Web服務(wù)器通常并不工作于80端口,因此,測試時(shí)在瀏覽器的地址欄中需要指定Web服務(wù)器的工作端口。例如:假設(shè)Web服務(wù)器運(yùn)行在域名為host.someschool.edu的主機(jī)上,監(jiān)聽端口6789,我們想獲取文件index.html。需要在瀏覽器的地址欄中輸入如下的URL:
http://host.someschool.edu:6789/index.html
如果忽略了 ":6789", 瀏覽器則默認(rèn)地認(rèn)為Web服務(wù)器監(jiān)聽80端口。
當(dāng)Web服務(wù)器遇到問題,將向?yàn)g覽器發(fā)送包含適當(dāng)響應(yīng)消息的HTML頁面,以便在瀏覽器中顯示錯(cuò)誤信息。

Web Server in Java: Part A

下面,我們將實(shí)現(xiàn)第一階段的編程任務(wù)。當(dāng)看到"?"時(shí),你需要在該處添加相應(yīng)的代碼。
我們的第一個(gè)Web服務(wù)器將是多線程的,所收到的每個(gè)Request消息將交由單獨(dú)的線程進(jìn)行處理。這使得服務(wù)器可以并發(fā)地為多個(gè)客戶服務(wù), 或者是并發(fā)地服務(wù)于一個(gè)客戶的多個(gè)請求.當(dāng)創(chuàng)建一個(gè)新線程時(shí),需要向線程的構(gòu)造函數(shù)傳遞實(shí)現(xiàn)了Runnable 接口的類的一個(gè)實(shí)例(即通過實(shí)現(xiàn)接口Runnable來實(shí)現(xiàn)多線程)。這正是我們定義單獨(dú)的類HttpRequest的原因。Web服務(wù)器的結(jié)構(gòu)如下:


import java.io.* ;
import java.net.* ;
import java.util.* ;
 
public final class WebServer
{
        public static void main(String argv[]) throws Exception
        {
               . . .
        }
}
 
final class HttpRequest implements Runnable
{
        . . .
}

通常,Web服務(wù)器為通過周知(well known)端口80收到的請求提供服務(wù)。可以選擇大于1024的任意端口作為Web服務(wù)器的監(jiān)聽端口,但需要記著在瀏覽器地址欄中輸入U(xiǎn)RL時(shí)指定Web服務(wù)器的動(dòng)作端口。


public static void main(String argv[]) throws Exception
{
        // Set the port number.
        int port = 6789;
 
        . . .
}

下面,創(chuàng)建監(jiān)聽端口以等待TCP連接請求。由于Web服務(wù)器將不間斷地提供服務(wù),我們將偵聽操作放在一個(gè)無窮循環(huán)的循環(huán)體中。這意味著需要通過在鍵盤上輸入^C來結(jié)束Web服務(wù)器的運(yùn)行。


// Establish the listen socket.
       ?
 
// Process HTTP service requests in an infinite loop.
while (true) {
        // Listen for a TCP connection request.
        ?
 
        . . .
}

當(dāng)收到請求后,我們創(chuàng)建一個(gè)HttpRequest 對象,將標(biāo)征著所建立TCP連接的Socket作為參數(shù)傳遞到它的構(gòu)造函數(shù)中。


// Construct an object to process the HTTP request message.
HttpRequest request = new HttpRequest( ? );
 
// Create a new thread to process the request.
Thread thread = new Thread(request);
 
// Start the thread.
thread.start();

為了讓HttpRequest對象在一個(gè)單獨(dú)的線程中處理隨后的HTTP請求,我們首先創(chuàng)建一個(gè)Thread對象,將HttpRequest對象作為參數(shù)傳遞給Thread的構(gòu)造函數(shù),然后調(diào)用Thread的start()方法啟動(dòng)線程。
當(dāng)一個(gè)Thread創(chuàng)建并啟動(dòng)后,主線程回到了循環(huán)體的首部。主線程將被阻塞(block)在accept處等待另一個(gè)TCP 連接請求的到達(dá)。此時(shí),剛剛創(chuàng)建的線程正在運(yùn)行。當(dāng)另一個(gè)TCP連接請求到達(dá)時(shí),主線程將不管前面創(chuàng)建的線程是否結(jié)束,重復(fù)上面的操作,創(chuàng)建新線程負(fù)責(zé)新連接的請求處理。
到這為止,主線程的工作就完成了,后面我們將集中精力設(shè)計(jì)類 HttpRequest。
我們聲明HttpRequest類中的兩個(gè)變量: CRLF and socket。根據(jù)HTTP規(guī)范, 我們需要用”回車換行”作為Response消息頭部行的結(jié)束。因此,為了使用方便,我們定義了一個(gè)CRLR字符串變量。變量socket用作connection socket, 它將被類HttpRequest的構(gòu)造函數(shù)初始化。

final class HttpRequest implements Runnable
{
        final static String CRLF = "\r\n";
        Socket socket;
 
        // Constructor
        public HttpRequest(Socket socket) throws Exception 
        {
               this.socket = socket;
        }
 
        // Implement the run() method of the Runnable interface.
        public void run()
        {
               . . .
        }
 
        private void processRequest() throws Exception
        {
               . . .
        }
}

為了將類HttpRequest的實(shí)例作為參數(shù)傳輸傳遞到Thread的構(gòu)造函數(shù)中,HttpRequest必須實(shí)現(xiàn)Runnable接口。因此,必須定義HttpRequest的public方法run(),其返回值類型為void。我們在run()中調(diào)用實(shí)現(xiàn)Request消息處理絕大部分操作的方法 processRequest()。
直到現(xiàn)在,我們其實(shí)一直在拋出異常, 而不是catching他們。不過,我們不能從方法run()中拋出異常,因?yàn)槲覀儽仨殗?yán)格遵守Runnable接口對run()的聲明。Runnable接口的run()方法不拋出任何異常。我們將在processRequest中放置處理代碼,并從此在run方法中利用try/catch塊處理異常。
// Implement the run() method of the Runnable interface.
public void run()
{
try {
processRequest();
} catch (Exception e) {
System.out.println(e);
}
}
現(xiàn)在,設(shè)計(jì)processRequest()中的代碼。首先獲得socket的輸入/出流的reference引用。然后,我們給input stream包裝過濾器(filters)。但是,輸出流無須包裝任何過濾器,主要原因是我們將向輸出流直接寫入bytes。

private void processRequest() throws Exception
{
        // Get a reference to the socket's input and output streams.
        InputStream is = ?;
        DataOutputStream os = ?;
 
        // Set up input stream filters.
        ? 
        BufferedReader br = ?;
 
        . . .
}

現(xiàn)在我們已經(jīng)準(zhǔn)備好來獲得客戶發(fā)來的HTTP Request消息了(通過從socket的輸入流讀取消息)。類BufferedReader的方法readLine()方法將從輸入流中讀取字符,直到遇到CRLF為止(也就是從input stream中讀取一行,行的結(jié)束符為CRLF)。
從input stream中讀出的第一行為HTTP Request消息的請求行 (參看教材2.2,了解請求行的定義)。

// Get the request line of the HTTP request message.
String requestLine = ?;
 
// Display the request line.
System.out.println();
System.out.println(requestLine);

讀取消息的請求行后,讀取消息的其它頭部行。由于我們并不知道客戶發(fā)送消息中有多少頭部行,必須利用一個(gè)循環(huán)操作來獲取Request消息的所有頭部行。

// Get and display the header lines.
String headerLine = null;
while ((headerLine = br.readLine()).length() != 0) {
        System.out.println(headerLine);
}

由于除了需要將頭部行中的內(nèi)容顯示在屏幕上外,現(xiàn)階段無須針對頭部行做其它的處理,我們僅僅利用臨時(shí)變量headerLine來保存頭部行的信息。循環(huán)操作直到下面的表達(dá)式值等于0時(shí)停止。也就是讀取的 頭部行的長度如果為零,表示讀出了一個(gè)空行,意味著所有的頭部行已經(jīng)全部讀出(參看教材的2.2 部分,頭部行和entity body之間利用一個(gè)空行作為分割)。

(headerLine = br.readLine()).length()

后面我們將添加分析客戶Request消息的代碼,并發(fā)送Response消息 。在進(jìn)行后面的程序設(shè)計(jì)前,我們先完成第一階段的任務(wù),并通過瀏覽器來測試它。添加如下代碼以關(guān)閉輸入/出流和connection socket。

// Close streams and socket.
os.close();
br.close();
socket.close();

當(dāng)程序編譯成功后,以適當(dāng)?shù)亩丝谧鳛閰?shù)運(yùn)行Web服務(wù)器,并利用瀏覽器訪問它。在瀏覽器地址欄中輸入下面的示例:
http://host.someschool.edu:6789/
Web服務(wù)器將顯示HTTP Request消息的內(nèi)容。檢查請求消息的格式是否與教材2.2中描述的HTTP Request消息格式相符。

Web Server in Java: Part B

Web服務(wù)器不能僅僅顯示收到的Request消息的內(nèi)容,而是應(yīng)該分析收到的Request消息并產(chǎn)生適當(dāng)?shù)腞esponse消息。我們將忽略Request消息頭部行中包含的信息,僅僅關(guān)注Request消息的請求行中包含的文件名字。我們將假設(shè)客戶發(fā)送的Request消息中的Request行總是使用GET方法,實(shí)際上,一個(gè)瀏覽器可能使用GET、POST和HEAD方法(HTTP1.0)
利用類StringTokenizer從Request行中解析出文件名字。

首先,創(chuàng)建一個(gè) StringTokenizer對象來容納Request行;

第二步:跳過Method字段(因?yàn)榭偸荊ET方法);

第三步,解析出文件名字。

// Extract the filename from the request line.
StringTokenizer tokens = new StringTokenizer(requestLine);
tokens.nextToken();  // skip over the method, which should be "GET"
String fileName = tokens.nextToken();
 
// Prepend a "." so that file request is within the current directory.
fileName = "." + fileName;

由于瀏覽器在文件名字前加了一個(gè)“/“,我們在它前面加上一個(gè)字符 ”.”,從而限定從當(dāng)前目錄開始獲取文件。
現(xiàn)在有了客戶請求的文件名字,我們可以打開該文件作為向客戶發(fā)送該文件的第一步。如果文件不存在,構(gòu)造函數(shù) FileInputStream() 將拋出異常FileNotFoundException,為了在拋出此可能的異常后不終止線程的執(zhí)行,利用一個(gè)try/catch塊將布爾型變量fileExists設(shè)置為false。后面我們將使用該變量來構(gòu)建一個(gè)錯(cuò)誤響應(yīng)消息,而不是發(fā)送一個(gè)根本不存在的文件。

// Open the requested file.
FileInputStream fis = null;
boolean fileExists = true;
try {
        fis = new FileInputStream(fileName);
} catch (FileNotFoundException e) {
        fileExists = false;
}

Response消息有三部分: the status line, the response headers, 和entity body。狀態(tài)行、頭部行以CRLF作為結(jié)束。利用變量statusLine 來保存響應(yīng)消息的statusline、contentTypeLine保存Content-Type頭部行信息。當(dāng)文件不存在時(shí),Web服務(wù)器將返回狀態(tài)行為“404 Not Found“,entity body中保存利用HTML創(chuàng)建的錯(cuò)誤消息。

// Construct the response message.
String statusLine = null;
String contentTypeLine = null;
String entityBody = null;
if (fileExists) {
        statusLine = ?;
        contentTypeLine = "Content-type: " + 
               contentType( fileName ) + CRLF;
} else {
        statusLine = ?;
        contentTypeLine = ?;
        entityBody = "<HTML>" + 
               "<HEAD><TITLE>Not Found</TITLE></HEAD>" +
               "<BODY>Not Found</BODY></HTML>";
}

當(dāng)文件存在,需要確定文件的MIME類型和發(fā)送適當(dāng)?shù)腗IME-Type指示符,利用private方法contentType()實(shí)現(xiàn)該上述任務(wù)。該方法將返回包含在Conten-Type頭部行的信息(字符串)。
現(xiàn)在,我們可以通過向socket的輸出流寫入status line 和唯一的一個(gè)header line來向客戶瀏覽器發(fā)送信息。


// Send the status line.
os.writeBytes(statusLine);
 
// Send the content type line.
os.writeBytes(?);
 
// Send a blank line to indicate the end of the header lines.
os.writeBytes(CRLF);

下面需要發(fā)送消息的entity body了。如果請求的文件存在,我們調(diào)用另一個(gè)方法來發(fā)送文件;如果請求的文件不存在,我們向客戶發(fā)送一個(gè)HTML編碼的錯(cuò)誤消息(前面已經(jīng)準(zhǔn)備好,即在變量entityBody中。

// Send the entity body.
if (fileExists) {
        sendBytes(fis, os);
        fis.close();
} else {
        os.writeBytes(?);
}

發(fā)送完entitybody后,線程的任務(wù)已經(jīng)全部完成,在結(jié)束線程前需要關(guān)閉流和socket.
我們還需要實(shí)現(xiàn)前面提到的兩個(gè)方法:contentType()和
sendBytes()。

private static void sendBytes(FileInputStream fis, OutputStream os) 
throws Exception
{
   // Construct a 1K buffer to hold bytes on their way to the socket.
   byte[] buffer = new byte[1024];
   int bytes = 0;
 
   // Copy requested file into the socket's output stream.
   while((bytes = fis.read(buffer)) != -1 ) {
      os.write(buffer, 0, bytes);
   }
}

read()和write()均拋出異常,我們在sendBytes中并不處理這些異常,而是將異常處理的任務(wù)交給調(diào)用sendBytes的方法。
變量buffer,用于作為文件和輸出流之間的中間存儲(chǔ)空間。當(dāng)從FileInputStream中讀取字節(jié)時(shí),,檢查讀取的字節(jié)是否為-1(即文件結(jié)束標(biāo)識 EOF)。如果讀到了EOF,read()返回已經(jīng)放入buffer的字節(jié)數(shù)。利用方法類OutputStream 的方法write() 將保存在buffer中的字節(jié)數(shù)據(jù)發(fā)送到輸出流, write的參數(shù)buffer、0、bytes分別為byte數(shù)組的名字、第一個(gè)字節(jié)的位置、需要寫出的字節(jié)數(shù)。
Web Server中需要完成最后一部分代碼為contentType,實(shí)現(xiàn)根據(jù)文件的擴(kuò)展名來確定所代表的MIME 類型。如果文件擴(kuò)展名未知,則方法返回application/octet-stream.


private static String contentType(String fileName)
{
        if(fileName.endsWith(".htm") || fileName.endsWith(".html")) {
               return "text/html";
        }
        if(?) {
               ?;
        }
        if(?) {
               ?;
        }
        return "application/octet-stream";
}

到現(xiàn)在為止,我們完成了Web Server的第二階段任務(wù)。嘗試從保存有homepage的目錄運(yùn)行Web服務(wù)器,記住在URL中包含Web服務(wù)器的工作端口。

整段代碼


import java.net.ServerSocket;
import java.net.Socket;
import java.awt.im.InputContext;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.StringTokenizer;

public final class Webserver {
    public static void main(String[] args) throws Exception {
        int port = 6666;

        //server創(chuàng)立接聽端口
        ServerSocket welcomeSocket = new ServerSocket(port);

        //處理一個(gè)死循環(huán)中的 HTTP 服務(wù)請求
        while(true)
        {
            //client監(jiān)聽一個(gè) TCP 連接請求
            Socket connectionSocket = welcomeSocket.accept();

            // 構(gòu)造一個(gè)對象來處理 HTTP 請求消息
            HttpRequest request = new HttpRequest(connectionSocket);

            //創(chuàng)建一個(gè)新的線程來處理請求
            Thread thread = new Thread(request);

            //開始新線程
            thread.start();
        }
    }
}

final class HttpRequest implements Runnable {

    //http空白行結(jié)束標(biāo)志
    final static String CRLF = "\r\n";
    Socket socket;
//構(gòu)造函數(shù)
    public HttpRequest(Socket socket) {
        this.socket = socket;
    }

    private void processRequest() throws Exception {
        //  獲取套接字的輸入和輸出流的引用
        InputStream is = socket.getInputStream();
        DataOutputStream os = new DataOutputStream(socket.getOutputStream());
        //設(shè)置輸入流的緩沖
        BufferedReader br = new BufferedReader(new InputStreamReader(is));

        //獲取請求的 HTTP 請求消息的行
        String requestline = br.readLine();
        //顯示請求行
        System.out.println();
        System.out.println(requestline);

        //得到且顯示獲取的頭部
        String headerline = null;
        while ((headerline = br.readLine()).length() != 0) {
            System.out.println(headerline);
        }
        //從請求行中提取文件名。
        StringTokenizer tokens = new StringTokenizer(requestline);
        tokens.nextToken();
        String fileName = tokens.nextToken();
        //前面加上“.”所以,在當(dāng)前目錄下的文件的請求
        fileName = '.' + fileName;
//打開文件流和文件信息
        FileInputStream fis = null;
        boolean fileExists = true;
        try {
            fis = new FileInputStream(fileName);
        } catch (FileNotFoundException e) {
            fileExists = false;
        }

        //構(gòu)建相應(yīng)信息
        String statusLine = null;
        String contentTypeLine = null;
        String entityBody = null;
        if (fileExists) {
            statusLine = "HTTP/1.0 200 OK";
            contentTypeLine = "Content-type:" + contentType(fileName) + CRLF;
        } else {
            statusLine = "HTTP/1.0 404 Not Found";
            contentTypeLine = "Content-type: text/html" + CRLF;
            entityBody = "<HTML>" + "<HEAD><TITLE>Not Found</TITLE></HEAD>" + "<BODY>Not Found</BODY></BODY>";
        }
        //發(fā)送狀態(tài)線
        os.writeBytes(statusLine);
        //發(fā)送鏈接類型
        os.writeBytes(contentTypeLine);
        ///發(fā)送一個(gè)空白行,以指示頭行的結(jié)束
        os.writeBytes(CRLF);

        if (fileExists) {
            sendBytes(fis, os);
            fis.close();
        } else {
            os.writeBytes(entityBody);
        }
        os.close();
        br.close();
        socket.close();
    }

    private void sendBytes(FileInputStream fis, DataOutputStream os) throws IOException {

        //構(gòu)建1K緩沖的方式字節(jié)
        byte[] buffer = new byte[1024];
        int bytes = 0;//桶裝為0

        //將請求的文件復(fù)制到套接字的輸出流中
        while ((bytes = fis.read(buffer)) != -1) {
            os.write(buffer, 0, bytes);
        }
    }

    private static String contentType(String fileName) {
        if (fileName.endsWith(".htm") || fileName.endsWith(".html")) {
            return "text/html";
        }
        if (fileName.endsWith(".jpg")) {
            return "text/jpg";
        }
        if (fileName.endsWith(".gif")) {
            return "text/gif";
        }

        if (fileName.endsWith(".mp3")) {
            return "audio/mp3";
        }
        if (fileName.endsWith(".mp4")) {
            return "video/mpeg4";
            }
        return "application/octet-stram";
    }
//實(shí)現(xiàn)runnable接口的run函數(shù)
    @Override
    public void run() {
        try {
            processRequest();
        } catch (Exception e) {
            System.out.println(e);
        }

    }

}

實(shí)驗(yàn)結(jié)果

test-1:

test-qilixiang.jpg

test-2:

![test-xingkong.gif . . .]


Paste_Image.png

test-3

Paste_Image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,568評論 19 139
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,803評論 11 349
  • HTTP概述 超文本傳輸協(xié)議(HTTP,HyperText Transfer Protocol) 是互聯(lián)網(wǎng)上應(yīng)用最...
    曹淵說創(chuàng)業(yè)閱讀 3,954評論 2 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,057評論 25 709
  • 生活中的跌跌撞撞,不斷試錯(cuò),在工作中修行----最終是為了找到愉悅的靈魂! 郭相麟
    郭相麟閱讀 149評論 0 0

友情鏈接更多精彩內(nèi)容