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-2:
![test-xingkong.gif . . .]

test-3
