Java模擬實現HTTP服務器項目實戰

一,HTTP 協議的工作過程

二、HTTP協議格式

1,抓包分析搜狗主頁

HTTP請求

首行: [方法] + [url] + [版本]

Header: 請求的屬性, 冒號分割的鍵值對;每組屬性之間使用\n分隔;遇到空行表示Header部 分結束 Body: 空行後面的內容都是Body.

Body允許為空字符串. 如果Body存在, 則在Header中會有 一個Content-Length屬性來標識Body的長度

HTTP響應

首行: [版本號] + [狀態碼] + [狀態碼解釋]

Header: 請求的屬性, 冒號分割的鍵值對;每組屬性之間使用\n分隔;遇到空行表示Header部 分結束

Body: 空行後面的內容都是Body. Body允許為空字符串. 如果Body存在, 則在Header中會有 一個Content-Length屬性來標識Body的長度; 如果服務器返回瞭一個html頁面, 那麼html頁 面內容就是在body中

2,協議格式總結

三、版本V1

        實現一個最簡單的 HTTP 服務器.

        在這個版本中, 我們隻是簡單解析 GET 請求, 並根據請求的路徑來構造出不同的響應.

        路徑為 /200, 返回一個 "歡迎頁面".

        路徑為 /404, 返回一個 "沒有找到" 的頁面.

        路徑為 /302, 重定向到其他的頁面

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class HttpServerV1 {
    // HTTP 底層要基於 TCP 來實現. 需要按照 TCP 的基本格式來先進行開發.
    private ServerSocket serverSocket = null;
 
    public HttpServerV1(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
 
    public void start() throws IOException {
        System.out.println("服務器啟動");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            // 1. 獲取連接
            Socket clientSocket = serverSocket.accept();
            // 2. 處理連接(使用短連接的方式實現)
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    process(clientSocket);
                }
            });
        }
    }
 
    private void process(Socket clientSocket) {
        // 由於 HTTP 協議是文本協議, 所以仍然使用字符流來處理.
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
            // 下面的操作都要嚴格按照 HTTP 協議來進行操作.
            // 1. 讀取請求並解析
            //  a) 解析首行, 三個部分使用空格切分
            String firstLine = bufferedReader.readLine();
            String[] firstLineTokens = firstLine.split(" ");
            String method = firstLineTokens[0];
            String url = firstLineTokens[1];
            String version = firstLineTokens[2];
            //  b) 解析 header, 按行讀取, 然後按照冒號空格來分割鍵值對
            Map<String, String> headers = new HashMap<>();
            String line = "";
            // readLine 讀取的一行內容, 是會自動去掉換行符的. 對於空行來說, 去掉瞭換行符, 就變成空字符串
            while ((line = bufferedReader.readLine()) != null && line.length() != 0) {
                // 不能使用 : 來切分. 像 referer 字段, 裡面的內容是可能包含 : .
                String[] headerTokens = line.split(": ");
                headers.put(headerTokens[0], headerTokens[1]);
            }
            //  c) 解析 body (暫時先不考慮)
            // 請求解析完畢, 加上一個日志, 觀察請求的內容是否正確.
            System.out.printf("%s %s %s\n", method, url, version);
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
            System.out.println();
            // 2. 根據請求計算響應
            // 不管是啥樣的請求, 咱們都返回一個 hello 這樣的 html
            String resp = "";
            if (url.equals("/200")) {
                bufferedWriter.write(version + " 200 OK\n");
                resp = "<h1>hello</h1>";
            } else if (url.equals("/404")) {
                bufferedWriter.write(version + " 404 Not Found\n");
                resp = "<h1>not found</h1>";
            } else if (url.equals("/302")) {
                bufferedWriter.write(version + " 303 Found\n");
                bufferedWriter.write("Location: http://www.sogou.com\n");
                resp = "";
            } else {
                bufferedWriter.write(version + " 200 OK\n");
                resp = "<h1>default</h1>";
            }
            // 3. 把響應寫回到客戶端
            bufferedWriter.write("Content-Type: text/html\n");
            bufferedWriter.write("Content-Length: " + resp.getBytes().length + "\n"); // 此處的長度, 不能寫成 resp.length(), 得到的是字符的數目, 而不是字節的數目
            bufferedWriter.write("\n");
            bufferedWriter.write(resp);
            // 此處這個 flush 就算沒有, 問題也不大. 緊接著
            // bufferedWriter 對象就要被關閉瞭. close 時就會自動觸發刷新操作.
            bufferedWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 
    public static void main(String[] args) throws IOException {
        HttpServerV1 server = new HttpServerV1(9090);
        server.start();
    }
}

