第一章:一個簡單的Web服務器
本章說明java web服務器是如何工作的。Web服務器也成為超文本傳輸協議(HTTP)服務器,因為它使用HTTP來跟客戶端進行通信的,這通常是個web瀏覽器。一個基于java的web服務器使用兩個重要的類:java.net.Socket和java.net.ServerSocket,并通過HTTP消息進行通信。因此這章就自然是從HTTP和這兩個類的討論開始的。接下去,解釋這章附帶的一個簡單的web服務器。
超文本傳輸協議(HTTP)
HTTP是一種協議,允許web服務器和瀏覽器通過互聯網進行來發送和接受數據。它是一種請求和響應協議。客戶端請求一個文件而服務器響應請求。HTTP使用可靠的TCP連接--TCP默認使用80端口。第一個HTTP版是HTTP/0.9,然后被HTTP/1.0所替代。正在取代HTTP/1.0的是當前版本HTTP/1.1,它定義于征求意見文檔(RFC) 2616,可以從http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf下載。
注意:本節涵蓋的HTTP 1.1只是簡略的幫助你理解web服務器應用發送的消息。假如你對更多詳細信息感興趣,請閱讀RFC 2616。
在HTTP中,始終都是客戶端通過建立連接和發送一個HTTP請求從而開啟一個事務。web服務器不需要聯系客戶端或者對客戶端做一個回調連接。無論是客戶端或者服務器都可以提前終止連接。舉例來說,當你正在使用一個web瀏覽器的時候,可以通過點擊瀏覽器上的停止按鈕來停止一個文件的下載進程,從而有效的關閉與web服務器的HTTP連接。
HTTP請求
一個HTTP請求包括三個組成部分:
· 方法—統一資源標識符(URI)—協議/版本
· 請求的頭部
· 主體內容
下面是一個HTTP請求的例子:
POST /examples/default.jsp HTTP/1.1
Accept: text/plain; text/html
Accept-Language: en-gb
Connection: Keep-Alive
Host: localhost
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
方法—統一資源標識符(URI)—協議/版本出現在請求的第一行。
POST /examples/default.jsp HTTP/1.1
這里POST是請求方法,/examples/default.jsp是URI,而HTTP/1.1是協議/版本部分。
每個HTTP請求可以使用HTTP標準里邊提到的多種方法之一。HTTP 1.1支持7種類型的請求:GET, POST,
HEAD, OPTIONS, PUT, DELETE和TRACE。GET和POST在互聯網應用里邊最普遍使用的。
URI完全指明了一個互聯網資源。URI通常是相對服務器的根目錄解釋的。因此,始終一斜線/開頭。統一資源定位器(URL)其實是一種URI(查看http://www.ietf.org/rfc/rfc2396.txt)來的。該協議版本代表了正在使用的HTTP協議的版本。
請求的頭部包含了關于客戶端環境和請求的主體內容的有用信息。例如它可能包括瀏覽器設置的語言,主體內容的長度等等。每個頭部通過一個回車換行符(CRLF)來分隔的。
對于HTTP請求格式來說,頭部和主體內容之間有一個回車換行符(CRLF)是相當重要的。CRLF告訴HTTP服務器主體內容是在什么地方開始的。在一些互聯網編程書籍中,CRLF還被認為是HTTP請求的第四部分。
在前面一個HTTP請求中,主體內容只不過是下面一行:
lastName=Franks&firstName=Michael
實體內容在一個典型的HTTP請求中可以很容易的變得更長。
HTTP響應
類似于HTTP請求,一個HTTP響應也包括三個組成部分:
· 方法—統一資源標識符(URI)—協議/版本
· 響應的頭部
· 主體內容
下面是一個HTTP響應的例子:
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Date: Mon, 5 Jan 2004 13:13:33 GMT
Content-Type: text/html
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
Content-Length: 112
<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Brainy Software
</body>
</html>
響應頭部的第一行類似于請求頭部的第一行。第一行告訴你該協議使用HTTP 1.1,請求成功(200=成功),表示一切都運行良好。
響應頭部和請求頭部類似,也包括很多有用的信息。響應的主體內容是響應本身的HTML內容。頭部和主體內容通過CRLF分隔開來。
Socket類
套接字是網絡連接的一個端點。套接字使得一個應用可以從網絡中讀取和寫入數據。放在兩個不同計算機上的兩個應用可以通過連接發送和接受字節流。為了從你的應用發送一條信息到另一個應用,你需要知道另一個應用的IP地址和套接字端口。在Java里邊,套接字指的是java.net.Socket類。
要創建一個套接字,你可以使用Socket類眾多構造方法中的一個。其中一個接收主機名稱和端口號:
public Socket (java.lang.String host, int port)
在這里主機是指遠程機器名稱或者IP地址,端口是指遠程應用的端口號。例如,要連接yahoo.com的80端口,你需要構造以下的Socket對象:
new Socket ("yahoo.com", 80);
一旦你成功創建了一個Socket類的實例,你可以使用它來發送和接受字節流。要發送字節流,你首先必須調用Socket類的getOutputStream方法來獲取一個java.io.OutputStream對象。要發送文本到一個遠程應用,你經常要從返回的OutputStream對象中構造一個java.io.PrintWriter對象。要從連接的另一端接受字節流,你可以調用Socket類的getInputStream方法用來返回一個java.io.InputStream對象。
以下的代碼片段創建了一個套接字,可以和本地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();
請注意,為了從web服務器獲取適當的響應,你需要發送一個遵守HTTP協議的HTTP請求。假如你已經閱讀了前面一節超文本傳輸協議(HTTP),你應該能夠理解上面代碼提到的HTTP請求。
注意:你可以本書附帶的com.brainysoftware.pyrmont.util.HttpSniffer類來發送一個HTTP請求并顯示響應。要使用這個Java程序,你必須連接到互聯網上。雖然它有可能并不會起作用,假如你有設置防火墻的話。
ServerSocket類
Socket類代表一個客戶端套接字,即任何時候你想連接到一個遠程服務器應用的時候你構造的套接字,現在,假如你想實施一個服務器應用,例如一個HTTP服務器或者FTP服務器,你需要一種不同的做法。這是因為你的服務器必須隨時待命,因為它不知道一個客戶端應用什么時候會嘗試去連接它。為了讓你的應用能隨時待命,你需要使用java.net.ServerSocket類。這是服務器套接字的實現。
ServerSocket和Socket不同,服務器套接字的角色是等待來自客戶端的連接請求。一旦服務器套接字獲得一個連接請求,它創建一個Socket實例來與客戶端進行通信。
要創建一個服務器套接字,你需要使用ServerSocket類提供的四個構造方法中的一個。你需要指定IP地址和服務器套接字將要進行監聽的端口號。通常,IP地址將會是127.0.0.1,也就是說,服務器套接字將會監聽本地機器。服務器套接字正在監聽的IP地址被稱為是綁定地址。服務器套接字的另一個重要的屬性是backlog,這是服務器套接字開始拒絕傳入的請求之前,傳入的連接請求的最大隊列長度。
其中一個ServerSocket類的構造方法如下所示:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
對于這個構造方法,綁定地址必須是java.net.InetAddress的一個實例。一種構造InetAddress對象的簡單的方法是調用它的靜態方法getByName,傳入一個包含主機名稱的字符串,就像下面的代碼一樣。
InetAddress.getByName("127.0.0.1");
下面一行代碼構造了一個監聽的本地機器8080端口的ServerSocket,它的backlog為1。
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
一旦你有一個ServerSocket實例,你可以讓它在綁定地址和服務器套接字正在監聽的端口上等待傳入的連接請求。你可以通過調用ServerSocket類的accept方法做到這點。這個方法只會在有連接請求時才會返回,并且返回值是一個Socket類的實例。Socket對象接下去可以發送字節流并從客戶端應用中接受字節流,就像前一節"Socket類"解釋的那樣。實際上,這章附帶的程序中,accept方法是唯一用到的方法。
應用程序
我們的web服務器應用程序放在ex01.pyrmont包里邊,由三個類組成:
· HttpServer
· Request
· Response
這個應用程序的入口點(靜態main方法)可以在HttpServer類里邊找到。main方法創建了一個HttpServer的實例并調用了它的await方法。await方法,顧名思義就是在一個指定的端口上等待HTTP請求,處理它們并發送響應返回客戶端。它一直等待直至接收到shutdown命令。
應用程序不能做什么,除了發送靜態資源,例如放在一個特定目錄的HTML文件和圖像文件。它也在控制臺上顯示傳入的HTTP請求的字節流。不過,它不給瀏覽器發送任何的頭部例如日期或者cookies。
現在我們將在以下各小節中看看這三個類。
HttpServer類
HttpServer類代表一個web服務器并展示在Listing 1.1中。請注意,await方法放在Listing 1.2中,為了節省空間沒有重復放在Listing 1.1中。
Listing 1.1: HttpServer類
package ex01.pyrmont;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.File;
public class HttpServer {
/** WEB_ROOT is the directory where our HTML and other files reside.
* For this package, WEB_ROOT is the "webroot" directory under the
* working directory.
* The working directory is the location in the file system
* from where the java command was invoked.
*/
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
// shutdown command
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
// the shutdown command received
private boolean shutdown = false;
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
}
public void await() {
...
}
}
Listing 1.2: 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);
}
// 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;
}
}
}
web服務器能提供公共靜態final變量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作為machineName。端口是8080,staticResource是你需要請求的文件的名稱,且必須位于WEB_ROOT里邊。
舉例來說,假如你正在使用同一臺計算機上測試應用程序,并且你想要調用HttpServer對象去發送一個index.html文件,你可以使用一下的URL:
http://localhost:8080/index.html
要停止服務器,你可以在web瀏覽器的地址欄或者網址框里邊敲入預定義字符串,就在URL的host:port的后面,發送一個shutdown命令。shutdown命令是在HttpServer類的靜態final變量SHUTDOWN里邊定義的:
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
因此,要停止服務器,使用下面的URL:
http://localhost:8080/SHUTDOWN
現在我們來看看Listing 1.2印出來的await方法。
使用方法名await而不是wait是因為wait方法是與線程相關的java.lang.Object類的一個重要方法。
await方法首先創建一個ServerSocket實例然后進入一個while循環。
serverSocket = new ServerSocket(port, 1,
InetAddress.getByName("127.0.0.1"));
...
// Loop waiting for a request
while (!shutdown) {
...
}
while循環里邊的代碼運行到ServletSocket的accept方法停了下來,只會在8080端口接收到一個HTTP請求的時候才返回:
socket = serverSocket.accept();
接收到請求之后,await方法從accept方法返回的Socket實例中取得java.io.InputStream和java.io.OutputStream對象。
input = socket.getInputStream();
output = socket.getOutputStream();
await方法接下去創建一個ex01.pyrmont.Request對象并且調用它的parse方法去解析HTTP請求的原始數據。
// create Request object and parse
Request request = new Request(input);
request.parse ();
在這之后,await方法創建一個Response對象,把Request對象設置給它,并調用它的sendStaticResource方法。
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
最后,await關閉套接字并調用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類
ex01.pyrmont.Request類代表一個HTTP請求。從負責與客戶端通信的Socket中傳遞過來InputStream對象來構造這個類的一個實例。你調用InputStream對象其中一個read方法來獲取HTTP請求的原始數據。
Request類顯示在Listing 1.3。Request對象有parse和getUri兩個公共方法,分別在Listings 1.4和1.5列出來。
Listing 1.3: Request類
package ex01.pyrmont;
import java.io.InputStream;
import java.io.IOException;
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
public void parse() {
...
}
private String parseUri(String requestString) {
...
}
public String getUri() {
return uri;
}
}
Listing 1.4: Request類的parse方法
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());
}
Listing 1.5: 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;
}
parse方法解析HTTP請求里邊的原始數據。這個方法沒有做很多事情。它唯一可用的信息是通過調用HTTP請求的私有方法parseUri獲得的URI。parseUri方法在uri變量里邊存儲URI。公共方法getUri被調用并返回HTTP請求的URI。
注意:在第3章和下面各章的附帶程序里邊,HTTP請求將會對原始數據進行更多的處理。
為了理解parse和parseUri方法是怎樣工作的,你需要知道上一節“超文本傳輸協議(HTTP)”討論的HTTP請求的結構。在這一章中,我們僅僅關注HTTP請求的第一部分,請求行。請求行從一個方法標記開始,接下去是請求的URI和協議版本,最后是用回車換行符(CRLF)結束。請求行里邊的元素是通過一個空格來分隔的。例如,使用GET方法來請求index.html文件的請求行如下所示。
GET /index.html HTTP/1.1
parse方法從傳遞給Requst對象的套接字的InputStream中讀取整個字節流并在一個緩沖區中存儲字節數組。然后它使用緩沖區字節數據的字節來填入一個StringBuffer對象,并且把代表StringBuffer的字符串傳遞給parseUri方法。
parse方法列在Listing 1.4。
然后parseUri方法從請求行里邊獲得URI。Listing 1.5給出了parseUri方法。parseUri方法搜索請求里邊的第一個和第二個空格并從中獲取URI。
Response類
ex01.pyrmont.Response類代表一個HTTP響應,在Listing 1.6里邊給出。
Listing 1.6: Response類
package ex01.pyrmont;
import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.File;
/*
HTTP Response = Status-Line
*(( general-header | response-header | entity-header ) CRLF)
CRLF
[ message-body ]
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
*/
public class Response {
private static final int BUFFER_SIZE = 1024;
Request request;
OutputStream output;
public Response(OutputStream output) {
this.output = output;
}
public void setRequest(Request request) {
this.request = request;
}
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();
}
}
}
首先注意到它的構造方法接收一個java.io.OutputStream對象,就像如下所示。
public Response(OutputStream output) {
this.output = output;
}
響應對象是通過傳遞由套接字獲得的OutputStream對象給HttpServer類的await方法來構造的。Response類有兩個公共方法:setRequest和sendStaticResource。setRequest方法用來傳遞一個Request對象給Response對象。
sendStaticResource方法是用來發送一個靜態資源,例如一個HTML文件。它首先通過傳遞上一級目錄的路徑和子路徑給File累的構造方法來實例化java.io.File類。
File file = new File(HttpServer.WEB_ROOT, request.getUri());
然后它檢查該文件是否存在。假如存在的話,通過傳遞File對象讓sendStaticResource構造一個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 =
"Content-Type: text/html\r\n" +
"Content-Length: 23\r\n" +
"\r\n" +
"<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());
運行應用程序
為了運行應用程序,可以在工作目錄下敲入下面的命令:
java ex01.pyrmont.HttpServer
為了測試應用程序,可以打開你的瀏覽器并在地址欄或網址框中敲入下面的命令:
http://localhost:8080/index.html
正如Figure 1.1所示,你將會在你的瀏覽器里邊看到index.html頁面。
Figure 1.1: web服務器的輸出
在控制臺中,你可以看到類似于下面的HTTP請求:
GET /index.html HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,
application/vnd.ms-excel, application/msword, application/vnd.ms-
powerpoint, application/x-shockwave-flash, application/pdf, */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR
1.1.4322)
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 6.0; Windows NT 5.1; .NET CLR
1.1.4322)
Host: localhost:8080
Connection: Keep-Alive
總結
在這章中你已經看到一個簡單的web服務器是如何工作的。這章附帶的程序僅僅由三個類組成,并不是全功能的。不過,它提供了一個良好的學習工具。下一章將要討論動態內容的處理過程。