1. 程式人生 > 實用技巧 >HTTP請求和響應報文與簡單實現Java Http伺服器

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"); 實驗結果如下圖:

如果改為 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);
    }
}