四、版本V2

        在這個版本中, 我們隻是簡單解析 GET 請求, 並根據請求的路徑來構造出不同的響應.

         在版本1 的基礎上, 我們做出一些改進:

        把解析請求和構造響應的代碼提取成單獨的類.

        能夠把 URL 中的 query string 解析成鍵值對.

        能夠給瀏覽器返回 Cookie.

1,創建 HttpRequest 類

        對照著 HTTP 請求的格式, 創建屬性: method, url, version, headers.

        創建 patameters, 用於存放 query string 的解析結果.

        創建一個靜態方法 build, 用來完成解析 HTTP 請求的過程.

        從 socket 中讀取數據的時候註意設置字符編碼方式

        創建一系列 getter 方法獲取到請求中的屬性.

        單獨寫一個方法 parseKV 用來解析 query string

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
 
// 表示一個 HTTP 請求, 並負責解析.
public class HttpRequest {
    private String method;
    // /index.html?a=10&b=20
    private String url;
    private String version;
    private Map<String, String> headers = new HashMap<>();
    private Map<String, String> parameters = new HashMap<>();
 
    // 請求的構造邏輯, 也使用工廠模式來構造.
    // 此處的參數, 就是從 socket 中獲取到的 InputStream 對象
    // 這個過程本質上就是在 "反序列化"
    public static HttpRequest build(InputStream inputStream) throws IOException {
        HttpRequest request = new HttpRequest();
        // 此處的邏輯中, 不能把 bufferedReader 寫到 try ( ) 中.
        // 一旦寫進去之後意味著 bufferReader 就會被關閉, 會影響到 clientSocket 的狀態.
        // 等到最後整個請求處理完瞭, 再統一關閉
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        // 此處的 build 的過程就是解析請求的過程.
        // 1. 解析首行
        String firstLine = bufferedReader.readLine();
        String[] firstLineTokens = firstLine.split(" ");
        request.method = firstLineTokens[0];
        request.url = firstLineTokens[1];
        request.version = firstLineTokens[2];
        // 2. 解析 url 中的參數
        int pos = request.url.indexOf("?");
        if (pos != -1) {
            // 看看 url 中是否有 ? . 如果沒有, 就說明不帶參數, 也就不必解析瞭
            // 此處的 parameters 是希望包含整個 參數 部分的內容
            // pos 表示 ? 的下標
            // /index.html?a=10&b=20
            // parameters 的結果就相當於是 a=10&b=20
            String parameters = request.url.substring(pos + 1);
            // 切分的最終結果, key a, value 10; key b, value 20;
            parseKV(parameters, request.parameters);
        }
        // 3. 解析 header
        String line = "";
        while ((line = bufferedReader.readLine()) != null && line.length() != 0) {
            String[] headerTokens = line.split(": ");
            request.headers.put(headerTokens[0], headerTokens[1]);
        }
        // 4. 解析 body (暫時先不考慮)
        return request;
    }
 
    private static void parseKV(String input, Map<String, String> output) {
        // 1. 先按照 & 切分成若幹組鍵值對
        String[] kvTokens = input.split("&");
        // 2. 針對切分結果再分別進行按照 = 切分, 就得到瞭鍵和值
        for (String kv : kvTokens) {
            String[] result = kv.split("=");
            output.put(result[0], result[1]);
        }
    }
 
    // 給這個類構造一些 getter 方法. (不要搞 setter).
    // 請求對象的內容應該是從網絡上解析來的. 用戶不應該修改.
    public String getMethod() {
        return method;
    }
 
    public String getUrl() {
        return url;
    }
 
    public String getVersion() {
        return version;
    }
 
    // 此處的 getter 手動寫, 自動生成的版本是直接得到整個 hash 表.
    // 而我們需要的是根據 key 來獲取值.
    public String getHeader(String key) {
        return headers.get(key);
    }
 
    public String getParameter(String key) {
        return parameters.get(key);
    }
 
    @Override
    public String toString() {
        return "HttpRequest{" +
                "method='" + method + '\'' +
                ", url='" + url + '\'' +
                ", version='" + version + '\'' +
                ", headers=" + headers +
                ", parameters=" + parameters +
                '}';
    }
}

2,創建 HttpResponse 類

        根據 HTTP 響應, 創建屬性: version, status, message, headers, body

        另外創建一個 OutputStream, 用來關聯到 Socket 的 OutputStream.

        往 socket 中寫入數據的時候註意指定字符編碼方式.

        創建一個靜態方法 build, 用來構造 HttpResponse 對象.

        創建一系列 setter 方法, 用來設置 HttpResponse 的屬性.

        創建一個 flush 方法, 用於最終把數據寫入 OutputStream

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;
 
// 表示一個 HTTP 響應, 負責構造
// 進行序列化操作
public class HttpResponse {
    private String version = "HTTP/1.1";
    private int status;     // 狀態碼
    private String message; // 狀態碼的描述信息
    private Map<String, String> headers = new HashMap<>();
    private StringBuilder body = new StringBuilder(); // 方便一會進行拼接.
    // 當代碼需要把響應寫回給客戶端的時候, 就往這個 OutputStream 中寫就好瞭.
    private OutputStream outputStream = null;
 
    public static HttpResponse build(OutputStream outputStream) {
        HttpResponse response = new HttpResponse();
        response.outputStream = outputStream;
        // 除瞭 outputStream 之外, 其他的屬性的內容, 暫時都無法確定. 要根據代碼的具體業務邏輯
        // 來確定. (服務器的 "根據請求並計算響應" 階段來進行設置的)
        return response;
    }
 
    public void setVersion(String version) {
        this.version = version;
    }
 
    public void setStatus(int status) {
        this.status = status;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
 
    public void setHeader(String key, String value) {
        headers.put(key, value);
    }
 
    public void writeBody(String content) {
        body.append(content);
    }
 
    // 以上的設置屬性的操作都是在內存中倒騰.
    // 還需要一個專門的方法, 把這些屬性 按照 HTTP 協議 都寫到 socket 中.
    public void flush() throws IOException {
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write(version + " " + status + " " + message + "\n");
        headers.put("Content-Length", body.toString().getBytes().length + "");
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            bufferedWriter.write(entry.getKey() + ": " + entry.getValue() + "\n");
        }
        bufferedWriter.write("\n");
        bufferedWriter.write(body.toString());
        bufferedWriter.flush();
    }
}

