ServerSocket實現超簡單HTTP服務器

1、相關知識簡介

HTTP協議

HTTP是常用的應用層協議之一,是面向文本的協議。HTTP報文傳輸基於TCP協議,TCP協議包含頭部與數據部分,而HTTP則是包含在TCP協議的數據部分,如下圖

這裏寫圖片描述

HTTP報文本質上是一個TCP報文,數據部分攜帶的內容爲HTTP報文,HTTP報文多數情況下是一串文本,當然也可能攜帶二進制信息。

HTTP報文

HTTP報文包含頭部和請求體,請求體內容可爲空。請求頭與請求體用單獨的空行分隔,即”\r\n”。HTTP頭部結構如下:

這裏寫圖片描述

這裏寫圖片描述

當報文爲請求報文時,第一行信息爲 {方法} {URI} {HTTP版本}
方法通常爲GET, POST,URI爲URL後面攜帶的參數信息,HTTP版本表示當前使用的HTTP版本。

當報文爲響應報文時,第一行的信息爲 {HTTP版本} 狀態
HTTP版本同上,下面是部分常見的狀態碼

狀態碼 英文名稱 含義
200 OK 請求成功
304 Not Modified 所請求的資源未修改
400 Bad Request 客戶端請求的語法錯誤,服務器無法理解
403 Forbidden 服務器理解請求客戶端的請求,但是拒絕執行此請求
404 Not Found 服務器無法根據客戶端的請求找到資源(網頁),常說的404錯誤就是指這個
405 Method Not Allowed 客戶端請求中的方法被禁止
502 Bad Gateway 充當網關或代理的服務器,從遠端服務器接收到了一個無效的請求


報文從第二行開始均爲 {字段名}: {字段值} 的格式。字段名通常是英文字母與”-“的組合,有常用的幾個,有時也可以使用自定義字段名,值得注意的是字段名最好不要包含空格,雖然我Postman上模擬沒問題,但在Chrome上試解析會出問題。

HTTP報文的請求體就是一段數據,沒有嚴格的格式限制,較爲隨意,但如果在頭部聲明Content-Type爲Multipart/form-data後就會有一定的格式規範,具體可以看看我之前寫的一篇文章
http://blog.csdn.net/kurozaki_kun/article/details/78646960

Socket

Socket是對TCP/IP的封裝,爲程序員提供了面向傳輸層及以上層的編程。Java中關於Socket的類主要是Socket,DatagramSocket,ServerSocket,還有NIO對應的類,這裏實現主要基於前三者。Socket能夠建立端到端的同通信。其實總結一句話,就是使用Socket能夠幫助程序員傳輸TCP/UDP報文。

2、基於Socket實現簡單的HTTP服務器

ServerSocket監聽端口

ServerSocket用於監聽特定端口,調用accept()方法會阻塞當前線程,直到接收到一個Socket,而我們需要處理所接收到的Socket。下面先寫出一個大致的框架

class ServerListeningThread extends Thread {

    private int bindPort;
    private ServerSocket serverSocket;

    public ServerListeningThread(int port) {
        this.bindPort = port;
    }

