Socket通信-web服務器基本原理(靜態)

一個Web服務器也被稱爲HTTP服務器,它通過HTTP協議與客戶端通信。這個客戶端通常指的是Web瀏覽器。一個基於Java的Web服務器用到二個重要的類,java.net.Socket與java.net.ServerSocket,並通過HTTP消息通信。因此,本文從討論HTTP與這二個類開始,然後我將解釋一個與本文相關的簡單的Web應用。

 

The Hypertext Transfer Protocol(HTTP)

    HTTP是一種讓Web服務器與瀏覽器(客戶端)通過Internet發送與接收數據的協議。它是一個請求、響應協議--客戶端發出一個請求,服務器響應這個請求。HTTP運用可靠的TCP連接,通常用的TCP80端口。它的第一個版本是HTTP/0.9 ,然後被HTTP/1.0取代。當前的版本是HTTP/1.1,由RFC2616(.pdf)定義。

本節主要對應HTTP1.1,足夠使你充分理解由Web服務器程序發出的消息。如果你對更加詳細的知識有興趣,可以參考 RFC2616。

在HTTP中,客戶端總是通過建立一個連接與發送一個HTTP請求來發起一個事務。服務器不能主動去與客戶端聯繫,也不能給客戶端發出一個回叫連接。客戶端與服務器端都可以提前中斷一個連接。例如,當用一個瀏覽器下載一個文件時,你可以通過點擊“停止”鍵來中斷文件的下載,關閉與服務器的HTTP連接。

HTTP請求

一個HTTP請求包含三個部分:

Method-URI-Protocol/Version 方法-地址-版本

Request header 請求頭

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

The Method-URI-Protocol/Version 在這個請求的第一行:

POST /servlet/default.jsp HTTP/1.1

 

其中 POST 是請求的類型。每個客戶端HTTP請求可以是HTTP規範中指定的許多請求類型中的一種。HTTP1.1支持七種類型的請求,它們是GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE。其中GET與POST是Internet 應用中經常用到的二種請求類型。

URI 完整地指定了 Internet 資源。一個URI通常被解析爲相對服務器的根目錄。這樣,它應該總是以一個 '/' 前綴開始。一個URL實際上是 URI 的一種類型。

Version 指的是該 HTTP 請求所用到的HTTP協議版本。

請求頭包含了客戶端環境與請求實體的一些有用的信息。例如它包含瀏覽器設定的語言、實體的長度等等。每條請求頭用回車換行符(CRLF)分開。

一個非常重要的空行分開了請求頭與實體,它標誌着實體內容的開始。一些 Internet 開發書籍認爲這個 CRLF 空行是 HTTP 請求的第四個部分。

在上面的 HTTP 請求中,實體只是簡單以下的一行:

LastName=Franks&FirstName=Michael

在一個典型的 HTTP 請求中,請求實體內容會長得多。

HTTP 響應

與請求相似,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

Welcome to Brainy Software

響應頭的第一行類似請求頭的第一行,告訴你所用的協議是 HTTP 1.1 ,請求成功(200=success),以及沒有任何問題。

響應頭類似請求頭也包含了一些有用的信息。響應的實體響應本身的 HTML 內容。頭與實體之間由回車換行的空行(CRLF)分開。

 

Socket 

    一個 socket 是一個網絡連接的端點,它使得一個應用可以從網絡讀與寫。在不同電腦上的二個應用軟件能夠通過收發字節流而彼此通信。要發一個信息到另一個應用程序,你需要知道它的IP地址,以及它的 socket 端口號。在 Java 中,一個 socket 用 java.net.Socket 來實現。

要創建一個 socket ,你可以用 Socket 類中幾個構建方法中的一個。其中一個接受主機名與端口號作爲參數:

new Socket("yahoo.com", 80);

一旦你成功地創建了一個 Socket 類的實例,你就可以用它去發送與接收字節流了。要發送字節流,你需要呼叫 Socket 類的 getOutputStream 方法來得到一個 java.io.OutputSteam 對象。要發送文本到遠程的程序,你通常需要從返回的 OutputStream 創建一個 java.io.PrintWriter 對象。要從連接的另一端接收字節流,你需要呼叫 Socket 類的 getInputStream 方法,它返回一個 java.io.InputStream 對象。

以下代碼創建一個可以與本地 HTTP 服務器通信的 socket (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;

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請求。

 

ServerSocket

    Socket類描述的是“客戶端”socket,當你需要創建與遠程服務程序連接時需要用到它。如果你想實現一個服務程序,如HTTP服務器或者FTP服務器,則需要另外不同的方法。這是因爲你的服務器必須隨時服務,它不知道什麼時候會有一個客戶端程序需要連接它。

因爲這個目的,你需要用到java.net.ServerSocket這個類,它是服務器端socket的一個實現。服務器端socket等待來自客戶端的連接請求。一旦它收到一個連接請求,它創建一個socket實例來與客戶端進行通信。

要創建服務器端socket,需要用到ServerSocket類提供的四個構建方法中的一個。你需要指定服務器端socket偵聽的IP地址與端口號。比較典型地,這個IP地址可以是127.0.0.1,意思是該服務器端socket偵聽的是本地機器。服務器端socket偵聽的IP地址指的是綁定地址。服務器端socket另一個重要的屬性是隊列長度,即它拒絕請求前所接受的最大請求排隊長度。

ServerSocket類的構建方法之一如下:

public ServerSocket(int port,int backLog,InetAddress bindingAddress);

對於這個構建方法,綁定地址必須是 java.net.InetAddress 類的實例。創建一個 InetAddress類的對象的簡單方法是呼叫其靜態方法 getByName,傳遞一個包含主機名的字符串。

InetAddress.getByName("127.0.0.1");

以下行的代碼創建了一個服務器端socket ,它偵聽本地機器的 8080 端口,限制隊列長度爲 1 。

new ServerSocket(8080,1,InetAddress.getByName("127.0.0.1"));

一旦有了一個 ServerSocket 實例,就可以通過呼叫其 accept 方法來讓它等待進來的鏈接請求。這個方法只有當接收到請求時才返回,它返回的是 Socket 類的實例。這個 Socket 對象就可以用來從客戶端應用程序發送與接收字節流,正如上節據說的那樣。實際上,accept 方法是本文例子中用到的唯一方法。

應用實例

我們的web服務器程序是 ex01.pyrmont 包的一部分,它包含三個類:HttpServer;Request;Response。

整個程序的入口(靜態main方法)是HttpServer類。它創建一個HttpServer的實例,並呼叫其await方法。正如名字表達的,await在一個特定的端口等待HTTP請求,處理它們,並返回響應給客戶端。它保持等待狀態,直到收到停止命令。(用方法名await代替wait,是因爲System中有一個重要的與線程相關的方法)

這個程序只從一個特定的目錄發送靜態資源,如 HTML 與圖像文件。它只支持沒有文件頭(如日期與 cookie)的情況。現在我們將在如下的幾節中看一下這三個類。

 

HttpServer

    HttpServer 實現了一個 web 服務器,它可以提供(serve)特定目錄及其子目錄下的靜態資源。這個特定的目錄由 public static final WEB_ROOT 指定。

WEB_ROOT 初始化如下:

public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";

代碼列表中包含了一具叫做 webroot 的目錄,裏面有一些靜態的資源,你可以用來測試本應用。

爲了請求一個靜態的資源,在瀏覽器的地址欄輸入如是地址:http://machinename:port/staticResources

如果你從不同的機器上發送請求到運行本應用的機器,則machinename是運行應用機器的機器名或IP地址,port是8080,staticResources是被請求的文件名稱,它必須包含在 WEB_ROOT目錄內。

例如,如果你用同一臺電腦來測試這個應用,你想要HttpServer發送index.html這個文件,用以下的地址:http://localhost:8080/index.html

要停止服務,只需要從瀏覽器發送一個停止(shutdown)命令,即在瀏覽器的地址欄輸入 host:port字段後,加上預先定義好的字符串。在我們的HttpServer類中,停止命令被定義爲SHUTDOWN,一個 static final變量。

private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

因此,要停止服務,你可以這樣:http://localhost:8080/SHUTDOWN

現在,讓我們看一下列表 1.1 中給出的 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 的循環。 
erverSocket = new ServerSocket(
port, 1, InetAddress.getByName("127.0.0.1"));...
// Loop waiting for a request
while (!shutdown) {

...
}

在 while 循環中的代碼,運行到 ServerSocket 的 accept 方法即停止。這個方法只有在 8080 端口接收到 HTTP 請求才返回:

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 對象並把 Request 對象設置給它,呼叫它的 sendStaticResource 方法: 
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();最後,await 方法關閉 Socket ,呼叫 Request 的 getUri 方法來檢查 HTTP 請求的地址是否是一個停止命令。如果是,則 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 對象,從而捕獲與客戶端的通信。呼叫 InputStream 對象的 read 方法中的一個就可以得到 HTTP 請求的原始數據。

Request 類有二個 public 方法 parse 與 getUri。parse 方法解析 HTTP 請求的原始數據。它做的事情不多--唯一它使之有效的信息是 HTTP 請求的 URI ,這個通過呼叫私有方法 parseUri 來獲得。parseUri 方法把 URI 作爲一個變量。調用 getUri 方法可以得到 HTTP 請求的 URI 。

要明白 parse 與 parseUri 的工作原理,你需要知道 HTTP 請求的結構,由 RFC2616 定義。

一個 HTTP 請求包括三個部分:Request line;Headers;Message body 。

現在,我們只需要關注 HTTP 請求的第一部分--請求行。請求行以方法記號開始,接着是請求的 URI 與協議版本,以回車換行符結束。請求行的元素之間以空格分開。例如,一個用 GET 方法的 index.html 文件的請求行如下:

GET /index.html HTTP/1.1

parse 方法從 socket 的 InputStream 傳遞給 Request 對象中讀取字節流,把這個字節數組存在緩衝裏。然後,它把 buffer 字節數組裏的字節放入叫做 request 的 StringBuffer 對象中,再把 StringBuffer 替換成 String 傳遞給 parseUri 方法。parse 方法的代碼如列表 1.2 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< buffer.length;j++)

request.append((char) buffer[j]);

}System.out.print(request.toString());

uri = parseUri(request.toString());

}

parseUri 方法查找請求行的第一個與第二個空格,從而從請求行獲得了 URI 。列表 1.3 展示了 parseUri 方法的代碼。

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 對象通過傳遞從 socket 獲得的 OutputStream 對象到 HttpServer 類的 await 方法而創建。

Response 類有二個公共方法 setRequest 與 setStaticResource 。setRequest 用來傳遞 Request 對象到 Response 對象。它比較簡單,代碼如列表 1.4 所示:

Listing 1.4. The Response class' setRequest method

public void setRequest(Request request) {

this.request = request;

}

sendStaticResource 方法用來發送靜態的資源,例如 HTML 文件。它的實現如列表 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" +"

File Not Found

";
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 對象 output 。就這樣,靜態資源的內容作爲原始數據被髮送到瀏覽器。 
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" +"File Not Found";
output.write(errorMessage.getBytes());

編譯與運行應用程序 
要編輯與運行本文的應用,首先你需要解壓源碼 zip 文件。直接解壓出來的目錄被稱爲工作目錄,它有三個子目錄:src/,classes/,lib/。要編譯應用,從工作目錄輸入如下命令:
 
javac -d . src/ex01/pyrmont/*.java
-d 選項把結果寫到當前目錄,而不是 src/ 目錄。
 
要運行應用,在當前工作目錄輸入如下命令:
 
java ex01.pyrmont.HttpServer測試這個應用,打開你的瀏覽器,在地址欄輸入如下地址:http://localhost:8080/index.html

你將在你的瀏覽器看到 MSIE 4.01; MSIE 4.01; Windows 98) 
Host: localhost:8080
Connection: Keep-Alive

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章