zoukankan      html  css  js  c++  java
  • 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);
        }
    }
    
  • 相关阅读:
    HDU 1058 Humble Numbers
    HDU 1160 FatMouse's Speed
    HDU 1087 Super Jumping! Jumping! Jumping!
    HDU 1003 Max Sum
    HDU 1297 Children’s Queue
    UVA1584环状序列 Circular Sequence
    UVA442 矩阵链乘 Matrix Chain Multiplication
    DjangoModels修改后出现You are trying to add a non-nullable field 'download' to book without a default; we can't do that (the database needs something to populate existing rows). Please select a fix:
    opencv做的简单播放器
    c++文件流输入输出
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/java-simple-nio-http-server.html
Copyright © 2011-2022 走看看