NIO实践

Wesley13
• 阅读 455

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

优质文章,第一时间送达

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

作者 | 了了在小

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

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

一、关于HTTP

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

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

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

2、HTTP约定的报文格式

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

NIO实践

二、系统架构

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

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

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

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

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

NIO实践

三、服务初始化

1、服务器实例声明

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

public class SimpleHttpServer {    // 服务端口    private int port;    // 处理器    private HttpServlet servlet;    // 轮询器    private final Selector selector;    // 启停标识    private volatile boolean run = false;    // 需要注册的Channel,避免与轮询器产生死锁    private Set<SocketChannel> 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<SelectionKey> 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();        }    });}

五、单元测试

@Testpublic 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里边做了很好的处理。这里以后再聊。

NIO实践

NIO实践

     

感谢点赞支持下哈 NIO实践

本文分享自微信公众号 - java1234(gh_27ed55ecb177)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
SQL优化这么做就对了
点击上方蓝色字体,选择“标星公众号”优质文章,第一时间送达作者 |狼爷来源| urlify.cn/FZ7Bji76套java从入门到精通实战课程分享(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__bi
Easter79 Easter79
3年前
springboot集成neo4j
点击上方蓝色字体,选择“标星公众号”优质文章,第一时间送达76套java从入门到精通实战课程分享(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzg4ODA3NTk0Nw%3D%3D%26mid%3D2
Easter79 Easter79
3年前
Springboot@Autowired和@Resource?
点击上方蓝色字体,选择“标星公众号”优质文章,第一时间送达66套java从入门到精通实战课程分享(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzg4ODA3NTk0Nw%3D%3D%26mid%3D2
Stella981 Stella981
3年前
Kafka、RabbitMQ、RocketMQ等消息中间件的对比
点击上方蓝色字体,选择“标星公众号”优质文章,第一时间送达76套java从入门到精通实战课程分享(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzg4ODA3NTk0Nw%3D%3D%26mid%3D2
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Stella981 Stella981
3年前
200的大额人民币即将面世?央行:Yes!
点击上方蓝字关注我们!(https://oscimg.oschina.net/oscnet/2a1c2ac00bf54458a78c48a6c2e547d5.png)点击上方“印象python”,选择“星标”公众号重磅干货,第一时间送达!!(
Stella981 Stella981
3年前
SpringBoot 并发登录人数控制
点击上方蓝色字体,选择“标星公众号”优质文章,第一时间送达作者 |苏先生139来源| urlify.cn/ryeYRj66套java从入门到精通实战课程分享(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fmp.weixin.qq.com%2Fs%3F