java的三种IO模型详解(BIO、NIO、AIO)
作者:BLUcoding
本文介绍了BIO、NIO和AIO三种不同的IO模型,分别分析了它们的工作机制、实现方式以及与BIO的对比,BIO是阻塞的,每个连接需要一个线程;NIO是同步非阻塞的,通过缓冲区和选择器实现I/O多路复用;AIO是异步的,操作系统处理IO操作,完成后通知应用程序
一、BIO 阻塞式 IO(Blocking IO)
每个客户端连接都会在一个独立的线程中处理,并且这个线程在处理 IO 操作时会阻塞,直到操作完成。
- 每个连接都需要一个独立的线程,连接数较多时,会消耗大量的内存和 CPU 资源
- 线程在处理 IO 操作时会阻塞
1.1、BIO 工作机制
- 客户端通过 Socket 对象与服务端建立连接,从 Socket 中得到字节输入流或输出流进行数据读写。
- 服务端通过
ServerSocket
注册端口,调用accept
方法监听客户端 Socket 请求,从 Socket 中得到字节输入流或输出流进行数据读写。
1.2、BIO 实现单发单收
客户端:
public static void main(String[] args) { Socket socket = null; try { //与服务端连接 socket = new Socket("127.0.0.1", 5000); //从 socket 管道中获取字节输出流 OutputStream os = socket.getOutputStream(); //将字节输出流包装为打印流 PrintStream ps = new PrintStream(os); //发一行数据 ps.println("Hi BIO! 与服务端通信成功"); ps.flush(); } catch (IOException e) { e.printStackTrace(); } }
服务端:
public static void main(String[] args) { System.out.println("===服务端启动==="); ServerSocket serverSocket = null; try { //注册端口 serverSocket = new ServerSocket(5000); //监听客户端请求 Socket socket = serverSocket.accept(); //从 socket 管道中获取字节输入流 InputStream is = socket.getInputStream(); //将字节输入流包装为缓冲字符输入流 BufferedReader br = new BufferedReader(new InputStreamReader(is)); String msg; //读一行数据 if ((msg = br.readLine()) != null) { System.out.println("服务端接收客户端信息为:" + msg); } }catch (Exception e){ System.out.println(e.getMessage()); } }
1.3、BIO 实现多发多收
客户端:
public static void main(String[] args) { try { Socket socket = new Socket("localhost",9988); OutputStream os = socket.getOutputStream(); PrintStream ps = new PrintStream(os); Scanner scanner = new Scanner(System.in); while (true){ System.out.println("请输入:"); String input = scanner.nextLine(); ps.println(input); ps.flush(); } } catch (IOException e) { e.printStackTrace(); } }
服务端:
public static void main(String[] args) { System.out.println("===服务端启动==="); try { ServerSocket ss = new ServerSocket(9988); Socket socket = ss.accept(); InputStream is = socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String message; while ((message = br.readLine()) != null){ System.out.println("服务端接收客户端信息为:" + message); } } catch (IOException e) { e.printStackTrace(); } }
1.4、BIO 实现客户端服务端多对一
服务端:
public void listen() throws IOException { ServerSocket serverSocket = null; try { log.info("服务启动监听"); serverSocket = new ServerSocket(9988); //循环接收到客户端的连接 while (true) { Socket socket = serverSocket.accept(); //得到连接后,开启一个线程处理连接 handleSocket(socket); } } finally { if(serverSocket != null){ serverSocket.close(); } } } private void handleSocket(Socket socket) { HandleSocket socketHandle = new HandleSocket(socket); new Thread(socketHandle).start(); }
public void run() { BufferedInputStream bufferedInputStream = null; BufferedOutputStream bufferedOutputStream = null; try { bufferedInputStream = new BufferedInputStream(socket.getInputStream()); byte[] bytes = new byte[1024]; int len ; if ((len = bufferedInputStream.read(bytes)) > -1) { String result = new String(bytes,0,len); System.out.println("本次接收到的结果:" + result); } System.out.println("回复信息给客户端:"); bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream()); String outString = Thread.currentThread().getName() + "接收到了"; bufferedOutputStream.write(outString.getBytes()); bufferedOutputStream.flush(); } catch (IOException e) { System.out.println("处理异常:" + e.getMessage()); } finally { try { if (bufferedInputStream != null) { bufferedInputStream.close(); } if (bufferedOutputStream != null) { bufferedOutputStream.close(); } }catch (IOException e){ System.out.println("关闭流异常:" + e.getMessage()); } } }
客户端:
public void start() throws IOException { Socket socket = new Socket("127.0.0.1", 8081); String msg = "Hi,This is the BioClient"; BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream()); byte[] bytes = msg.getBytes(); bufferedOutputStream.write(bytes); bufferedOutputStream.flush(); System.out.println("发送完毕"); BufferedInputStream bufferedInputStream = new BufferedInputStream(socket.getInputStream()); byte[] inBytes = new byte[1024]; int len; if ((len = bufferedInputStream.read(inBytes)) != -1) { String result = new String(inBytes, 0, len); System.out.println("接收到的消息="+result); } bufferedOutputStream.close(); bufferedInputStream.close(); socket.close(); }
1.5、BIO 模式下的端口转发思想
一个客户端的消息经由服务端发送给所有的客户端,实现群聊功能。
public class Server { // 定义一个静态集合 public static List<Socket> allSocketOnLine = new ArrayList(); public static void main(String[] args) { try { ServerSocket ss = new ServerSocket(9999); while (true){ Socket socket = ss.accept(); // 把登录的客户端socket存入到一个在线的集合中去 allSocketOnLine.add(socket); // 为当前登录成功的socket分配一个独立的线程来处理 new ServerReaderThread(socket).start(); } } catch (IOException e) { e.printStackTrace(); } } } public class ServerReaderThread extends Thread{ private Socket socket; public ServerReaderThread(Socket socket){ this.socket = socket; } @Override public void run() { try { BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String msg; while ((msg = br.readLine()) != null) { // 发送给所有的在线socket sendMsgToAllClient(msg); } } catch (Exception e) { System.out.println("有人下线了"); Server.allSocketOnLine.remove(socket); } } /** * 把当前客户端发来的消息发送给全部的在线socket * @param msg */ private void sendMsgToAllClient(String msg) throws IOException { for (Socket sk : Server.allSocketOnLine) { PrintWriter pw = new PrintWriter(sk.getOutputStream()); pw.println(msg); pw.flush(); } } }
二、NIO 同步非阻塞式 IO(Non-blocking IO)
允许线程在等待IO操作完成期间可以继续执行其他任务。
2.1、NIO 3个核心组件(缓冲区、通道、选择器)
- 缓冲区(Buffer):用于存储数据的对象。数据从通道读取到缓冲区,或者从缓冲区写入到通道。
- 通道(Channel):既可以从通道中读取数据,又可以写数据到通道
- 选择器(Selector):同时管理多个通道,通过注册通道的事件(如连接就绪、读就绪、写就绪),使用单个线程就能处理多个通道,从而管理多个网络连接,提高了效率。
2.2、NIO 主要特性
- 非阻塞I/O:允许线程在等待IO操作完成期间可以继续执行其他任务
- IO多路复用:通过选择器,NIO允许多个通道共用一个线程进行管理,减少了线程的资源消耗。
- 异步IO操作:可以在通道上注册事件和回调函数,实现非阻塞的IO操作
- 内存映射文件:将文件的一部分或全部直接映射到内存中,这样可以像访问内存一样访问文件,提高了文件处理的效率。
- 文件锁定:允许对文件的部分或全部进行锁定,从而控制对文件的并发访问。
2.3、NIO 与 BIO 的对比
- 面向流与面向缓冲:BIO 是面向流的,每次从流中读一个或多个字节,直至读取所有字节;而 NIO 是面向缓冲区的。
- 阻塞与非阻塞:BIO 的流是阻塞的,当一个线程调用
read()
或write()
时,该线程被阻塞,直到有一些数据被读取或数据完全写入;而 NIO 是非阻塞的,一个线程从某通道发送请求读取数据,它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。 - 线程开销:BIO 为每个客户端连接创建一个线程,在大量并发连接的情况下会带来巨大的线程开销;NIO 通过选择器实现
I/O多路复用
,在一个线程中处理多个通道,减少了线程开销。
2.4、Buffer 常用子类
ByteBuffer
:用于存储字节数据;CharBuffer
:用于存储字符数据;ShortBuffer
:用于存储Short类型数据;IntBuffer
:用于存储Int类型数据;LongBuffer
:用于存储Long类型数据;FloatBuffer
:用于存储Float类型数据;DoubleBuffer
:用于存储Double类型数据;
2.5、Buffer 重要属性
capacity(容量)
:表示 Buffer 所占的内存大小,不能为负且创建后不能更改limit(限制)
:表示 Buffer 中可以操作数据的大小,不能为负且不能大于 capacity
写模式下,表示最多能往 Buffer 里写多少数据,即 limit 等于 capacity
读模式下,表示最多能读到多少数据,即已写入的所有数据position(位置)
:表示下一个要读取或写入的数据的索引
缓冲区位置不能为负,且不能大于其限制
初始 position 值为 0,最大为 capacity – 1。当一个 byte、long 等数据写到 Buffer 后, position 会向前移动到下一个可插入数据的 Buffer 单元mark(标记)
:表示记录当前 position 的位置,可通过 reset() 恢复到 mark 的位置
三、AIO 异步式 IO(Asynchronous IO)
异步式IO操作不会阻塞线程,而是交由操作系统处理。
完成后,操作系统会通知应用程序,或者应用程序主动查询完成状态。
使线程在等待IO完成的同时可以执行其他任务,提高了系统的并发性能。
3.1、AIO 核心组件(异步通道、完成处理器)
1.异步通道(Asynchronous Channel):AIO 中进行I/O操作的基础设施。
AIO提供了多种异步通道:
AsynchronousSocketChannel
(异步套接字通道,支持面向连接的网络通信)AsynchronousServerSocketChannel
(异步服务器套接字通道,支持异步服务器端套接字通信)AsynchronousFileChannel
(异步文件通道,支持异步文件读写操作)
2.完成处理器(Completion Handler):用于在I/O操作完成后处理结果的回调接口。
完成处理器包含两个方法:
completed(V result, A attachment)
在I/O操作成功完成时调用;failed(Throwable exc, A attachment)
在I/O操作失败时调用。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。