首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

java 中的 BIO/NIO/AIO 详解

java 的 IO 演进之路

我们在前面学习了 linux 的 5 种 I/O 模型详解

下面我们一起来学习下如何使用 java 实现 BIO/NIO/AIO 这 3 种不同的网络 IO 模型编程。

BIO 编程

BIO 作为最基础的 IO 版本,实现起来比较简单。

Server

client

启动测试

启动服务端

启动客户端

再次查看服务端日志

线程池版本

BIO 的缺点

缺点其实非常明显,每次都要创建一个线程去处理。

比如我的实现是直接阻塞当前线程的,这当然非常的不友好。

可以使用线线程池的方式进行优化改进。

线程版本

线程池版本

其他代码保持不变。

优缺点

线程池版本的 BIO 又被称作伪异步 IO。

属于在 NIO 还没有流行之前的一种实战解决方案。

这种方式的性能和 BIO 想比较提升了很多,实现起来也比较简单,但是可靠性相对较差。

NIO 基本概念

Buffer

Java NIO Buffers用于和NIO Channel交互。正如你已经知道的,我们从channel中读取数据到buffers里,从buffer把数据写入到channels.

buffer 本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。

这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。

Channel

Java NIO Channel通道和流非常相似,主要有以下几点区别:

通道可以读也可以写,流一般来说是单向的(只能读或者写)。

通道可以异步读写。

通道总是基于缓冲区Buffer来读写。

Selector

用单线程处理多个channels的好处是我需要更少的线程来处理channel。

实际上,你甚至可以用一个线程来处理所有的channels。

从操作系统的角度来看,切换线程开销是比较昂贵的,并且每个线程都需要占用系统资源,因此暂用线程越少越好。

需要留意的是,现代操作系统和CPU在多任务处理上已经变得越来越好,所以多线程带来的影响也越来越小。

如果一个CPU是多核的,如果不执行多任务反而是浪费了机器的性能。不过这些设计讨论是另外的话题了。

简而言之,通过Selector我们可以实现单线程操作多个channel。

NIO 实现方式

NIO 采取通道(Channel)和缓冲区(Buffer)来传输和保存数据,它是非阻塞式的 I/O,即在等待连接、读写数据(这些都是在一线程以客户端的程序中会阻塞线程的操作)的时候,程序也可以做其他事情,以实现线程的异步操作。

考虑一个即时消息服务器,可能有上千个客户端同时连接到服务器,但是在任何时刻只有非常少量的消息需要读取和分发(如果采用线程池或者一线程一客户端方式,则会非常浪费资源),这就需要一种方法能阻塞等待,直到有一个通道可以进行 I/O 操作。

NIO 的 Selector 选择器就实现了这样的功能,一个 Selector 实例可以同时检查一组信道的 I/O 状态,它就类似一个观察者,只要我们把需要探知的 SocketChannel 告诉 Selector,我们接着做别的事情,当有事件(比如,连接打开、数据到达等)发生时,它会通知我们,传回一组 SelectionKey,我们读取这些 Key,就会获得我们刚刚注册过的 SocketChannel,然后,我们从这个 Channel 中读取数据,接着我们可以处理这些数据。

Selector 内部原理实际是在做一个对所注册的 Channel 的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个 Channel 有所注册的事情发生,比如数据来了,它就会读取 Channel 中的数据,并对其进行处理。

要使用选择器,需要创建一个 Selector 实例,并将其注册到想要监控的信道上(通过 Channel 的方法实现)。

最后调用选择器的 select()方法,该方法会阻塞等待,直到有一个或多个信道准备好了 I/O 操作或等待超时,或另一个线程调用了该选择器的 wakeup()方法。

现在,在一个单独的线程中,通过调用 select()方法,就能检查多个信道是否准备好进行 I/O 操作,由于非阻塞 I/O 的异步特性,在检查的同时,我们也可以执行其他任务。

服务端

步骤

(1)创建一个 Selector 实例;

(2)将其注册到各种信道,并指定每个信道上感兴趣的I/O操作;

(3)重复执行:

代码实现

客户端

代码实现

测试

运行服务端

服务端

运行客户端

客户端

服务端

JDK AIO

jdk7中新增了一些与文件(网络)I/O相关的一些api。这些API被称为NIO.2,或称为AIO(Asynchronous I/O)。

AIO最大的一个特性就是异步能力,这种能力对socket与文件I/O都起作用。

实现方式

Future 方式

即提交一个 I/O 操作请求(accept/read/write),返回一个 Future。

然后您可以对 Future 进行检查(调用get(timeout)),确定它是否完成,或者阻塞 IO 操作直到操作正常完成或者超时异常。

使用 Future 方式很简单,需要注意的是,因为Future.get()是同步的,所以如果不仔细考虑使用场合,使用 Future 方式可能很容易进入完全同步的编程模式,从而使得异步操作成为一个摆设。

如果这样,那么原来旧版本的 Socket API 便可以完全胜任,大可不必使用异步 I/O.

Callback 方式

即提交一个 I/O 操作请求,并且指定一个 CompletionHandler。

当异步 I/O 操作完成时,便发送一个通知,此时这个 CompletionHandler 对象的 completed 或者 failed 方法将会被调用。

性能

因为AIO的实施需充分调用OS参与,IO需要操作系统支持、并发也同样需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。

Future 实现方式

Server

客户端

测试

启动服务端

启动客户端

服务端日志

Callback 模式

服务端

客户端

同上

小结

本文讲述了 jdk 实现的 bio/nio/aio 的方式,你是否会感觉 jdk 中的 api 设计过于复杂呢?

下一节我们将通过 netty 框架实现上述功能,并讲述我们为什么要选择 netty 作为网络开发的基本工具。

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次相遇。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20201220A0A94A00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券