一個WEB服務器也被稱為一個HTTP服務器,因為它使用HTTP協議和它的客戶進行通訊,而這些客戶通常是瀏覽器。 一個基于JAVA的WEB服務器使用了兩個重要的類:java.net.Socket和java.net.ServerSocket,并且是通過HTTP消息進行通訊的。本文開頭將討論HTTP和這兩個類,后面,將解釋一個簡單WEB服務器應用程序的工作機制。
超文本傳輸協議 (HTTP)
HTTP協議允許服務器和客戶機通過INTERNET接收和發送數據。它是個請求和回應協議----客戶機發送請求,服務器對請求給出回應。HTTP 使用可靠的TCP 連接,默認TCP端口是80。HTTP的第一版是HTTP/0.9,隨后被 HTTP/1.0所取代。當前最新的版本是HTTP/1.1,這個在RPC2616規范文檔中給出了定義。
這一章節簡單講敘了HTTP 1.1, 對于你理解WEB服務器應用程序發送的消息還是足夠的。如果你很感興趣,可以參考RFC 2616文檔。
使用HTTP,客戶端通過建立一個連接和發送一個HTTP請求來初始化事務會話,服務器聯系客戶端或者回應一個callback連接給客戶端。 它們都可以中斷連接。比如,在使用WEB瀏覽器時,你可以點擊瀏覽器上的STOP按鈕來停止文件下載進程,就有效的關閉了和這個WEB服務器的HTTP連接。
HTTP 請求(Requests)
一個HTTP request包含三個部分:
方法,URL,協議/版本(Method-URI-Protocol/Version)
請求包頭Request headers
實體包(Entity body)
下面給出一個HTTP請求的范例:
POST /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
LastName=Franks&FirstName=Michael
請求的第一行就是method-URI-Protocol/Version。
POST /servlet/default.jsp HTTP/1.1
請求的是POST方法,后面的 /servlet/default.jsp 表示一個URL地址,HTTP/1.1表示協議的版本。
HTTP標準規范定義了一些請求方法,用來給每個HTTP請求所使用。HTTP 1.1支持7中請求方法: GET, POST, HEAD, OPTIONS, PUT, DELETE, 和 TRACE。 GET和POST 在INTERNET的應用程序中是使用最普遍的兩個方法。
URI完整的指明了一個INTERNET資源。一個URI通常是相對于服務器的根目錄被解釋的。 因此,它總是使用符號(/)開頭。一個URL實際是一個URI類型。協議版本表示當前正在使用的HTTP協議的版本。
請求包頭(request header)包含了一些有用的客戶機環境的信息和請求的實體(entity body)信息。比如,它可以包含瀏覽器使用的語言和實體的長度等等。每個請求包頭都被CRLF(回車換行)序列所分離。
在先前的HTTP請求中,實體是下面簡單的一行:
LastName=Franks&FirstName=Michael
在一個典型的HTTP請求中,這個實體能夠很容易地變得更長。
HTTP響應(Responses)
和請求類似,一個HTTP響應也包含三個部分:
協議狀態 代碼描敘(Protocol-Status code-Description)
響應包頭(Response headers)
實體(Entity body)
下面是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 Response Example</title></head><body>
Welcome to Brainy Software
</body>
</html>
第一行的響應包頭和上面的請求包頭很相似。 第一行告訴我們,協議是使用的HTTP1.1,響應請求已成功(200表示成功),一切已OK。
響應包頭和請求包頭相似,也包含一些有用的信息。響應的實體是HTML那一部分的內容。包頭和實體也都是被CRLF序列分離開的。
Socket類
套接字(socket)是網絡連接的一個端點。它使得應用程序能夠通過網絡進行讀和寫的操作。 通過在連接上發送和接受字節流,兩個位于不同計算機的軟件程序能夠彼此相互通訊。為了發送一個消息到另一個程序,你需要知道對方機器的IP地址和socket端口號。在JAVA中,一個socket是由java.net.Socket類所表示的。
為了創建一個套接字,你可以使用Socket類的構造函數來完成。 這些構造函數接受主機名和端口:
public Socket(String host, int port)
host表示遠程計算機名或者IP地址,port表示該遠程應用的端口號。比如,要在80端口連接到yahoo.com,你需要構造下面的socket:
new Socket("yahoo.com", 80);
一旦你成功創建了一個Socket類的實例,就可以使用它來發送和接受字節流了。 要發送字節流,必須首先調用Socket類的getOutputStream 方法來獲得一個java.io.OutputStream對象。要發送一個文本到遠程應用程序,經常要構造一個從OutputStream對象返回的java.io.PrintWriter對象。要接收連接另一端的字節流,要調用Socket類的getInputStream方法,該方法是從 java.io.InputStream返回的。
下面的程序段創建了一個socket,和本地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();
要從服務器得到一個確切的響應,你需要發送一個遵循HTTP協議規則的HTTP請求。如果你閱讀了上面的那段"超文本傳輸協議(HTTP)" ,那么你就應該能夠理解剛才上面建立socket的代碼。
ServerSocket 類
Socket 類表示的是客戶端的socket。無論什么時候,只要你想連接到一個遠程服務器的應用,你都要構建一個socket。如果你想執行一個服務器應用程序,比如HTTP服務或者FTP服務的程序,那么你需要使用不同的途徑。因為你的服務器必須一直是開機閑置,所以它不知道什么時候客戶機試圖來連接它。
這個時候,需要使用java.net.ServerSocket 類。它會實現一個服務器socket。一個服務器socket會等待來自客戶端的連接。一旦它接收到一個連接請求,它就會創建一個 Socket 實例來處理和客戶端通訊的問題。
要創建一個服務器socket,可以使用四種ServerSocket類構造方法中的一種來實現。你需要制定服務器socket監聽的IP地址和端口。 典型的,IP地址如果是127.0.0.1,意味著服務器socket將監聽本地機器。這個被監聽的IP地址被認為是一種綁定地址。server socket的另一個重要屬性是它的 backlog屬性,它是在server socket拒絕連接請求前,能夠接受的連接請求的最大隊列長度。
ServerSocket類的構造函數之一如下:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
對于這個構造函數而言,綁定地址必須是java.net.InetAddress 的一個實例。一個簡單的辦法是通過調用它的靜態方法getByName來構造一個InetAddres對象。該方法來一個包含主機名的字符串參數:
InetAddress.getByName("127.0.0.1");
下面一行代碼構造一個ServerSocket ,它監聽本地機器的8080端口,backlog設置為1。
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
一旦有了一個 ServerSocket 實例,可以通過調用accept方法來告訴它等待進來的連接請求。這個方法只有在有一個連接請求時才返回。它返回的是Socket類的實例。這個Socket對象能夠發送和接受來自客戶端應用的字節流,就是第一節所講到的socket類。實際上,accept 是本文提及的唯一一個在應用中使用的方法。
Application應用
我們的web服務器應用是ex01.pyrmont包的一部分,包含三個類:
HttpServer
Request
Response
這個應用的入口(靜態main方法)是HttpServer類。它創建了一個HttpServer 實例來調用它的await方法。 就象這個方法名所暗示的,await 方法在一個指定的端口等待一個HTTP請求,并處理它們,然后發送回應給客戶端。它保持等待狀態,直到收到一個shutdown命令。 (命令名await來代替wait的原因是wait是System.Object類中的一個用于線程方面的重要方法)
應用僅僅只發送靜態資源,比如來自特定目錄的HTML和圖片文件。不支持動態包頭 (比如日期或者cookie) 。
在下面的段落中,讓我們來看看這三個類吧。
HttpServer 類
HttpServer類表示一個web服務器,且在公共靜態目錄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作為機器名。端口是8080。staticResource是請求的文件夾名,它必須位于WEB-ROOT目錄中。
必然,如果你使用同一個計算機來測試應用,你想向HttpServer請求發送一個index.html 文件,那么使用如下URL:
http://localhost:8080/index.html
想要停止服務器,可以通過發送一個shutdown命令。該命令是被HttpServer 類中的靜態SHUTDOWN_COMMAND變量所定義:
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
因此,要停止服務,你可以使用命令:
http://localhost:8080/SHUTDOWN
現在讓我們來看看前面提到的await方法。下面一個程序清單給出了解釋。
Listing 1.1. The HttpServer class' await method
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;
}
}
}
await方法是通過創建一個ServerSocket實例而開始的。然后它進入了一個WHILE循環:
serverSocket = new ServerSocket(
port, 1, InetAddress.getByName("127.0.0.1"));
...
// Loop waiting for a request
while (!shutdown) {
...
}
socket = serverSocket.accept();
在收到一個請求后,await方法從accept方法返回的socket實例中獲得java.io.InputStream 和java.io.OutputStream對象。
input = socket.getInputStream();
output = socket.getOutputStream();
await于是就創建一個Request對象并調用它的 parse 方法來解析原始的HTTP請求信息。
// create Request object and parse
Request request = new Request(input);
request.parse();
接下來,await 方法創建了一個Response 對象,使用setRequest方法并調用它的sendStaticResource 方法。
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
最后,await關閉該Socket。調用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類
Request類代表一個HTTP請求。Socket處理客戶端的通訊,將返回一個InputStream對象,通過傳遞該對象,可以構造一個Request類的實例。通過調用InputStream 對象的read方法來獲得這個HTTP請求的原始數據(raw data)。
Request 有兩個公共方法:parse 和 getUri。parse方法解釋HTTP請求的原始數據。它不做很多事情----它能夠利用的唯一信息只是HTTP請求的URI ,這個URI是從私有方法 parseUri.得到的。parseUri 方法保存URI 到uri 變量中,然后調用公共方法getUri來返回一個HTTP請求的URI。
為了理解parse 和 parseUri 方法是如何工作的,需要知道HTTP請求的內部結構。這個結構是在RFC2616文檔中定義的。
一個HTTP請求包含三個部分:
請求行(Request line)
請求包頭(Headers)
消息體(Message body)
現在,我們僅僅只對HTTP請求的第一部分請求行(Request line)感興趣。一個請求行由方法標記開始,后面根請求的URI和協議版本,最后由CRLF字符結束。請求行中的元素被空格字符分開。比如,使用GET方法請求的index.html文件的請求行如下:
GET /index.html HTTP/1.1 //這是一個請求行
方法parse從socket的InputStream 中讀取整個字節流,該字節流是 Request 對象傳遞進來的,然后parse將這些字節流存儲在一個緩沖區里, 在緩沖區中組裝一個稱為request的StringBuffer對象。
下面的Listing 1.2.顯示了parse方法的用法:
Listing 1.2. The Request class' parse method
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());
}
parseUri 方法從請求行那里得到URI。Listing 1.3 展示了parseUri 方法的用途。 parseUri 減縮請求中的第一個和第二個空格來獲得URI。
Listing 1.3. The Request class' parseUri method
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;
}
Response類
Response表示一個HTTP響應。它的構造函數接受一個OutputStream對象,比如下面的:
public Response(OutputStream output) {
this.output = output;
}
Response 對象被HttpServer類的await方法構造,該方法被傳遞的參數是從socket那里得到的OutputStream對象。
Response類有兩個公共方法: setRequest和sendStaticResource. setRequest方法傳遞一個Request對象給Response對象。Listing 1.4中的代碼顯示了這個:
Listing 1.4. The Response class' setRequest method
public void setRequest(Request request) {
this.request = request;
}
sendStaticResource 方法用來發送一個靜態資源,比如HTML文件。Listing 1.5給出了它的實現過程:
Listing 1.5. The Response class' sendStaticResource method
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();
}
}
sendStaticResource 方法是非常簡單的。它首先傳遞父路徑和子路徑給File類的構造器,從而對java.io.File類進行了實例化。
File file = new File(HttpServer.WEB_ROOT, request.getUri());
然后它檢查文件是否存在。如果存在,sendStaticResource 方法通過傳遞File對象來構造一個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 = "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());
編譯和運行應用程序
為了編譯和運行應用,你首先需要解壓包含本文應用程序的.zip文件。你解壓的目錄成為工作目錄(working directory),它有三個子目錄: src/, classes/, 和 lib/。 要編譯應用程序需要在工作目錄輸入如下語句:
javac -d . src/ex01/pyrmont/*.java
這個-d 選項參數將結果寫到當前目錄,而不是src/ 目錄。
要運行應用程序,在工作目錄中輸入如下語句:
java ex01.pyrmont.HttpServer
要測試你的應用程序,打開瀏覽器,在地址欄中輸入如下URL:
http://localhost:8080/index.html
你將可以看到瀏覽器中顯示的index.html 頁面。
Figure 1. The output from the web server
在控制臺(Console),你能看到如下內容:
GET /index.html HTTP/1.1
Accept: */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
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 4.01; Windows 98)
Host: localhost:8080
Connection: Keep-Alive
概要總結
在本文中,你了解了一個簡單的WEB服務器的工作機制。本文附帶的應用程序源代碼只包含三個類,但并不是所有的都有用。盡管如此,它還是能被作為一種很好的學習工具為我們服務。