Programming Assignment 1: Building a Multi-Threaded Web Server
一、什么是Web服務(wù)器
網(wǎng)頁服務(wù)器(Web server)一詞有兩個意思:一臺負(fù)責(zé)提供網(wǎng)頁的電腦揪漩,主要是各種編程語言構(gòu)建而成旋恼,通過HTTP協(xié)議傳給客戶端(一般是指網(wǎng)頁瀏覽器)。一個提供網(wǎng)頁的服務(wù)器程序奄容。
雖然每個網(wǎng)頁服務(wù)器程序有很多不同冰更,但有一些共同的特點(diǎn):每一個網(wǎng)頁服務(wù)器程序都需要從網(wǎng)絡(luò)接受HTTP request,然后提供HTTP response給請求者昂勒。HTTP回復(fù)一般包含一個HTML文件蜀细,有時也可以包含純文本文件、圖像或其他類型的文件戈盈。
二奠衔、HTTP協(xié)議
簡介
本試驗(yàn)中我們將通過兩個階段來開發(fā)一個web服務(wù)器,最后完成一個能夠并行服務(wù)與多個請求的多線程Web服務(wù)器塘娶。
我們將實(shí)現(xiàn)在RFC 1945定義的HTTP1.0归斤。根據(jù)定義,每個Web page中的對象將通過單獨(dú)的HTTP消息來獲取刁岸。所實(shí)現(xiàn)Web服務(wù)器將能夠并發(fā)地服務(wù)于多個請求脏里,這意味著Web服務(wù)器是多線程的。 Web服務(wù)器的主線程負(fù)責(zé)偵聽某個端口虹曙,當(dāng)收到TCP連接請求時迫横,將創(chuàng)建一個新的socket負(fù)責(zé)與該TCP連接,并創(chuàng)建新的線程具體負(fù)責(zé)通過該連接的消息傳遞酝碳。為了簡化程序設(shè)計(jì)任務(wù)矾踱,我們分兩階段來設(shè)計(jì)Web服務(wù)器。
第一階段:編寫僅僅顯示所收到HTTP Request消息所有頭部行的一個多線程Web服務(wù)器疏哗。當(dāng)該程序運(yùn)行正確后呛讲,將添加適當(dāng)?shù)拇a以實(shí)現(xiàn)對Request消息的適當(dāng)響應(yīng)。
開發(fā)Web服務(wù)器時沃斤,可以通過Web瀏覽器來測試它圣蝎。不過,所編寫的Web服務(wù)器通常并不工作于80端口衡瓶,因此徘公,測試時在瀏覽器的地址欄中需要指定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頁面捂齐,以便在瀏覽器中顯示錯誤信息。
Web Server in Java: Part A
下面缩抡,我們將實(shí)現(xiàn)第一階段的編程任務(wù)奠宜。當(dāng)看到"?"時,你需要在該處添加相應(yīng)的代碼瞻想。
我們的第一個Web服務(wù)器將是多線程的压真,所收到的每個Request消息將交由單獨(dú)的線程進(jìn)行處理。這使得服務(wù)器可以并發(fā)地為多個客戶服務(wù), 或者是并發(fā)地服務(wù)于一個客戶的多個請求.當(dāng)創(chuàng)建一個新線程時蘑险,需要向線程的構(gòu)造函數(shù)傳遞實(shí)現(xiàn)了Runnable 接口的類的一個實(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)聽端口呵俏,但需要記著在瀏覽器地址欄中輸入URL時指定Web服務(wù)器的動作端口堆缘。
public static void main(String argv[]) throws Exception
{
// Set the port number.
int port = 6789;
. . .
}
下面,創(chuàng)建監(jiān)聽端口以等待TCP連接請求普碎。由于Web服務(wù)器將不間斷地提供服務(wù)套啤,我們將偵聽操作放在一個無窮循環(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)建一個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對象在一個單獨(dú)的線程中處理隨后的HTTP請求,我們首先創(chuàng)建一個Thread對象,將HttpRequest對象作為參數(shù)傳遞給Thread的構(gòu)造函數(shù)涝影,然后調(diào)用Thread的start()方法啟動線程枣察。
當(dāng)一個Thread創(chuàng)建并啟動后,主線程回到了循環(huán)體的首部燃逻。主線程將被阻塞(block)在accept處等待另一個TCP 連接請求的到達(dá)序目。此時,剛剛創(chuàng)建的線程正在運(yùn)行伯襟。當(dāng)另一個TCP連接請求到達(dá)時猿涨,主線程將不管前面創(chuàng)建的線程是否結(jié)束,重復(fù)上面的操作姆怪,創(chuàng)建新線程負(fù)責(zé)新連接的請求處理叛赚。
到這為止澡绩,主線程的工作就完成了,后面我們將集中精力設(shè)計(jì)類 HttpRequest俺附。
我們聲明HttpRequest類中的兩個變量: CRLF and socket肥卡。根據(jù)HTTP規(guī)范, 我們需要用”回車換行”作為Response消息頭部行的結(jié)束。因此事镣,為了使用方便步鉴,我們定義了一個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ā)送消息中有多少頭部行出爹,必須利用一個循環(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)階段無須針對頭部行做其它的處理严就,我們僅僅利用臨時變量headerLine來保存頭部行的信息总寻。循環(huán)操作直到下面的表達(dá)式值等于0時停止。也就是讀取的 頭部行的長度如果為零梢为,表示讀出了一個空行渐行,意味著所有的頭部行已經(jīng)全部讀出(參看教材的2.2 部分,頭部行和entity body之間利用一個空行作為分割)铸董。
(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í)際上,一個瀏覽器可能使用GET碘勉、POST和HEAD方法(HTTP1.0)
利用類StringTokenizer從Request行中解析出文件名字稚失。
首先,創(chuàng)建一個 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;
由于瀏覽器在文件名字前加了一個“/“,我們在它前面加上一個字符 ”.”矾屯,從而限定從當(dāng)前目錄開始獲取文件兼蕊。
現(xiàn)在有了客戶請求的文件名字,我們可以打開該文件作為向客戶發(fā)送該文件的第一步件蚕。如果文件不存在孙技,構(gòu)造函數(shù) FileInputStream() 將拋出異常FileNotFoundException产禾,為了在拋出此可能的異常后不終止線程的執(zhí)行,利用一個try/catch塊將布爾型變量fileExists設(shè)置為false诽嘉。后面我們將使用該變量來構(gòu)建一個錯誤響應(yīng)消息煌抒,而不是發(fā)送一個根本不存在的文件昙沦。
// 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)文件不存在時,Web服務(wù)器將返回狀態(tài)行為“404 Not Found“彭羹,entity body中保存利用HTML創(chuàng)建的錯誤消息黄伊。
// 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 和唯一的一個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)用另一個方法來發(fā)送文件悦即;如果請求的文件不存在,我們向客戶發(fā)送一個HTML編碼的錯誤消息(前面已經(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)前面提到的兩個方法: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,用于作為文件和輸出流之間的中間存儲空間种蝶。當(dāng)從FileInputStream中讀取字節(jié)時契耿,,檢查讀取的字節(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ù)組的名字踢械、第一個字節(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);
//處理一個死循環(huán)中的 HTTP 服務(wù)請求
while(true)
{
//client監(jiān)聽一個 TCP 連接請求
Socket connectionSocket = welcomeSocket.accept();
// 構(gòu)造一個對象來處理 HTTP 請求消息
HttpRequest request = new HttpRequest(connectionSocket);
//創(chuàng)建一個新的線程來處理請求
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ā)送一個空白行个粱,以指示頭行的結(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 . . .]