项目地址

NIO的工作原理

Selector 与 Channel

直接上代码

代码解析

<code>NioHTTPClient</code>的连接创建和发送请求流程:

<code>NioHTTPClient</code>的数据读取流程:

HTTP协议解析

看一下实际的调用代码:

总结

交流

超级简单,100行Java实现NIO HTTP客户端,无第三方依赖

本文发表于入职啦(公众号: ruzhila) 大家可以访问入职啦学习更多的编程实战。

🎉 用100行代码实现Java NIO HTTP客户端,代码简单明了,搞明白NIO的工作原理,完全异步的HTTP协议解析流程

项目地址

代码已经开源, java-nio-http-downloader 👏 欢迎Star

所有的项目都在github上开源:100-line-code 欢迎Star 👏

用100行代码的不同语言(Java、Python、Go、Javascript、Rust)实现项目,通过讲解项目的实现,帮助大家学习编程

我们会定期在群里分享最新的项目实战代码,包括不同语言的实现

老师还会详细讲解代码优化的思路,扫码加入实战群:

入群学习

NIO的工作原理

大部分情况下,我们的IO调用都是阻塞调用,比如你调用Java的URLConnection,它会一直等待服务器返回数据,直到数据返回后,才会继续执行后面的代码:

URL url = new URL("http://example.org");
URLConnection connection = url.openConnection();
InputStream inputStream = connection.getInputStream();
....

这个代码就是一个典型的阻塞IO调用,当我们调用connection.getInputStream()时,程序会一直等待服务器返回数据,直到数据返回后,才会继续执行后面的代码。

这种写法比较简单易懂,但是有一个问题,每个请求IO都逻辑都需要分配一个独立的线程,当有大量的IO请求时,会导致线程资源耗尽,程序性能下降。

Non-blocking IO就是为了解决这个问题而生的,它是Java提供的一种异步IO的解决方案,通过NIO,我们可以用一个线程处理多个IO请求,提高程序的性能:

nio flow

这是一个典型的NIO程序的程序结构图,我们可以看到,NIO的工作流程是这样的:

  • (1) Selector 通过 select() 方法监听所有的 Channel,当有 Channel 可读、可写、有新连接等事件发生时,Selector 会返回这些事件。
  • (2) 系统调用select方法,内核的Socket会被监听,当有事件发生时,会将有事件发生的Socket放入到select的结果集中。
  • (3) 遍历select的结果集,处理事件,比如可以写入数据
  • (4) 调用write方法,将数据写入到Socket中
  • (5) 内核的Socket会将数据发送到网络中

Selector 与 Channel

在NIO中,我们主要使用SelectorChannel来实现异步IO,Selector是一个多路复用器,它可以同时监听多个Channel的事件,当有事件发生时,Selector会返回这些事件,我们可以通过Channel来读写数据:

Selector selector = Selector.open();
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_CONNECT);
while(true) {
    selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    for(SelectionKey key: keys) {
        if(key.isConnectable()) {
            // do something
        }
    }
}

通过向selector注册OP_CONNECT事件,当channel连接成功时,我们就可以处理链接的连接成功事件。

直接上代码

code

代码解析

通过NIO实现了一个简单的HTTP客户端,可以发送HTTP请求,并且获取服务器的响应。 实现了一个NioHTTPClient的类,提供了几个方法:

  • sendRequest, 创建连接,并且发送HTTP请求
  • HTTPResponseListener, NIO的客户端只能通过回掉的方式获取数据,我们通过这个接口来获取数据
    • onResponse 当请求有相应时,会调用这个方法
    • onData 当开始返回Body数据时,会调用这个方法

介绍完NioHTTPClient之后,先看一下整体的程序是怎么工作的:
selector

  • 创建一个Selector对象
  • 创建一个NioHTTPClient对象
  • 调用sendRequest方法,发送HTTP请求
  • 循环调用selector.select()方法,监听事件,如果有事件发生,就调用NioHTTPClient的对应的onCanWrite, onCanRead等方法处理数据

NioHTTPClient的连接创建和发送请求流程:

  • 27-31行 sendRequest 创建连接,发起socket的connect操作
  • 40行 等待连接成功的事件
  • 43行Selector得到这个SocketChannel连接成功之后,调用onConnect的函数
    • 46行 告诉Selector需要监听可以写的事件
    • 50-54行 当下次Selector发现这个SocketChannel可以写的时候,调用onCanWrite函数

从43行->46行->50行,这个流程是最重要的一个异步概念:每次IO操作之前都应该先等到可以操作之后再调用

也就是说,我们在onConnect函数中,告诉Selector我们需要监听写事件,当Selector发现这个SocketChannel可以写的时候,我们再调用onCanWrite函数,这样才能把数据发送出去

NioHTTPClient的数据读取流程:

  • 53行 当发送出去请求之后,就会向Selecor注册读事件,也就是当有数据返回之后,会调用onCanRead函数

onCanRead函数中,需要处理比较复杂的状态:

TCP数据每个报文传输是1500字节以内,内核会把网络传输的数据放到缓冲区 如果onCanRead每次调用的间隔时间比较长,那么那么可能一次read就能读到一个完整的HTTP请求数据。

但是代码不能这么乐观的估计,要用最保守的方式来处理数据,每次读数据后放到buffer中,然后判断是否读到了完整的HTTP请求数据 如果没有读到,就继续读,直到读到完整的HTTP请求数据才能执行解析工作。

HTTP协议解析

HTTP响应的数据格式如下:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1256

PNG....

根据HTTP请求规范,每个HTTP请求的头部都是以\r\n\r\n结尾的,所以我们可以通过这个来判断是否读到了完整的HTTP请求数据。

  • 67行isResponseParsedfalse时,说明还没有解析完整的HTTP请求数据,需要判断是否出现\r\n\r\n,如果出现了,就说明读到了完整的HTTP请求数据
    • 77-90行 解析HTTP响应,并且调用listener.onResponse函数,告诉调用者请求已经响应
  • 93-97行isResponseParsedtrue时,说明已经解析完整的HTTP请求数据,就可以直接读取Body数据了

这部分流程是异步编程最难理解的部分,因为数据并不会按照你的应用协议(比如HTTP)完整的返回,而是分批次返回,所以需要我们自己来处理数据的拼接和解析。

看一下实际的调用代码:

client

这个代码就可以实现一个线程内处理异步的HTTP请求,通过NioHTTPClient类,我们可以实现一个简单的HTTP客户端,通过HTTPResponseListener接口,我们可以获取到HTTP请求的响应数据。

总结

NIO是后端编程必备的技术,因为这个需要对系统操作有比较深的积累,并且对协议的理解也需要比较深入

还需要处理Zero-Copy多线程等技术,这些都是比较高级的技术,需要大家多多实践。

高性能的服务器编程都是基于NIO实现的,比如Redis,Nginx等经典产品都是基于NIO。

这个版本只支持HTTP协议,不支持HTTPS, HTTPS协议会更加复杂,考虑的点更多,比如证书验证、加密解密等

关注我们的公众号,加入我们的项目实战群可以获取HTTPS版本的代码和教程

交流

我们构建了一个100行代码项目的实战群,大家可以扫码加入,一起学习编程

入群学习

也可以访问入职啦学习更多的编程实战

所有的代码都在github上开源:100-line-code 欢迎Star 👏

友情链接:

Copyright© 2024 Ruzhila.cn 版权所有