3,創建 HttpServer 類

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class HttpServerV2 {
    private ServerSocket serverSocket = null;
 
    public HttpServerV2(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
 
    public void start() throws IOException {
        System.out.println("服務器啟動");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = serverSocket.accept();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    process(clientSocket);
                }
            });
        }
    }
 
    public void process(Socket clientSocket) {
        try {
            // 1. 讀取並解析請求
            HttpRequest request = HttpRequest.build(clientSocket.getInputStream());
            System.out.println("request: " + request);
            HttpResponse response = HttpResponse.build(clientSocket.getOutputStream());
            response.setHeader("Content-Type", "text/html");
            // 2. 根據請求計算響應
            if (request.getUrl().startsWith("/200")) {
                response.setStatus(200);
                response.setMessage("OK");
                response.writeBody("<h1>hello</h1>");
            } else if (request.getUrl().startsWith("/add")) {
                // 這個邏輯要根據參數的內容進行計算
                // 先獲取到 a 和 b 兩個參數的值
                String aStr = request.getParameter("a");
                String bStr = request.getParameter("b");
                // System.out.println("a: " + aStr + ", b: " + bStr);
                int a = Integer.parseInt(aStr);
                int b = Integer.parseInt(bStr);
                int result = a + b;
                response.setStatus(200);
                response.setMessage("OK");
                response.writeBody("<h1> result = " + result + "</h1>");
            } else if (request.getUrl().startsWith("/cookieUser")) {
                response.setStatus(200);
                response.setMessage("OK");
                // HTTP 的 header 中允許有多個 Set-Cookie 字段. 但是
                // 此處 response 中使用 HashMap 來表示 header 的. 此時相同的 key 就覆蓋
                response.setHeader("Set-Cookie", "user=zhangsan");
                response.writeBody("<h1>set cookieUser</h1>");
            } else if (request.getUrl().startsWith("/cookieTime")) {
                response.setStatus(200);
                response.setMessage("OK");
                // HTTP 的 header 中允許有多個 Set-Cookie 字段. 但是
                // 此處 response 中使用 HashMap 來表示 header 的. 此時相同的 key 就覆蓋
                response.setHeader("Set-Cookie", "time=" + (System.currentTimeMillis() / 1000));
                response.writeBody("<h1>set cookieTime</h1>");
            } else {
                response.setStatus(200);
                response.setMessage("OK");
                response.writeBody("<h1>default</h1>");
            }
            // 3. 把響應寫回到客戶端
            response.flush();
        } catch (IOException | NullPointerException e) {
            e.printStackTrace();
        } finally {
            try {
                // 這個操作會同時關閉 getInputStream 和 getOutputStream 對象
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws IOException {
        HttpServerV2 server = new HttpServerV2(9090);
        server.start();
    }
}

強化理解Cookie

Cookie就是一個字符串(裡面的內容由程序員自己決定)

Cookie從服務器來,服務器會在header中引入一個Set-Cookie字段,對應的值就會保存在瀏覽器中

Cookie按照域名/地址來存,每個域名/地址有自己的Cookie

Cookei在後續訪問相同的域名/地址的請求,就會自動帶上Cookie,服務器感知到這個Cookie之後就可以在服務器端進行一些邏輯處理

五、版本V3

在版本 2 的基礎上, 再做出進一步的改進.

解析請求中的 Cookie, 解析成鍵值對

解析請求中的 body, 按照 x-www-form-urlencoded 的方式解析.

根據請求方法, 分別調用 doGet / doPost

能夠返回指定的靜態頁面. 實現簡單的會話機制.

1. 創建 HttpRequest 類

屬性中新增瞭 cookies 和 body

新增一個方法 parseCookie, 在解析 header 完成後解析

cookie 新增瞭解析 body 的流程.

import javax.print.attribute.standard.RequestingUserName;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
 
public class HttpRequest {
    private String method;
    private String url;
    private String version;
    private Map<String, String> headers = new HashMap<>();
    // url 中的參數和 body 中的參數都放到這個 parameters hash 表中.
    private Map<String, String> parameters = new HashMap<>();
    private Map<String, String> cookies = new HashMap<>();
    private String body;
 
    public static HttpRequest build(InputStream inputStream) throws IOException {
        HttpRequest request = new HttpRequest();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        // 1. 處理首行
        String firstLine = bufferedReader.readLine();
        String[] firstLineTokens = firstLine.split(" ");
        request.method = firstLineTokens[0];
        request.url = firstLineTokens[1];
        request.version = firstLineTokens[2];
        // 2. 解析 url
        int pos = request.url.indexOf("?");
        if (pos != -1) {
            String queryString = request.url.substring(pos + 1);
            parseKV(queryString, request.parameters);
        }
        // 3. 循環處理 header 部分
        String line = "";
        while ((line = bufferedReader.readLine()) != null && line.length() != 0) {
            String[] headerTokens = line.split(": ");
            request.headers.put(headerTokens[0], headerTokens[1]);
        }
        // 4. 解析 cookie
        String cookie = request.headers.get("Cookie");
        if (cookie != null) {
            // 把 cookie 進行解析
            parseCookie(cookie, request.cookies);
        }
        // 5. 解析 body
        if ("POST".equalsIgnoreCase(request.method)
                || "PUT".equalsIgnoreCase(request.method)) {
            // 這兩個方法需要處理 body, 其他方法暫時不考慮
            // 需要把 body 讀取出來.
            // 需要先知道 body 的長度. Content-Length 就是幹這個的.
            // 此處的長度單位是 "字節"
            int contentLength = Integer.parseInt(request.headers.get("Content-Length"));
            // 註意體會此處的含義~~
            // 例如 contentLength 為 100 , body 中有 100 個字節.
            // 下面創建的緩沖區長度是 100 個 char (相當於是 200 個字節)
            // 緩沖區不怕長. 就怕不夠用. 這樣創建的緩沖區才能保證長度管夠~~
            char[] buffer = new char[contentLength];
            int len = bufferedReader.read(buffer);
            request.body = new String(buffer, 0, len);
            // body 中的格式形如: username=tanglaoshi&password=123
            parseKV(request.body, request.parameters);
        }
        return request;
    }
 
    private static void parseCookie(String cookie, Map<String, String> cookies) {
        // 1. 按照 分號空格 拆分成多個鍵值對
        String[] kvTokens = cookie.split("; ");
        // 2. 按照 = 拆分每個鍵和值
        for (String kv : kvTokens) {
            String[] result = kv.split("=");
            cookies.put(result[0], result[1]);
        }
    }
 
    private static void parseKV(String queryString, Map<String, String> parameters) {
        // 1. 按照 & 拆分成多個鍵值對
        String[] kvTokens = queryString.split("&");
        // 2. 按照 = 拆分每個鍵和值
        for (String kv : kvTokens) {
            String[] result = kv.split("=");
            parameters.put(result[0], result[1]);
        }
    }
 
    public String getMethod() {
        return method;
    }
 
    public String getUrl() {
        return url;
    }
 
    public String getVersion() {
        return version;
    }
 
    public String getBody() {
        return body;
    }
 
    public String getParameter(String key) {
        return parameters.get(key);
    }
 
    public String getHeader(String key) {
        return headers.get(key);
    }
 
    public String getCookie(String key) {
        return cookies.get(key);
    }
}

2,創建 HttpResponse 類

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;
 
public class HttpResponse {
    private String version = "HTTP/1.1";
    private int status;
    private String message;
    private Map<String, String> headers = new HashMap<>();
    private StringBuilder body = new StringBuilder();
    private OutputStream outputStream = null;
 
    public static HttpResponse build(OutputStream outputStream) {
        HttpResponse response = new HttpResponse();
        response.outputStream = outputStream;
        return response;
    }
 
    public void setVersion(String version) {
        this.version = version;
    }
 
    public void setStatus(int status) {
        this.status = status;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
 
    public void setHeader(String key, String value) {
        headers.put(key, value);
    }
 
    public void writeBody(String content) {
        body.append(content);
    }
 
    public void flush() throws IOException {
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write(version + " " + status + " " + message + "\n");
        headers.put("Content-Length", body.toString().getBytes().length + "");
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            bufferedWriter.write(entry.getKey() + ": " + entry.getValue() + "\n");
        }
        bufferedWriter.write("\n");
        bufferedWriter.write(body.toString());
        bufferedWriter.flush();
    }
}

3,創建 HttpServer 類

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class HttpServerV3 {
    static class User {
        // 保存用戶的相關信息
        public String userName;
        public int age;
        public String school;
    }
 
    private ServerSocket serverSocket = null;
    // session 會話. 指的就是同一個用戶的一組訪問服務器的操作, 歸類到一起, 就是一個會話.
    // 記者來采訪你, 記者問的問題就是一個請求, 你回答的內容, 就是一個響應. 一次采訪過程中
    // 涉及到很多問題和回答(請求和響應), 這一組問題和回答, 就可以稱為是一個 "會話" (整個采訪的過程)
    // sessions 中就包含很多會話. (每個鍵值對就是一個會話)
    private HashMap<String, User> sessions = new HashMap<>();
 
    public HttpServerV3(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
 
    public void start() throws IOException {
        System.out.println("服務器啟動");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = serverSocket.accept();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    process(clientSocket);
                }
            });
        }
    }
 
    public void process(Socket clientSocket) {
        // 處理核心邏輯
        try {
            // 1. 讀取請求並解析
            HttpRequest request = HttpRequest.build(clientSocket.getInputStream());
            HttpResponse response = HttpResponse.build(clientSocket.getOutputStream());
            // 2. 根據請求計算響應
            // 此處按照不同的 HTTP 方法, 拆分成多個不同的邏輯
            if ("GET".equalsIgnoreCase(request.getMethod())) {
                doGet(request, response);
            } else if ("POST".equalsIgnoreCase(request.getMethod())) {
                doPost(request, response);
            } else {
                // 其他方法, 返回一個 405 這樣的狀態碼
                response.setStatus(405);
                response.setMessage("Method Not Allowed");
            }
            // 3. 把響應寫回到客戶端
            response.flush();
        } catch (IOException | NullPointerException e) {
            e.printStackTrace();
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 
    private void doGet(HttpRequest request, HttpResponse response) throws IOException {
        // 1. 能夠支持返回一個 html 文件.
        if (request.getUrl().startsWith("/index.html")) {
            String sessionId = request.getCookie("sessionId");
            User user = sessions.get(sessionId);
            if (sessionId == null || user == null) {
                // 說明當前用戶尚未登陸, 就返回一個登陸頁面即可.
 
                // 這種情況下, 就讓代碼讀取一個 index.html 這樣的文件.
                // 要想讀文件, 需要先知道文件路徑. 而現在隻知道一個 文件名 index.html
                // 此時這個 html 文件所屬的路徑, 可以自己來約定(約定某個 d:/...) 專門放 html .
                // 把文件內容寫入到響應的 body 中
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeader("Content-Type", "text/html; charset=utf-8");
                InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                // 按行讀取內容, 把數據寫入到 response 中
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    response.writeBody(line + "\n");
                }
                bufferedReader.close();
            } else {
                // 用戶已經登陸, 無需再登陸瞭.
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeader("Content-Type", "text/html; charset=utf-8");
                response.writeBody("<html>");
                response.writeBody("<div>" + "您已經登陸瞭! 無需再次登陸! 用戶名: " + user.userName + "</div>");
                response.writeBody(+ user.age + "</div>");
                response.writeBody("<div>" + user.school + "</div>");
                response.writeBody("</html>");
            }
        }
    }
 
    private void doPost(HttpRequest request, HttpResponse response) {
        // 2. 實現 /login 的處理
        if (request.getUrl().startsWith("/login")) {
            // 讀取用戶提交的用戶名和密碼
            String userName = request.getParameter("username");
            String password = request.getParameter("password");
//            System.out.println("userName: " + userName);
//            System.out.println("password: " + password);
            // 登陸邏輯就需要驗證用戶名密碼是否正確.
            // 此處為瞭簡單, 咱們把用戶名和密碼在代碼中寫死瞭.
            // 更科學的處理方式, 應該是從數據庫中讀取用戶名對應的密碼, 校驗密碼是否一致.
            if ("zhangsan".equals(userName) && "123".equals(password)) {
                // 登陸成功
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeader("Content-Type", "text/html; charset=utf-8");
                // 原來登陸成功, 是給瀏覽器寫瞭一個 cookie, cookie 中保存的是用戶的用戶名.
                // response.setHeader("Set-Cookie", "userName=" + userName);
 
                // 現有的對於登陸成功的處理. 給這次登陸的用戶分配瞭一個 session
                // (在 hash 中新增瞭一個鍵值對), key 是隨機生成的. value 就是用戶的身份信息
                // 身份信息保存在服務器中, 此時也就不再有泄露的問題瞭
                // 給瀏覽器返回的 Cookie 中隻需要包含 sessionId 即可
                String sessionId = UUID.randomUUID().toString();
                User user = new User();
                user.userName = "zhangsan";
                user.age = 20;
                user.school = "北京大學";
                sessions.put(sessionId, user);
                response.setHeader("Set-Cookie", "sessionId=" + sessionId);
 
                response.writeBody("<html>");
                response.writeBody("<div>歡迎您! " + userName + "</div>");
                response.writeBody("</html>");
            } else {
                // 登陸失敗
                response.setStatus(403);
                response.setMessage("Forbidden");
                response.setHeader("Content-Type", "text/html; charset=utf-8");
                response.writeBody("<html>");
                response.writeBody("<div>登陸失敗</div>");
                response.writeBody("</html>");
            }
        }
    }
 
    public static void main(String[] args) throws IOException {
        HttpServerV3 serverV3 = new HttpServerV3(9090);
        serverV3.start();
    }
}

4,insex.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>登錄頁面</title>
</head>
<body>
    <form method="post" action="/login">
        <div style="margin-bottom: 5px">
            <input type="text" name="username" placeholder="請輸入名字">
        </div>
        <div style="margin-bottom: 5px">
            <input type="password" name="password" placeholder="請輸入密碼">
        </div>
        <div>
            <input type="submit" value="登錄">
        </div>
 
    </form>
</body>
</html>

 請求中沒有Cookie

  響應中帶有Cookie字段,此時瀏覽器就會帶有Cookie

到此這篇關於Java模擬實現HTTP服務器項目實戰的文章就介紹到這瞭,更多相關Java HTTP服務器內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: