HTTP请求和响应报文与简单实现Java Http服务器

报文结构

HTTP 报文包含以下三个部分:

  • 起始行
    报文的第一行是起始行,在请求报文中用来说明要做什么,而在响应报文中用来说明出现了什么情况。
  • 首部
    起始行后面有零个或多个首部字段。每个首部字段都包含一个名字和一个值,为了便于解析,两者之间用冒号(:)来分隔
    首部以一个空行结束。添加一个首部字段和添加新行一样简单。
  • 主体
    空行之后就是可选的报文主体了,其中包含了所有类型的数据。请求主体中包括了要发送给 Web 服务器的数据;响应主体中装载了要返回给客户端的数据。
    起始行和首部都是文本形式且都是结构化的,而主体不同,主体中可以包含任意的二进制数据(比如图片,视频,音轨,软件程序)。当然,主体中也可以包含文本。

HTTP 请求报文

  • 回车换行指代

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("
");
        // 响应头
        if (response.body != null && response.body.length() > 0) {
            builder.append("Content-Length:").append(response.body.length()).append("
");
            builder.append("Content-Type:text/html
");
        }
        if (response.headers != null) {
            String headStr = response.headers.entrySet().stream().map(e -> e.getKey() + ":" + e.getValue())
                    .collect(Collectors.joining("
"));
            if (!headStr.isEmpty()) {
                builder.append(headStr).append("
");
            }
        }
        // 首部以一个空行结束
        builder.append("
");
        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);
    }
}
原文地址:https://www.cnblogs.com/kendoziyu/p/java-simple-nio-http-server.html