HTTP請求和響應報文與簡單實現Java Http服務器


報文結構

HTTP 報文包含以下三個部分:

  • 起始行
    報文的第一行是起始行,在請求報文中用來說明要做什么,而在響應報文中用來說明出現了什么情況。
  • 首部
    起始行后面有零個或多個首部字段。每個首部字段都包含一個名字和一個值,為了便於解析,兩者之間用冒號(:)來分隔
    首部以一個空行結束。添加一個首部字段和添加新行一樣簡單。
  • 主體
    空行之后就是可選的報文主體了,其中包含了所有類型的數據。請求主體中包括了要發送給 Web 服務器的數據;響應主體中裝載了要返回給客戶端的數據。
    起始行和首部都是文本形式且都是結構化的,而主體不同,主體中可以包含任意的二進制數據(比如圖片,視頻,音軌,軟件程序)。當然,主體中也可以包含文本。

HTTP 請求報文

  • 回車換行指代 \r\n

HTTP 響應報文

Http協議處理流程

流程說明:

  1. 客戶端(瀏覽器)發起請求,並根據協議封裝請求頭與請求參數。
  2. 服務端接受連接
  3. 讀取請求數據包
  4. 將數據包解碼HttpRequest 對象
  5. 業務處理(異步),並將結果封裝成 HttpResponse 對象
  6. 基於協議編碼 HttpResponse
  7. 將編碼后的數據寫入管道

上一章博客 Java1.4從BIO模型發展到NIO模型
就已經介紹了如何實現一個簡單的 NIO 事件驅動的服務器,處理了包括接受連接、讀取數據、回寫數據的流程。本文就主要針對解碼和編碼進行細致的分析。

關鍵步驟

HttpResponse 交給 IO 線程負責回寫給客戶端

