超级简单,100行Java实现NIO HTTP客户端,无第三方依赖
🎉 用100行代码实现Java NIO HTTP客户端,代码简单明了,搞明白NIO的工作原理,完全异步的HTTP协议解析流程
项目地址
代码已经开源, java-nio-http-downloader 👏 欢迎Star
所有的项目都在github上开源:100-line-code 欢迎Star 👏
用100行代码的不同语言(Java、Python、Go、Javascript、Rust)实现项目,通过讲解项目的实现,帮助大家学习编程:github.com/ruzhila/100…
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程序的程序结构图,我们可以看到,NIO的工作流程是这样的:
- (1)
Selector
通过select()
方法监听所有的 Channel,当有 Channel 可读、可写、有新连接等事件发生时,Selector 会返回这些事件。 - (2) 系统调用
select
方法,内核的Socket会被监听,当有事件发生时,会将有事件发生的Socket放入到select
的结果集中。 - (3) 遍历
select
的结果集,处理事件,比如可以写入数据 - (4) 调用
write
方法,将数据写入到Socket中 - (5) 内核的
Socket
会将数据发送到网络中
Selector 与 Channel
在NIO中,我们主要使用Selector
和Channel
来实现异步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
连接成功时,我们就可以处理链接的连接成功事件。
直接上代码
代码解析
通过NIO实现了一个简单的HTTP客户端,可以发送HTTP请求,并且获取服务器的响应。
实现了一个NioHTTPClient
的类,提供了几个方法:
sendRequest
, 创建连接,并且发送HTTP请求HTTPResponseListener
, NIO的客户端只能通过回掉的方式获取数据,我们通过这个接口来获取数据onResponse
当请求有相应时,会调用这个方法onData
当开始返回Body数据时,会调用这个方法
介绍完NioHTTPClient
之后,先看一下整体的程序是怎么工作的:
- 创建一个
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
函数
- 46行 告诉
从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行 当
isResponseParsed
为false
时,说明还没有解析完整的HTTP请求数据,需要判断是否出现\r\n\r\n
,如果出现了,就说明读到了完整的HTTP请求数据- 77-90行 解析HTTP响应,并且调用
listener.onResponse
函数,告诉调用者请求已经响应
- 77-90行 解析HTTP响应,并且调用
- 93-97行 当
isResponseParsed
为true
时,说明已经解析完整的HTTP请求数据,就可以直接读取Body数据了
这部分流程是异步编程最难理解的部分,因为数据并不会按照你的应用协议(比如HTTP)完整的返回,而是分批次返回,所以需要我们自己来处理数据的拼接和解析。
看一下实际的调用代码:
这个代码就可以实现一个线程内处理异步的HTTP请求,通过NioHTTPClient
类,我们可以实现一个简单的HTTP客户端,通过HTTPResponseListener
接口,我们可以获取到HTTP请求的响应数据。
总结
NIO是后端编程必备的技术,因为这个需要对系统操作有比较深的积累,并且对协议的理解也需要比较深入
还需要处理Zero-Copy
和多线程
等技术,这些都是比较高级的技术,需要大家多多实践。
高性能的服务器编程都是基于NIO实现的,比如Redis
,Nginx
等经典产品都是基于NIO。
这个版本只支持HTTP协议,不支持HTTPS, HTTPS协议会更加复杂,考虑的点更多,比如证书验证、加密解密等
关注我们的公众号,加入我们的项目实战群可以获取HTTPS版本的代码和教程
交流
所有的代码都在github上开源:100-line-code 欢迎Star 👏
转载自:https://juejin.cn/post/7366863987210158130