NIO实践-HTTP交互实现暨简版Tomcat交互内核

java1234

共 7524字,需浏览 16分钟

 · 2020-07-28

 

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达


66套java从入门到精通实战课程分享

  作 | 了了在小

来源 | cnblogs.com/UYGHYTYH/p/13336354.html


今天就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。


public class SimpleHttpServer {
    // 服务端口
    private int port;
    // 处理器
    private HttpServlet servlet;
    // 轮询器
    private final Selector selector;
    // 启停标识
    private volatile boolean run = false;
    // 需要注册的Channel,避免与轮询器产生死锁
    private Set allConnections = new HashSet<>();
    // 执行业务线程池
    private ExecutorService executor = Executors.newFixedThreadPool(5);

    public SimpleHttpServer(int port, HttpServlet servlet) throws IOException {
        this.port = port;
        this.servlet = servlet;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        selector = Selector.open();
        serverSocketChannel.bind(new InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        // 一旦初始化就开始监听客户端接入事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }
}


2、业务处理HttpServlet的细节

HttpServlet


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


Request


public class Request {
    Map<String, String> heads;
    String url;
    String method;
    String version;
    //请求内容
    String body;
    Map<String, String> params;
}


Response


public class Response {
    Map<String, String> headers;
    // 状态码
    int code;
    //返回结果
    String body;
}


3、编解码相关

编码


//编码Http 服务
private byte[] encode(Response response) {
    StringBuilder builder = new StringBuilder(512);
    builder.append("HTTP/1.1 ").append(response.code).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")
                .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"));
        builder.append(headStr + "\r\n");
    }
    builder.append("\r\n").append(response.body);
    return builder.toString().getBytes();
}


解码


// 解码Http服务
private Request decode(byte[] bytes) throws IOException {
    Request request = new Request();
    BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
    String firstLine = reader.readLine();
    System.out.println(firstLine);
    String[] split = firstLine.trim().split(" ");
    request.method = split[0];
    request.url = split[1];
    request.version = split[2];
    //读取请求头
    Map<String, String> heads = new HashMap<>();
    while (true) {
        String line = reader.readLine();
        if (line.trim().equals("")) {
            break;
        }
        String[] split1 = line.split(":");
        heads.put(split1[0], split1[1]);
    }
    request.heads = heads;
    request.params = getUrlParams(request.url);
    //读取请求体
    request.body = reader.readLine();
    return request;
}


获取请求参数


private static Map getUrlParams(String url) {
    Map<String, String> map = new HashMap<>();
    url = url.replace("?", ";");
    if (!url.contains(";")) {
        return map;
    }
    if (url.split(";").length > 0) {
        String[] arr = url.split(";")[1].split("&");
        for (String s : arr) {
            if (s.contains("=")) {
                String key = s.split("=")[0];
                String value = s.split("=")[1];
                map.put(key, value);
            } else {
                map.put(s, null);
            }
        }
        return map;
    } else {
        return map;
    }
}


四、交互实现

1、服务端启动

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


public void start() {
    this.run = true;
    new Thread(() -> {
        try {
            while (run) {
                selector.select(2000);
                Iterator iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    // 监听客户端接入
                    if (key.isAcceptable()) {
                        handleAccept(key);
                    }
                    // 监听客户端发送消息
                    else if (key.isReadable()) {
                        handleRead(key);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }, "selector-io").start();
}


2、处理客户端接入


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


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


/**
 * 接收到客户端发送的数据进行处理
 * 1、将客户端的请求数据取出来,放到ByteArrayOutputStream。
 * 2、将数据交给Servlet处理。
 */

private void handleRead(SelectionKey key) throws IOException {
    final SocketChannel channel = (SocketChannel) key.channel();
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    final ByteArrayOutputStream out = new ByteArrayOutputStream();
    while (channel.read(buffer) > 0) {
        buffer.flip();
        out.write(buffer.array(), 0, buffer.limit());
        buffer.clear();
    }
    if (out.size() <= 0) {
        channel.close();
        return;
    }
    process(channel, out);
}


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


private void process(SocketChannel channel, ByteArrayOutputStream out) {
    executor.submit(() -> {
        try {
            Request request = decode(out.toByteArray());
            Response response = new Response();
            if (request.method.equalsIgnoreCase("GET")) {
                servlet.doGet(request, response);
            } else {
                servlet.doPost(request, response);
            }
            channel.write(ByteBuffer.wrap(encode(response)));
        } catch (Throwable e) {
            e.printStackTrace();
        }
    });
}


五、单元测试

@Test
public void simpleHttpTest() throws IOException, InterruptedException {
    SimpleHttpServer simpleHttpServer = new SimpleHttpServer(8080, new HttpServlet() {
        @Override
        public void doGet(Request request, Response response) {
            System.out.println(request.url);
            response.body="hello_word:" + System.currentTimeMillis();
            response.code=200;
            response.headers=new HashMap<>();
        }
        @Override
        public void doPost(Request request, Response response) {}
    });
    simpleHttpServer.start();
    new CountDownLatch(1).await();
}


六、小结

  以上,使用原生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里边做了很好的处理。这里以后再聊。


     




感谢点赞支持下哈 



浏览 24
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报