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);
        }
    }
    
  • 相关阅读:
    微信登录
    Nginx负载均衡的优缺点
    elk 比较不错的博客
    Filebeat 5.x 日志收集器 安装和配置
    日志管理系统ELK6.2.3
    python3爬虫编码问题
    zabbix监控进程
    linux下查询进程占用的内存方法总结
    Ubuntu 16.04安装Elasticsearch,Logstash和Kibana(ELK)Filebeat
    ELK多种架构及优劣
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/java-simple-nio-http-server.html
Copyright © 2011-2022 走看看