    @Override
    public void run() {
        try {
            serverSocket = new ServerSocket(bindPort);
            while (true) {
                Socket rcvSocket = serverSocket.accept();

                //單獨寫一個類,處理接收的Socket,類的定義在下面
                HttpRequestHandler request = new HttpRequestHandler(rcvSocket);
                request.handle();

                rcvSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //最後要確保以下把ServerSocket關閉掉
            if (serverSocket != null && !serverSocket.isClosed()) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class HttpRequestHandler {
    private Socket socket;

    public HttpRequestHandler(Socket socket) {
        this.socket = socket;
    }

    public void handle() throws IOException {
        //TODO 這裏寫處理接收到的socket的邏輯
    }
}

回送簡單的HTTP報文

接下來的關注點應該在如何處理Socket上,先從最簡單的開始做起,不管socket裏的是什麼,都一律只回復一個響應報文,上面的handle()方法處理應該如下


class HttpRequestHandler {
    private Socket socket;

    public HttpRequestHandler(Socket socket) {
        this.socket = socket;
    }

    public void handle() throws IOException {
        socket.getOutputStream().
                write(("HTTP/1.1 200 OK\r\n" +  //響應頭第一行
                        "Content-Type: text/html; charset=utf-8\r\n" +  //簡單放一個頭部信息
                        "\r\n" +  //這個空行是來分隔請求頭與請求體的
                        "<h1>這是響應報文</h1>\r\n").getBytes());
    }
}

然後來試試效果,在main函數調用一下,這裏監聽8888端口

public static void main(String[] args) {
    new ServerListeningThread(8888).start();
}

用瀏覽器打開 127.0.0.1:8888 或 localhost:8888,能夠顯示下面結果
這裏寫圖片描述

可以見到剛纔通過socket回送的響應報文被瀏覽器成解析了,紅色箭頭位置是自己添加的頭部信息。

讀取請求並回送

一個HTTP請求真正處理起來還是比較繁瑣的,這裏只介紹下簡單的情景,例如請求報文帶有POST參數,先讀取socket的數據,並控制檯輸出一下HTTP請求的報文是什麼樣的

class HttpRequestHandler {
    //此處代碼省略

    public void handle() throws IOException {
        //獲取輸入流,讀取數據
        StringBuilder builder = new StringBuilder();
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        char[] charBuf = new char[1024];
        int mark;
        while ((mark = isr.read(charBuf)) != -1) {
            builder.append(charBuf, 0, mark);
            if (mark < charBuf.length) {
                break;
            }
        }
        System.out.println(builder.toString());


        socket.getOutputStream().
                write(("HTTP/1.1 200 OK\r\n" +
                        "Content-Type: text/html; charset=utf-8\r\n" +
                        "\r\n" +
                        "<h1>這是響應報文</h1>\r\n").getBytes());
    }
}

使用postman向8888端口發送一個攜帶POST參數的HTTP請求,如下
這裏寫圖片描述

控制檯輸出結果爲
這裏寫圖片描述

其中三個提交的參數在body的表現形式爲 參數名=值,多個參數用&連接成字符串,該字符串佔一行。下面可以使用字符串操作將這些信息解析出來,並且將解析結果回送回去。

class HttpRequestHandler {
    //此處代碼省略...

    public void handle() throws IOException {

        StringBuilder builder = new StringBuilder();
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        char[] charBuf = new char[1024];
        int mark = -1;
        while ((mark = isr.read(charBuf)) != -1) {
            builder.append(charBuf, 0, mark);
            if (mark < charBuf.length) {
                break;
            }
        }
        if (mark == -1) {
            return;
        }

        Map<String, String> headers = new HashMap<>();
        Map<String, String> parameters = new HashMap<>();

        String[] splits = builder.toString().split("\r\n");
        int index = 1;

        //處理header
        while (splits[index].length() > 0) {
            String[] keyVal = splits[index].split(":");
            headers.put(keyVal[0], keyVal[1].trim());
            index++;
        }
        String body = splits[index + 1];
        String[] bodySplits = body.split("&");

        //處理body的參數
        for (String str : bodySplits) {
            String[] param = str.split("=");
            parameters.put(param[0], param[1]);
        }

        String respStr = "頭部信息\r\n";
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            respStr += "名稱: " + entry.getKey() + ", 值: " + entry.getValue() + "<br/>";
        }

        respStr += "\r\nbody信息\r\n";
        for (Map.Entry<String, String> entry : parameters.entrySet()) {
            respStr += "名稱: " + entry.getKey() + ", 值: " + entry.getValue() + "<br/>";
        }

        socket.getOutputStream().
                write(("HTTP/1.1 200 OK\r\n" +
                        "Content-Type: text/html; charset=utf-8\r\n" +
                        "\r\n" +
                        "<h1>這是響應報文</h1>\r\n" + respStr).getBytes());
    }
}

使用POST方法帶參訪問8888端口,其返回結果如下
這裏寫圖片描述

在這基礎上,還可以根據提交參數查詢數據庫等等操作,一個成熟的服務器實際上已經封裝好了如上的解析步驟,然後監聽主機的80端口(即HTTP默認端口),真正實現一個服務器要處理的情況遠比這裏講述的多,例如處理文件傳輸等等。

小結

這裏主要使用ServerSocket和Socket來實現,實際上還可以使用NIO的ServerSocketChannel和SocketChannel。服務器處理請求的步驟通常就是 監聽端口->收到請求->處理->響應請求,中間的處理會有多層的步驟。

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