超级简单,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程序的程序结构图,我们可以看到,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连接成功时,我们就可以处理链接的连接成功事件。

直接上代码

在这里插入图片描述

代码解析

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

通过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函数

从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)完整的返回,而是分批次返回,所以需要我们自己来处理数据的拼接和解析。

看一下实际的调用代码:

在这里插入图片描述

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

总结

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

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

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

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

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

交流

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

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

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

  • 35
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 NIO(Non-blocking I/O)实现 Java客户端和服务端可以提高网络通信的效率。下面是一个简单的示例,演示了如何使用 NIO 实现一个简单客户端和服务端。 首先是服务端代码: ```java import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; public class NioServer { private static int BUF_SIZE = 1024; private static int PORT = 8080; private static int TIMEOUT = 3000; public static void main(String[] args) { selector(); } public static void handleAccept(SelectionKey key) throws IOException { ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel(); SocketChannel sc = ssChannel.accept(); sc.configureBlocking(false); sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(BUF_SIZE)); } public static void handleRead(SelectionKey key) throws IOException { SocketChannel sc = (SocketChannel) key.channel(); ByteBuffer buf = (ByteBuffer) key.attachment(); int bytesRead = sc.read(buf); while (bytesRead > 0) { buf.flip(); while (buf.hasRemaining()) { System.out.print((char) buf.get()); } System.out.println(); buf.clear(); bytesRead = sc.read(buf); } if (bytesRead == -1) { sc.close(); } } public static void handleWrite(SelectionKey key) throws IOException { ByteBuffer buf = (ByteBuffer) key.attachment(); buf.flip(); SocketChannel sc = (SocketChannel) key.channel(); while (buf.hasRemaining()) { sc.write(buf); } buf.compact(); } public static void selector() { Selector selector = null; ServerSocketChannel ssc = null; try { selector = Selector.open(); ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress(PORT)); ssc.configureBlocking(false); ssc.register(selector, SelectionKey.OP_ACCEPT); while (true) { if (selector.select(TIMEOUT) == 0) { System.out.println("=="); continue; } Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iterator = keys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isAcceptable()) { handleAccept(key); } if (key.isReadable()) { handleRead(key); } if (key.isWritable() && key.isValid()) { handleWrite(key); } if (key.isConnectable()) { System.out.println("isConnectable = true"); } iterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } finally { try { if (selector != null) { selector.close(); } if (ssc != null) { ssc.close(); } } catch (IOException e) { e.printStackTrace(); } } } } ``` 然后是客户端代码: ```java import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Scanner; import java.util.Set; public class NioClient { private static int BUF_SIZE = 1024; private static int PORT = 8080; private static int TIMEOUT = 3000; public static void main(String[] args) throws IOException { SocketChannel clientChannel = SocketChannel.open(); clientChannel.configureBlocking(false); Selector selector = Selector.open(); clientChannel.register(selector, SelectionKey.OP_CONNECT); clientChannel.connect(new InetSocketAddress(PORT)); while (true) { if (selector.select(TIMEOUT) == 0) { System.out.println("=="); continue; } Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iterator = keys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isConnectable()) { SocketChannel channel = (SocketChannel) key.channel(); if (channel.isConnectionPending()) { channel.finishConnect(); } channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); Scanner scanner = new Scanner(System.in); String message = scanner.nextLine(); channel.write(ByteBuffer.wrap(message.getBytes())); } else if (key.isReadable()) { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE); int bytesRead = channel.read(buffer); while (bytesRead > 0) { buffer.flip(); while (buffer.hasRemaining()) { System.out.print((char) buffer.get()); } System.out.println(); buffer.clear(); bytesRead = channel.read(buffer); } } iterator.remove(); } } } } ``` 这里的服务端监听端口为 8080,客户端连接的端口也为 8080。客户端首先向服务端发送一条消息,然后等待服务端的响应。当服务端接收到客户端的消息后,就会输出到控制台,并将消息原封不动地返回给客户端客户端接收到服务端的响应后,也会将其输出到控制台。 注意,这个示例只是一个简单的演示,实际开发中需要考虑更多的因素,例如线程安全、异常处理等等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值