API:java.nio.channels.SelectionKey 1.4

  • Object attach(Object ob)
    將給定的對象附加到此鍵。稍后可以通過{@link #attachment()}檢索附件。
  • Object attachment()
    檢索當前附件。

通過這個 API 我們就可以將寫操作移出業務線程

service.submit(new Runnable() {
      @Override
      public void run() {
            HttpResponse response = new HttpResponse();
            if ("get".equalsIgnoreCase(request.method)) {
                  servlet.doGet(request, response);
            } else if ("post".equalsIgnoreCase(request.method)) {
                  servlet.doPost(request, response);
            }
            // 獲得響應
            key.interestOps(SelectionKey.OP_WRITE);
            key.attach(response);
//                socketChannel.register(key.selector(), SelectionKey.OP_WRITE, response);
            // 坑:異步喚醒
            key.selector().wakeup();
      }
});

值得注意的是,因為我選擇使用 select() 來遍歷鍵,因此需要在業務線程准備好 HttpResponse 后,立即喚醒 IO 線程。

使用 ByteArrayOutputStream 來裝緩沖區數據

private void read(SocketChannel socketChannel, ByteArrayOutputStream out) throws IOException {
      ByteBuffer buffer = ByteBuffer.allocate(1024);
      while (socketChannel.read(buffer) > 0) {
            buffer.flip(); // 切換到讀模式
            out.write(buffer.array());
            buffer.clear(); // 清理緩沖區
      }
}

一次可能不能讀取所有報文數據,所以用 ByteArrayOutputStream 來連接數據。

讀取到空報文,拋出NullPointerException

在處理讀取數據時讀取到空數據時,意外導致 decode 方法拋出 NullPointerException,所以屏蔽了空數據的情況

// 坑:瀏覽器空數據
if (out.size() == 0) {
      System.out.println("關閉連接:"+ socketChannel.getRemoteAddress());
      socketChannel.close();
      return;
}

長連接與短連接

通過 響應首部可以控制保持連接還是每次重新建立連接。

public void doGet(HttpRequest request, HttpResponse response) {
      System.out.println(request.url);
      response.body = "<html><h1>Hello World!</h1></html>";
      response.headers = new HashMap<>();
//        response.headers.put("Connection", "keep-alive");
      response.headers.put("Connection", "close");
}

加入關閉連接請求頭 response.headers.put("Connection", "close"); 實驗結果如下圖:

connection-close

如果改為 response.headers.put("Connection", "keep-alive"); 實驗結果如下圖:

總結

本文使用 java.nio.channels 中的類實現了一個簡陋的 Http 服務器。實現了網絡 IO 邏輯與業務邏輯分離,分別運行在 IO 線程和 業務線程池中。

  • HTTP 是基於 TCP 協議之上的半雙工通信協議,客戶端向服務端發起請求,服務端處理完成后,給出響應。
  • HTTP 報文主要由三部分構成:起始行,首部,主體。
    其中起始行是必須的,首部和主體都是非必須的。起始行和首部都采用文本格式且都是結構化的。主體部分既可以是二進制數據也可以是文本格式的數據。

參考代碼

  • 工具類 Code ,因為 sun.net.httpserver.Code 無法直接使用,所以拷貝一份出來使用。
  • HttpRequest & HttpResponse 實體類
public class HttpRequest {
    String method;  // 請求方法
    String url;     // 請求地址
    String version; // http版本
    Map<String, String> headers; // 請求頭
    String body;    // 請求主體
}

public class HttpResponse {
    String version; // http版本
    int code;       // 響應碼
    String status;  // 狀態信息
    Map<String, String> headers; // 響應頭
    String body;    // 響應數據
}
  • HttpServlet
public class HttpServlet {

    public void doGet(HttpRequest request, HttpResponse response) {
        System.out.println(request.url);
        response.body = "<html><h1>Hello World!</h1></html>";
    }

    public void doPost(HttpRequest request, HttpResponse response) {

    }
}
  • HttpServer 一個簡陋的 Http 服務器
public class HttpServer {

    final int port;
    private final Selector selector;
    private final HttpServlet servlet;
    ExecutorService service;

    /**
     * 初始化
     * @param port
     * @param servlet
     * @throws IOException
     */
    public HttpServer(int port, HttpServlet servlet) throws IOException {
        this.port = port;
        this.servlet = servlet;
        this.service = Executors.newFixedThreadPool(5);
        ServerSocketChannel channel = ServerSocketChannel.open();
        channel.configureBlocking(false);
        channel.bind(new InetSocketAddress(80));
        selector = Selector.open();
        channel.register(selector, SelectionKey.OP_ACCEPT);
    }

    /**
     * 啟動
     */
    public void start() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    poll(selector);
                } catch (IOException e) {
                    System.out.println("服務器異常退出...");
                    e.printStackTrace();
                }
            }
        }, "Selector-IO").start();
    }

    public static void main(String[] args) throws IOException {
        try {
            HttpServer server = new HttpServer(80, new HttpServlet());
            server.start();
            System.out.println("服務器啟動成功, 您現在可以訪問 http://localhost:" + server.port);
        } catch (IOException e) {
            System.out.println("服務器啟動失敗...");
            e.printStackTrace();
        }
        System.in.read();
    }

    /**
     * 輪詢鍵集
     * @param selector
     * @throws IOException
     */
    private void poll(Selector selector) throws IOException {
        while (true) {
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    handleAccept(key);
                } else if (key.isReadable()) {
                    handleRead(key);
                } else if (key.isWritable()) {
                    handleWrite(key);
                }
                iterator.remove();
            }
        }
    }

    private void handleRead(SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        // 1. 讀取數據
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        read(socketChannel, out);
        // 坑:瀏覽器空數據
        if (out.size() == 0) {
            System.out.println("關閉連接:"+ socketChannel.getRemoteAddress());
            socketChannel.close();
            return;
        }
        // 2. 解碼
        final HttpRequest request = decode(out.toByteArray());
        // 3. 業務處理
        service.submit(new Runnable() {
            @Override
            public void run() {
                HttpResponse response = new HttpResponse();
                if ("get".equalsIgnoreCase(request.method)) {
                    servlet.doGet(request, response);
                } else if ("post".equalsIgnoreCase(request.method)) {
                    servlet.doPost(request, response);
                }
                // 獲得響應
                key.interestOps(SelectionKey.OP_WRITE);
                key.attach(response);
                // 坑:異步喚醒
                key.selector().wakeup();
//                socketChannel.register(key.selector(), SelectionKey.OP_WRITE, response);
            }
        });

    }

    /**
     * 從緩沖區讀取數據並寫入 {@link ByteArrayOutputStream}
     * @param socketChannel
     * @param out
     * @throws IOException
     */
    private void read(SocketChannel socketChannel, ByteArrayOutputStream out) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (socketChannel.read(buffer) > 0) {
            buffer.flip(); // 切換到讀模式
            out.write(buffer.array());
            buffer.clear(); // 清理緩沖區
        }
    }

    /**
     * 解碼 Http 請求報文
     * @param array
     * @return
     */
    private HttpRequest decode(byte[] array) {
        try {
            HttpRequest request = new HttpRequest();
            ByteArrayInputStream inStream = new ByteArrayInputStream(array);
            InputStreamReader reader = new InputStreamReader(inStream);
            BufferedReader in = new BufferedReader(reader);

            // 解析起始行
            String firstLine = in.readLine();
            System.out.println(firstLine);
            String[] split = firstLine.split(" ");
            request.method = split[0];
            request.url = split[1];
            request.version = split[2];

            // 解析首部
            Map<String, String> headers = new HashMap<>();
            while (true) {
                String line = in.readLine();
                // 首部以一個空行結束
                if ("".equals(line.trim())) {
                    break;
                }
                String[] keyValue = line.split(":");
                headers.put(keyValue[0], keyValue[1]);
            }
            request.headers = headers;

            // 解析請求主體
            CharBuffer buffer = CharBuffer.allocate(1024);
            CharArrayWriter out = new CharArrayWriter();
            while (in.read(buffer) > 0) {
                buffer.flip();
                out.write(buffer.array());
                buffer.clear();
            }
            request.body = out.toString();
            return request;
        } catch (Exception e) {
            System.out.println("解碼 Http 失敗");
            e.printStackTrace();
        }
        return null;
    }

    private void handleWrite(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        HttpResponse response = (HttpResponse) key.attachment();

        // 編碼
        byte[] bytes = encode(response);
        channel.write(ByteBuffer.wrap(bytes));

        key.interestOps(SelectionKey.OP_READ);
        key.attach(null);
    }

    /**
     * http 響應報文編碼
     * @param response
     * @return
     */
    private byte[] encode(HttpResponse response) {
        StringBuilder builder = new StringBuilder();
        if (response.code == 0) {
            response.code = 200; // 默認成功
        }
        // 響應起始行
        builder.append("HTTP/1.1 ").append(response.code).append(" ").append(Code.msg(response.code)).append("\r\n");
        // 響應頭
        if (response.body != null && response.body.length() > 0) {
            builder.append("Content-Length:").append(response.body.length()).append("\r\n");
            builder.append("Content-Type:text/html\r\n");
        }
        if (response.headers != null) {
            String headStr = response.headers.entrySet().stream().map(e -> e.getKey() + ":" + e.getValue())
                    .collect(Collectors.joining("\r\n"));
            if (!headStr.isEmpty()) {
                builder.append(headStr).append("\r\n");
            }
        }
        // 首部以一個空行結束
        builder.append("\r\n");
        if (response.body != null) {
            builder.append(response.body);
        }
        return builder.toString().getBytes();
    }

    private void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        Selector selector = key.selector();

        SocketChannel socketChannel = serverSocketChannel.accept();
        System.out.println(socketChannel);
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM