1. 程式人生 > >HTTP學習與Web伺服器程式設計

HTTP學習與Web伺服器程式設計

這次的主題是查詢HTTP協議的相關資料,基於此編寫一個簡單的Web伺服器。
需要完成的幾大主要的要求有:
1)編寫一個簡單的Web伺服器;
2)實現的伺服器應能與標準的瀏覽器進行簡單的互動;
3)記錄瀏覽器與服務的互動過程;
4)利用HTML語言編寫網頁瀏覽器可通過編寫的Web伺服器正常訪問該網頁;
5)支援多使用者併發訪問;
6)擴充套件編寫的簡單Web伺服器,使瀏覽器能夠瀏覽Web上儲存的影象

一.瞭解http協議(參考百度百科)

HTTP是一個客戶端和伺服器端請求和應答的標準(TCP)。客戶端是終端使用者,伺服器端是網站。通過使用Web瀏覽器、網路爬蟲或者其它的工具,客戶端發起一個到伺服器上指定埠(預設埠為80)的HTTP請求。(我們稱這個客戶端)叫使用者代理,應答的伺服器上儲存著(一些)資源,比如HTML檔案和影象;(我們稱)這個應答伺服器為源伺服器。“客戶”與“伺服器”是一個相對的概念,只存在於一個特定的連線期間,即在某個連線中的客戶在另一個連線中可能作為伺服器。基於HTTP協議的客戶/伺服器模式的資訊交換過程,它分四個過程:建立連線、傳送請求資訊、傳送響應資訊、關閉連線。
HTTP協議是基於請求/響應正規化的。一個客戶機與伺服器建立連線後,傳送一個請求給伺服器,請求方式的格式為,統一資源識別符號、協議版本號,後邊是MIME資訊包括請求修飾符、客戶機資訊和可能的內容。伺服器接到請求後,給予相應的響應資訊,其格式為一個狀態行包括資訊的協議版本號、一個成功或錯誤的程式碼,後邊是MIME資訊包括伺服器資訊、實體資訊和可能的內容。
其實簡單說就是任何伺服器除了包括HTML檔案以外,還有一個HTTP駐留程式,用於響應使用者請求。你的瀏覽器是HTTP客戶,向伺服器傳送請求,當瀏覽器中輸入了一個開始檔案或點選了一個超級連結時,瀏覽器就向伺服器傳送了HTTP請求,此請求被送往由IP地址指定的URL。駐留程式接收到請求,在進行必要的操作後回送所要求的檔案。在這一過程中,在網路上傳送和接收的資料已經被分成一個或多個數據包,每個資料包包括:要傳送的資料;控制資訊,即告訴網路怎樣處理資料包。TCP/IP決定了每個資料包的格式。如果事先不告訴你,你可能不會知道資訊被分成用於傳輸和再重新組合起來的許多小塊。
HTTP報文由從客戶機到伺服器的請求和從伺服器到客戶機的響應構成。
請求報文格式為:請求行 - 通用資訊頭 - 請求頭 - 實體頭 - 報文主體。請求行以方法欄位開始,後面分別是 URL 欄位和 HTTP 協議版本欄位,並以 CRLF 結尾。SP 是分隔符。除了在最後的 CRLF 序列中 CF 和 LF 是必需的之外,其他都可以不要。有關通用資訊頭,請求頭和實體頭方面的具體內容可以參照相關檔案。
應答報文格式為:狀態行 - 通用資訊頭 - 響應頭 - 實體頭 - 報文主體。狀態碼元由3位數字組成,表示請求是否被理解或被滿足。原因分析是對原文的狀態碼作簡短的描述,狀態碼用來支援自動操作,而原因分析用來供使用者使用。客戶機無需用來檢查或顯示語法。有關通用資訊頭,響應頭和實體頭方面的具體內容可以參照相關檔案。
簡而言之,使用http就像我們打電話訂貨一樣,我們可以打電話給商家,告訴他我們需要什麼規格的商品,然後商家再告訴我們什麼商品有貨,什麼商品缺貨。這些,我們是通過電話線用電話聯絡(HTTP是通過TCP/IP),當然我們也可以通過傳真,只要商家那邊也有傳真。

二.建立簡單的web伺服器
我們將使用java語言,基於java.net.Socket和java.net.ServerSocket實現一個簡單的web伺服器。

首先我們來看一下整個程式的大致結構(使用的IDE為eclipse):
這裡寫圖片描述
有三個Java類,分別是伺服器類MyWebServer.java和需要在伺服器類裡呼叫的Request類和Responses類,分別用來處理接收到的http協議報文的解析工作和響應工作。在本工程目錄下建一個資料夾resource用來儲存所有的html檔案和圖片。

現在主要說說整個程式的思路:
1.建立一個ServerSocket物件;
2.呼叫ServerSocket物件的accept方法,等待連線,連線成功會返回一個Socket物件,否則一直阻塞等待;
3.從Socket物件中獲取InputStream和OutputStream位元組流,這兩個流分別對應request請求和response響應;
4.處理請求:讀取InputStream位元組流資訊,轉成字串形式,並解析
5.處理響應:根據解析出來的uri資訊,從WEB_ROOT目錄中尋找請求的資源資原始檔, 讀取資原始檔,並將其寫入到OutputStream位元組流中;
6.關閉Socket物件;
7.轉到步驟2,繼續等待連線請求;

(1)伺服器類

//MyWebServer.java
package homework3;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetAddress;  
import java.net.ServerSocket;  
import java.net.Socket;



public
class MyWebServer { public static int PORT=8888; public static final String WEB_ROOT="resource"; public static void main(String[] args) { System.out.println("開啟伺服器"); try { ServerSocket WebServer=null; WebServer = new ServerSocket(PORT,1,InetAddress.getByName("127.0.0.1")); while(true) //不斷迴圈監聽是否有新的請求,有的話啟動一個執行緒響應 { Socket Client=WebServer.accept(); new HttpConnectThread(Client).start(); } } catch (IOException e) { e.printStackTrace(); } } } class HttpConnectThread extends Thread { private Socket Client; public HttpConnectThread(Socket s) { Client=s; } public void run() { try { InputStream input=null; OutputStream output=null; input=Client.getInputStream(); output=Client.getOutputStream(); //從Socket物件中獲取InputStream和OutputStream位元組流, //這兩個流分別對應request請求和response響應; Request request=new Request(input); if(request.parse(Client)==1){ //處理請求:讀取InputStream位元組流資訊,轉成字串形式,並解析 Response response=new Response(output); response.setRequest(request); response.sendStaticResource(Client); } //處理響應:根據解析出來的資訊,從WEB_ROOT目錄中尋找請求的資源資原始檔, //讀取資原始檔,並將其寫入到OutputStream位元組流中; Client.close(); } catch (IOException e) { e.printStackTrace(); } } }

簡單的說一下這一塊:
先是定義了伺服器的埠port(8888)和基目錄webroot(resource),然後利用ServerSocket建立了一個本機上8888埠的伺服器。為了完成要求5)支援多使用者併發訪問,我們在這邊做了兩件事,首先用一個while迴圈不斷地監聽是否有對應本伺服器的請求,第二件事對於每一個請求訪問的ip地址,新建一個執行緒與其進行互動。所以while裡邊就是利用新建一個socket並且讓他WebServer.accept()去監聽別人的請求,沒聽到就阻塞在這裡一直監聽,聽到了new一個我們寫好的執行緒HttpConnectThread並且start它。

再來看看這個執行緒的run函式。首先利用socket的getInputStream()和getOutputStream()從Socket物件中獲取InputStream和OutputStream位元組流,這兩個流分別對應request請求和response響應。request處理請求主要工作為讀取InputStream位元組流資訊,轉成字串形式,並解析。Response處理響應會根據解析出來的資訊,從WEB_ROOT目錄中尋找請求的資原始檔,讀取資原始檔,並將其寫入到OutputStream位元組流中。需要說明的是這裡我使用if(request.parse(Client)==1)這句話是因為這裡出現的一個小bug,後邊我在說明request類再詳細說明。

(2)請求類

//request.java
package homework3;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.Socket;  

public class Request {  
    private InputStream input;  
    private String uri;  

    public Request(InputStream input){  
        this.input=input;  
    }  

    public int parse(Socket socket){  
        //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 (Exception 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()); 


        if(request.toString().split("\n")[0].contains("html")||request.toString().split("\n")[0].contains("jpg")){  
            return 1;
        }
        else{
            // 下面是由伺服器直接生成的主頁內容
            // 1、首先向瀏覽器輸出響應頭資訊
            PrintStream out;
            try {
                out = new PrintStream(socket.getOutputStream(), false, "GB2312");

                out.println("HTTP/1.1 200\r"); 
                out.println("Content_Type:text/html\r");
                out.println("");//報文頭和資訊之間要空一行
                // 2、輸出主頁資訊
                out.println(
"<HTML><BODY>"
+ "<center>"
+ "<H1>HTTP協議測試伺服器"
+ "</H1>"
+ "<form method='get' action='http://127.0.0.1:8888/'>username:<input type='text' name='username'/>password:<input type='text' name='password'/><input type='submit' value='GET測試'/></form><br/>"
+ "<form method='post' enctype='text/plain' action='http://127.0.0.1:8888/'>username:<input type='text' name='username'/>password:<input type='text' name='password'/><input type='submit' value='POST測試'/></form><br/>"
+ "</center>您提交的資料如下:<pre>" + request.toString()
+ "</pre></BODY></HTML>");
                out.flush();
                out.close();    
                System.out.println("msg.toString()   "+request.toString());
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        return 1;

    }  

    public 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;  
    }  

    public String getUri(){  
        return this.uri;  
    }  
}

這個程式我調了很久的bug,因為本來並沒有寫html檔案,而是像上邊程式中一樣直接通過printStream輸出流往socket的getOutputStream輸出html語句就可以出現對應的網頁,但是在顯示圖片的時候出現了坑爹的情況。首先使用絕對路徑的時候瀏覽器會報錯“Not allowed to load local resource”,究其原因是瀏覽器基於安全考慮不允許直接訪問。換成相對路徑吧,伺服器當前工作路徑下建一個resource放圖片,寫地址用/resource/xxx.jpg。好麼,圖片死活不出來,在瀏覽器按F12審查了半天也沒審查個所以然來,感覺就是傳過去的檔案type是text/html型別的而不是jpg型別的(但是最後改完可以顯示了我一看還是text/html型別),總之經歷了很久debug的絕望以後我換成了現在這種寫法,即要顯示圖片的話還是老老實實寫一個html頁面,如果是動態顯示記錄的瀏覽器與服務的互動過程(是的本程式你不知可以在console裡看,在瀏覽器也可以直接看),沒涉及圖片輸出,用我原來的想法。這就解釋了我為什麼要把require返回給伺服器的return分為1和0了,解析報文如果有請求圖片和html檔案,那麼返回1,呼叫response迴應對應檔案,否則返回0不呼叫response的內容,而是直接通過printStream輸出html語句動態顯示get和post過程中的報文協議。

(3)迴應請求類

//response.Java
package homework3;

import java.io.File;  
import java.io.FileInputStream;  
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;  

/** 
 * 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*1024;  
    Request request;  
    OutputStream output;  

    public Response(OutputStream output){  
        this.output=output;  
    }  

    public void setRequest(Request request){  
        this.request=request;  
    }  

    public void sendStaticResource(Socket socket)throws IOException{  
        byte[] bytes=new byte[BUFFER_SIZE];  
        FileInputStream fis=null;  
        try { 
                File file=new File(MyWebServer.WEB_ROOT,request.getUri());
                if(file.exists()){ 

                    fis=new FileInputStream(file);  
                    int ch=fis.read(bytes,0,BUFFER_SIZE); 
                    String header = "HTTP/1.1 200\r\n" + "Content-Type: text/html\r\n"+ 
                            "Content-Length: " + file.length() + "\r\n" + "\r\n";
                    output.write(header.getBytes());
                    while(ch!=-1){  

                        output.write(bytes, 0, BUFFER_SIZE);  
                        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) {  
            System.out.println(e.toString());  
        }finally{  
            if(fis!=null){
                output.close();
                fis.close();  
            }  
        }  
    }  
}

(2)(3)兩個類就不詳細一句句說明了,就著註釋應該很容易明白。

三.實現效果

點開MyWebServer檔案,點選執行,console會跳出:
這裡寫圖片描述
隨便開啟一個瀏覽器(我用的是chrome),輸入127.0.0.1:8888
這裡寫圖片描述
在get和post隨便輸入資料點選按鈕,可以觀察下邊的資料變化情況
OK~如果想要訪問圖片和伺服器裡的html檔案,輸入127.0.0.1:8888/index.html
這裡寫圖片描述
根據頁面內容點選連結可以訪問不同的檔案。當然回頭看一下console裡面記錄著所有的protocol資訊。
這裡寫圖片描述
這裡寫圖片描述
最後附上index.html和index2.html(圖片大家自己找啦)

<!DOCTYPE html>  
<html>  
<head>  
<meta charset="UTF-8">  
<title>Web伺服器</title>  
</head>  
<body>  
    <h1>This is yaozonghai's webserver</h1>
    <img src="pic\a3.jpg" /><br>
    <h5>This is a simple index.Your request will be sent to my WebServer</h5>
    <p>you can click <a href="index2.html"> more image</a> to scan picture on web<p>
    <img src="pic\a5.jpg" /><br>
    <p>you can click <a href="xxx"> observe the http protocol</a> to observe the http protocol<p>
    <h3>Thank you for using<h3>
</body>  
</html>  
<!DOCTYPE html>  
<html>  
<head>  
<meta charset="UTF-8">  
<title>LBJ</title>  
</head>  
<body>  
    <h1>so handsome the man is</h1> 
    <img src="pic\a1.jpg" />
    <img src="pic\a2.jpg" />
    <img src="pic\a3.jpg" />
    <img src="pic\a4.jpg" />
    <a href="index.html"> 返回</a>

</body>  
</html>  

That’s all,thank you!