zoukankan      html  css  js  c++  java
  • NIO实践-HTTP交互实现暨简版Tomcat交互内核

      今天就NIO实现简单的HTTP交互做一下笔记,进而来加深Tomcat源码印象。

    一、关于HTTP

      1、HTTP的两个显著特点,HTTP是一种可靠的超文本传输协议

        第一、实际中,浏览器作为客户端,每次访问,必须明确指定IP、PORT。这是因为,HTTP协议底层传输就是使用的TCP方式。

        第二、HTTP协议作为一种规范,简单理解,首先,它传输的是文本(即字符串,这个是区别于二级制数据的)。其次,他对文本的格式是有要求的。

      2、HTTP约定的报文格式

        对于以下报文格式,我们只需要对拿到的数据,进行readLine,然后做基于换行回车空格判断切割等,就能拿到所有信息。

        

       

    二、系统架构

      基于第一节的结论,我们就能启动NIO作为服务端,然后用浏览器来发起客户端接入、发送数据,然后服务端回执。浏览器显示回执。其中,浏览器内核持有一个客户端SocketChannel,并且会自动维护其事件监听。并且会自动按照HTTP协议报文格式来解析服务端返回的报文,并自动渲染。所以,我们只需要关注服务端,这里涉及一下几个步骤:

      <1>、接收浏览器SocketChannel发送的数据。

      <2>、解码:进行请求报文解析。

      <3>、编码:计算响应数据,并将响应数据封装为HTTP协议格式。

      <4>、写入SocketChannel,即发送给浏览器。

      

     三、服务初始化

     1、服务器实例声明

      我们使用NO作为服务端,所以端口、多路复用器这些必不可少。与此同时,我们需要一个线程池去专门进行业务处理,其中具体的业务处理交给HttpServlet。

     1 public class SimpleHttpServer {
     2     // 服务端口
     3     private int port;
     4     // 处理器
     5     private HttpServlet servlet;
     6     // 轮询器
     7     private final Selector selector;
     8     // 启停标识
     9     private volatile boolean run = false;
    10     // 需要注册的Channel,避免与轮询器产生死锁
    11     private Set<SocketChannel> allConnections = new HashSet<>();
    12     // 执行业务线程池
    13     private ExecutorService executor = Executors.newFixedThreadPool(5);
    14     
    15     public SimpleHttpServer(int port, HttpServlet servlet) throws IOException {
    16         this.port = port;
    17         this.servlet = servlet;
    18         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    19         selector = Selector.open();
    20         serverSocketChannel.bind(new InetSocketAddress(port));
    21         serverSocketChannel.configureBlocking(false);
    22         // 一旦初始化就开始监听客户端接入事件
    23         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    24     }
    25 }

    2、业务处理HttpServlet的细节

    HttpServlet

    1 public interface HttpServlet {
    2     void doGet(Request request, Response response);
    3     void doPost(Request request, Response response);
    4 }

    Request

    1 public class Request {
    2     Map<String, String> heads;
    3     String url;
    4     String method;
    5     String version;
    6     //请求内容
    7     String body;    
    8     Map<String, String> params;
    9 }

    Response

    1 public class Response {
    2     Map<String, String> headers;
    3     // 状态码
    4     int code;
    5     //返回结果
    6     String body; 
    7 }

    3、编解码相关

    编码

     1 //编码Http 服务
     2 private byte[] encode(Response response) {
     3     StringBuilder builder = new StringBuilder(512);
     4     builder.append("HTTP/1.1 ").append(response.code).append(Code.msg(response.code)).append("
    ");
     5     if (response.body != null && response.body.length() != 0) {
     6         builder.append("Content-Length: ")
     7                 .append(response.body.length()).append("
    ")
     8                 .append("Content-Type: text/html
    ");
     9     }
    10     if (response.headers != null) {
    11         String headStr = response.headers.entrySet().stream().map(e -> e.getKey() + ":" + e.getValue())
    12                 .collect(Collectors.joining("
    "));
    13         builder.append(headStr + "
    ");
    14     }
    15     builder.append("
    ").append(response.body);
    16     return builder.toString().getBytes();
    17 }

    解码

     1 // 解码Http服务
     2 private Request decode(byte[] bytes) throws IOException {
     3     Request request = new Request();
     4     BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
     5     String firstLine = reader.readLine();
     6     System.out.println(firstLine);
     7     String[] split = firstLine.trim().split(" ");
     8     request.method = split[0];
     9     request.url = split[1];
    10     request.version = split[2];
    11     //读取请求头
    12     Map<String, String> heads = new HashMap<>();
    13     while (true) {
    14         String line = reader.readLine();
    15         if (line.trim().equals("")) {
    16             break;
    17         }
    18         String[] split1 = line.split(":");
    19         heads.put(split1[0], split1[1]);
    20     }
    21     request.heads = heads;
    22     request.params = getUrlParams(request.url);
    23     //读取请求体
    24     request.body = reader.readLine();
    25     return request;
    26 }

    获取请求参数

     1 private static Map getUrlParams(String url) {
     2     Map<String, String> map = new HashMap<>();
     3     url = url.replace("?", ";");
     4     if (!url.contains(";")) {
     5         return map;
     6     }
     7     if (url.split(";").length > 0) {
     8         String[] arr = url.split(";")[1].split("&");
     9         for (String s : arr) {
    10             if (s.contains("=")) {
    11                 String key = s.split("=")[0];
    12                 String value = s.split("=")[1];
    13                 map.put(key, value);
    14             } else {
    15                 map.put(s, null);
    16             }
    17         }
    18         return map;
    19     } else {
    20         return map;
    21     }
    22 }

    四、交互实现

    1、服务端启动

      对于已经初始化好的ServerSocketChannel,我们下来要做的无非就是while(true)轮询selector。这个套路已经非常固定了。这里我们启动一个线程来轮询:

     1 public void start() {
     2     this.run = true;
     3     new Thread(() -> {
     4         try {
     5             while (run) {
     6                 selector.select(2000);
     7                 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
     8                 while (iterator.hasNext()) {
     9                     SelectionKey key = iterator.next();
    10                     iterator.remove();
    11                     // 监听客户端接入
    12                     if (key.isAcceptable()) {
    13                         handleAccept(key);
    14                     }
    15                     // 监听客户端发送消息
    16                     else if (key.isReadable()) {
    17                         handleRead(key);
    18                     }
    19                 }
    20             }
    21         } catch (IOException e) {
    22             e.printStackTrace();
    23         }
    24     }, "selector-io").start();
    25 }

    2、处理客户端接入

    1 // 当有客户端接入的时候,为其注册 可读 事件监听,等待客户端发送数据
    2 private void handleAccept(SelectionKey key) throws IOException {
    3     ServerSocketChannel channel = (ServerSocketChannel) key.channel();
    4     SocketChannel socketChannel = channel.accept();
    5     socketChannel.configureBlocking(false);
    6     socketChannel.register(selector, SelectionKey.OP_READ);
    7 }

    3、处理客户端发送的消息

     1 /**
     2  * 接收到客户端发送的数据进行处理
     3  * 1、将客户端的请求数据取出来,放到ByteArrayOutputStream。
     4  * 2、将数据交给Servlet处理。
     5  */
     6 private void handleRead(SelectionKey key) throws IOException {
     7     final SocketChannel channel = (SocketChannel) key.channel();
     8     ByteBuffer buffer = ByteBuffer.allocate(1024);
     9     final ByteArrayOutputStream out = new ByteArrayOutputStream();
    10     while (channel.read(buffer) > 0) {
    11         buffer.flip();
    12         out.write(buffer.array(), 0, buffer.limit());
    13         buffer.clear();
    14     }
    15     if (out.size() <= 0) {
    16         channel.close();
    17         return;
    18     }
    19     process(channel, out);
    20 }

    4、业务处理并发送返回数据

     1 private void process(SocketChannel channel, ByteArrayOutputStream out) {
     2     executor.submit(() -> {
     3         try {
     4             Request request = decode(out.toByteArray());
     5             Response response = new Response();
     6             if (request.method.equalsIgnoreCase("GET")) {
     7                 servlet.doGet(request, response);
     8             } else {
     9                 servlet.doPost(request, response);
    10             }
    11             channel.write(ByteBuffer.wrap(encode(response)));
    12         } catch (Throwable e) {
    13             e.printStackTrace();
    14         }
    15     });
    16 }

    五、单元测试

     1 @Test
     2 public void simpleHttpTest() throws IOException, InterruptedException {
     3     SimpleHttpServer simpleHttpServer = new SimpleHttpServer(8080, new HttpServlet() {
     4         @Override
     5         public void doGet(Request request, Response response) {
     6             System.out.println(request.url);
     7             response.body="hello_word:" + System.currentTimeMillis();
     8             response.code=200;
     9             response.headers=new HashMap<>();
    10         }
    11         @Override
    12         public void doPost(Request request, Response response) {}
    13     });
    14     simpleHttpServer.start();
    15     new CountDownLatch(1).await();
    16 }

    六、小结

      以上,使用原生NIO实现了一个简单的HTTP交互样例,虽然,只做了自定义Servlet中做了GET方法的实现。其实原理已经很明了。真正的Tomcat交互内核,其实就是在这个原理的基础上做了工业级软件架构设计。小结一下:

      <1>、浏览器地址栏访问,对于浏览器内核,可以理解触发了两个事件,OP_CONNECT事件、OP_WRITE事件。

      <2>、NIO实现的服务端还是遵循固定套路。当监听到OP_READ事件后,直接处理,然后回写结果。

      <3>、浏览器会在OP_WRITE事件后,自动变更监听为OP_READ事件。等待服务端返回。

      <4>、关于编码、解码、请求参数获取等,均属于HTTP协议的范畴,其实无关NIO。

      <5>、服务端selector轮询、accept接入channel注册。这两个操作之间使用的是用一个同步器,所以存在死锁的风险。Tomcat里边做了很好的处理。这里以后再聊。

      谨以此笔记记录一下原生NIO学习心得,为后续Tomcat源码部门铺一下技术前提。GIT地址:https://gitee.com/llzx/nio_practice.git

  • 相关阅读:
    043_MySQL 索引原理 与 慢查询优化
    042_MySQL 之【视图】【触发器】【存储过程】【函数】【事物】【数据库锁】【数据库备份】
    041_SQL逻辑查询语句执行顺序
    039_MySQL 数据操作
    040_数据库设计三范式
    039_MySQL_多表查询
    039_MySQL_单表查询
    038_MySQL 表的操作
    MySQL 存储引擎
    037_MySQL操作
  • 原文地址:https://www.cnblogs.com/UYGHYTYH/p/13336354.html
Copyright © 2011-2022 走看看