服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 一次搞懂Java三种IO模型

一次搞懂Java三种IO模型

2023-05-10 15:42三分恶老三 Java教程

Java BIO​相关的实现都位于java.io​包下,其通信原理是客户端、服务端之间通过Socket套接字建立管道连接,然后从管道中获取对应的输入/输出流,最后利用输入/输出流对象实现发送/接收信息。

大家好,我是老三,上一节我们讨论了Linux的五种IO模型,接下来,我们从Java语言层面,来看看对IO的实现。

在Java中,一共有三种IO模型,分别是阻塞IO(BIO)、非阻塞IO(NIO)和异步IO(AIO)。

一次搞懂Java三种IO模型

Linux五种IO模型和Java三种IO模型

Java BIO

Java BIO就是Java的传统IO模型,对应了操作系统IO模型里的阻塞IO。

Java BIO相关的实现都位于java.io包下,其通信原理是客户端、服务端之间通过Socket套接字建立管道连接,然后从管道中获取对应的输入/输出流,最后利用输入/输出流对象实现发送/接收信息。

我们来看个Demo:

  • BioServer:
  1. /** 
  2.  * @Author 三分恶 
  3.  * @Date 2023/4/30 
  4.  * @Description BIO服务端 
  5.  */ 
  6. public class BioServer { 
  7.  
  8.     public static void main(String[] args) throws IOException { 
  9.         //定义一个ServerSocket服务端对象,并为其绑定端口号 
  10.         ServerSocket server = new ServerSocket(8888); 
  11.         System.out.println("===========BIO服务端启动================"); 
  12.         //对BIO来讲,每个Socket都需要一个Thread 
  13.         while (true) { 
  14.             //监听客户端Socket连接 
  15.             Socket socket = server.accept(); 
  16.             new BioServerThread(socket).start(); 
  17.         } 
  18.  
  19.     } 
  20.  
  21.     /** 
  22.      * BIO Server线程 
  23.      */ 
  24.     static class BioServerThread extends Thread{ 
  25.         //socket连接 
  26.         private Socket socket; 
  27.         public BioServerThread(Socket socket){ 
  28.             this.socket=socket; 
  29.         } 
  30.  
  31.         @Override 
  32.         public void run() { 
  33.             try { 
  34.                 //从socket中获取输入流 
  35.                 InputStream inputStream=socket.getInputStream(); 
  36.                 //转换为 
  37.                 BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream)); 
  38.                 String msg; 
  39.                 //从Buffer中读取信息,如果读取到信息则输出 
  40.                 while((msg=bufferedReader.readLine())!=null){ 
  41.                     System.out.println("收到客户端消息:"+msg); 
  42.                 } 
  43.  
  44.                 //从socket中获取输出流 
  45.                 OutputStream outputStream=socket.getOutputStream(); 
  46.                 PrintStream printStream=new PrintStream(outputStream); 
  47.                 //通过输出流对象向客户端传递信息 
  48.                 printStream.println("你好,吊毛!"); 
  49.                 //清空输出流 
  50.                 printStream.flush(); 
  51.                 //关闭socket 
  52.                 socket.shutdownOutput(); 
  53.             } catch (IOException e) { 
  54.                 e.printStackTrace(); 
  55.             } 
  56.         } 
  57.     } 
  • BioClient
  1. /** 
  2.  * @Author 三分恶 
  3.  * @Date 2023/4/30 
  4.  * @Description BIO客户端 
  5.  */ 
  6. public class BioClient { 
  7.  
  8.     public static void main(String[] args) throws IOException { 
  9.         List<String> names= Arrays.asList("帅哥","靓仔","坤坤"); 
  10.         //通过循环创建多个多个client 
  11.         for (String name:names){ 
  12.             //创建socket并根据IP地址与端口连接服务端 
  13.             Socket socket=new Socket("127.0.0.1",8888); 
  14.             System.out.println("===========BIO客户端启动================"); 
  15.             //从socket中获取字节输出流 
  16.             OutputStream outputStream=socket.getOutputStream(); 
  17.             //通过输出流向服务端传递信息 
  18.             String hello="你好,"+name+"!"
  19.             outputStream.write(hello.getBytes()); 
  20.             //清空流,关闭socket输出 
  21.             outputStream.flush(); 
  22.             socket.shutdownOutput(); 
  23.  
  24.             //从socket中获取字节输入流 
  25.             InputStream inputStream=socket.getInputStream(); 
  26.             BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream)); 
  27.             //读取服务端消息 
  28.             String msg; 
  29.             while((msg=bufferedReader.readLine())!=null){ 
  30.                 System.out.println("收到服务端消息:"+msg); 
  31.             } 
  32.             inputStream.close(); 
  33.             outputStream.close(); 
  34.             socket.close(); 
  35.         } 
  36.     } 

  • 先启动BioServer,再启动BioClient,运行结果
  1. ===========BIO服务端启动================ 
  2. 收到客户端消息:你好,帅哥! 
  3. 收到客户端消息:你好,靓仔! 
  4. 收到客户端消息:你好,坤坤! 
  1. ===========BIO客户端启动================ 
  2. 收到服务端消息:你好,吊毛! 
  3. ===========BIO客户端启动================ 
  4. 收到服务端消息:你好,吊毛! 
  5. ===========BIO客户端启动================ 
  6. 收到服务端消息:你好,吊毛! 

在上述Java-BIO的通信过程中,如果客户端一直没有发送消息过来,服务端则会一直等待下去,从而服务端陷入阻塞状态。同理,由于客户端也一直在等待服务端的消息,如果服务端一直未响应消息回来,客户端也会陷入阻塞状态。

在BioServer定义了一个类BioServerThread,继承了Thread类,run方法里主要是通过socket和流来读取客户端的消息,以及发送消息给客户端,每处理一个客户端的Socket连接,就得新建一个线程。

同时,IO读写操作也是阻塞的,如果客户端一直没有发送消息过来,线程就会进入阻塞状态,一直等待下去。

在BioClient里,循环创建Socket,向服务端收发消息,客户端的读写也是阻塞的。

在这个Demo里就体现了BIO的两个特点:

  • 一个客户端连接对应一个处理线程
  • 读写操作都是阻塞的

一次搞懂Java三种IO模型

Java BIO

毫无疑问,不管是创建太多线程,还是阻塞读写,都会浪费服务器的资源。

Java NIO

那么我们就进入Java的下一种IO模型——Java NIO,它对应操作系统IO模型中的多路复用IO,底层采用了epoll实现。

Java-NIO则是JDK1.4中新引入的API,它在BIO功能的基础上实现了非阻塞式的特性,其所有实现都位于java.nio包下。NIO是一种基于通道、面向缓冲区的IO操作,相较BIO而言,它能够更为高效的对数据进行读写操作,同时与原先的BIO使用方式也大有不同。

我们还是先来看个Demo:

  • NioServer
  1. /** 
  2.  * @Author 三分恶 
  3.  * @Date 2023/4/30 
  4.  * @Description NIO服务端 
  5.  */ 
  6. public class NioServer { 
  7.  
  8.     public static void main(String[] args) throws IOException { 
  9.         //创建一个选择器selector 
  10.         Selector selector= Selector.open(); 
  11.         //创建serverSocketChannel 
  12.         ServerSocketChannel serverSocketChannel=ServerSocketChannel.open(); 
  13.         //绑定端口 
  14.         serverSocketChannel.socket().bind(new InetSocketAddress(8888)); 
  15.         //必须得设置成非阻塞模式 
  16.         serverSocketChannel.configureBlocking(false); 
  17.         //将channel注册到selector并设置监听事件为ACCEPT 
  18.         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); 
  19.         System.out.println("===========NIO服务端启动============"); 
  20.         while(true){ 
  21.             //超时等待 
  22.             if(selector.select(1000)==0){ 
  23.                 System.out.println("===========NIO服务端超时等待============"); 
  24.                 continue
  25.             } 
  26.             // 有客户端请求被轮询监听到,获取返回的SelectionKey集合 
  27.             Iterator<SelectionKey> iterator=selector.selectedKeys().iterator(); 
  28.             //迭代器遍历SelectionKey集合 
  29.             while (iterator.hasNext()){ 
  30.                 SelectionKey key=iterator.next(); 
  31.                 // 判断是否为ACCEPT事件 
  32.                 if (key.isAcceptable()){ 
  33.                     // 处理接收请求事件 
  34.                     SocketChannel socketChannel=((ServerSocketChannel) key.channel()).accept(); 
  35.                     //非阻塞模式 
  36.                     socketChannel.configureBlocking(false); 
  37.                     // 注册到Selector并设置监听事件为READ 
  38.                     socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024)); 
  39.                     System.out.println("成功连接客户端"); 
  40.                 } 
  41.                 //判断是否为READ事件 
  42.                 if (key.isReadable()){ 
  43.                     SocketChannel socketChannel = (SocketChannel) key.channel(); 
  44.  
  45.                     try { 
  46.                         // 获取以前设置的附件对象,如果没有则新建一个 
  47.                         ByteBuffer buffer = (ByteBuffer) key.attachment(); 
  48.                         if (buffer == null) { 
  49.                             buffer = ByteBuffer.allocate(1024); 
  50.                             key.attach(buffer); 
  51.                         } 
  52.                         // 清空缓冲区 
  53.                         buffer.clear(); 
  54.                         // 将通道中的数据读到缓冲区 
  55.                         int len = socketChannel.read(buffer); 
  56.                         if (len > 0) { 
  57.                             buffer.flip(); 
  58.                             String message = new String(buffer.array(), 0, len); 
  59.                             System.out.println("收到客户端消息:" + message); 
  60.                         } else if (len < 0) { 
  61.                             // 接收到-1,表示连接已关闭 
  62.                             key.cancel(); 
  63.                             socketChannel.close(); 
  64.                             continue
  65.                         } 
  66.                         // 注册写事件,下次向客户端发送消息 
  67.                         socketChannel.register(selector, SelectionKey.OP_WRITE, buffer); 
  68.                     } catch (IOException e) { 
  69.                         // 取消SelectionKey并关闭对应的SocketChannel 
  70.                         key.cancel(); 
  71.                         socketChannel.close(); 
  72.                     } 
  73.                 } 
  74.                 //判断是否为WRITE事件 
  75.                 if (key.isWritable()){ 
  76.                     SocketChannel socketChannel = (SocketChannel) key.channel(); 
  77.                     //获取buffer 
  78.                     ByteBuffer buffer = (ByteBuffer) key.attachment(); 
  79.                     String hello = "你好,坤坤!"
  80.                     //清空buffer 
  81.                     buffer.clear(); 
  82.                     //buffer中写入消息 
  83.                     buffer.put(hello.getBytes()); 
  84.                     buffer.flip(); 
  85.                     //向channel中写入消息 
  86.                     socketChannel.write(buffer); 
  87.                     buffer.clear(); 
  88.                     System.out.println("向客户端发送消息:" + hello); 
  89.                     // 设置下次读写操作,向 Selector 进行注册 
  90.                     socketChannel.register(selector, SelectionKey.OP_READ, buffer); 
  91.                 } 
  92.                 // 移除本次处理的SelectionKey,防止重复处理 
  93.                 iterator.remove(); 
  94.             } 
  95.         } 
  96.  
  97.     } 

  • NioClient
  1. public class NioClient { 
  2.  
  3.     public static void main(String[] args) throws IOException { 
  4.         // 创建SocketChannel并指定ip地址和端口号 
  5.         SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888)); 
  6.         System.out.println("==============NIO客户端启动================"); 
  7.         // 非阻塞模式 
  8.         socketChannel.configureBlocking(false); 
  9.         String hello="你好,靓仔!"
  10.         ByteBuffer buffer = ByteBuffer.wrap(hello.getBytes()); 
  11.         // 向通道中写入数据 
  12.         socketChannel.write(buffer); 
  13.         System.out.println("发送消息:" + hello); 
  14.         buffer.clear(); 
  15.         // 将channel注册到Selector并监听READ事件 
  16.         socketChannel.register(Selector.open(), SelectionKey.OP_READ, buffer); 
  17.         while (true) { 
  18.             // 读取服务端数据 
  19.             if (socketChannel.read(buffer) > 0) { 
  20.                 buffer.flip(); 
  21.                 String msg = new String(buffer.array(), 0, buffer.limit()); 
  22.                 System.out.println("收到服务端消息:" + msg); 
  23.                 break
  24.             } 
  25.         } 
  26.         // 关闭输入流 
  27.         socketChannel.shutdownInput(); 
  28.         // 关闭SocketChannel连接 
  29.         socketChannel.close(); 
  30.     } 
  • 先运行NioServer,再运行NioClient,运行结果:
  1. ===========NIO服务端启动============ 
  2. ===========NIO服务端超时等待============ 
  3. ===========NIO服务端超时等待============ 
  4. 成功连接客户端 
  5. 收到客户端消息:你好,靓仔! 
  6. 向客户端发送消息:你好,坤坤! 
  1. ==============NIO客户端启动================ 
  2. 发送消息:你好,靓仔! 
  3. 收到服务端消息:你好,坤坤! 

我们在这个案例里实现了一个比较简单的Java NIO 客户端服务端通信,里面有两个小的点需要注意,注册到选择器上的通道都必须要为非阻塞模型,同时通过缓冲区传输数据时,必须要调用flip()方法切换为读取模式。

一次搞懂Java三种IO模型

代码流程示意图

Java-NIO中有三个核心概念:**Buffer(缓冲区)、Channel(通道)、Selector(选择器)**。

一次搞懂Java三种IO模型

Java NIO

  • 每个客户端连连接本质上对应着一个Channel通道,每个通道都有自己的Buffer缓冲区来进行读写,这些Channel被Selector选择器管理调度
  • Selector负责轮询所有已注册的Channel,监听到有事件发生,才提交给服务端线程处理,服务端线程不需要做任何阻塞等待,直接在Buffer里处理Channel事件的数据即可,处理完马上结束,或返回线程池供其他客户端事件继续使用。
  • 通过Selector,服务端的一个Thread就可以处理多个客户端的请求
  • Buffer(缓冲区)就是饭店用来存放食材的储藏室,当服务员点餐时,需要从储藏室中取出食材进行制作。
  • Channel(通道)是用于传输数据的车道,就像饭店里的上菜窗口,可以快速把点好的菜品送到客人的桌上。
  • Selector(选择器)就是大堂经理,负责协调服务员、厨师和客人的配合和沟通,以保证整个就餐过程的效率和顺畅。

Java AIO

Java-AIO也被成为NIO2,它是在NIO的基础上,引入了新的异步通道的概念,并提供了异步文件通道和异步套接字的实现。

一次搞懂Java三种IO模型

异步通道的实现体系

它们的主要区别就在于这个异步通道,见名知意:使用异步通道去进行IO操作时,所有操作都为异步非阻塞的,当调用read()/write()/accept()/connect()方法时,本质上都会交由操作系统去完成,比如要接收一个客户端的数据时,操作系统会先将通道中可读的数据先传入read()回调方法指定的缓冲区中,然后再主动通知Java程序去处理。

我们还是先来看个Demo:

  • AioServer
  1. /** 
  2.  * @Author 三分恶 
  3.  * @Date 2023/5/1 
  4.  * @Description AIO服务端 
  5.  */ 
  6. public class AioServer { 
  7.  
  8.     public static void main(String[] args) throws Exception { 
  9.         // 创建异步通道组,处理IO事件 
  10.         AsynchronousChannelGroup group = AsynchronousChannelGroup.withFixedThreadPool(10, Executors.defaultThreadFactory()); 
  11.         //创建异步服务器Socket通道,并绑定端口 
  12.         AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(8888)); 
  13.         System.out.println("=============AIO服务端启动========="); 
  14.  
  15.         // 异步等待接收客户端连接 
  16.         server.accept(nullnew CompletionHandler<AsynchronousSocketChannel, Object>() { 
  17.             // 创建ByteBuffer 
  18.             final ByteBuffer buffer = ByteBuffer.allocate(1024); 
  19.  
  20.             @Override 
  21.             public void completed(AsynchronousSocketChannel channel, Object attachment) { 
  22.                 System.out.println("客户端连接成功"); 
  23.                 try { 
  24.                     buffer.clear(); 
  25.                     // 异步读取客户端发送的消息 
  26.                     channel.read(buffer, nullnew CompletionHandler<Integer, Object>() { 
  27.                         @Override 
  28.                         public void completed(Integer len, Object attachment) { 
  29.                             buffer.flip(); 
  30.                             String message = new String(buffer.array(), 0, len); 
  31.                             System.out.println("收到客户端消息:" + message); 
  32.  
  33.                             // 异步发送消息给客户端 
  34.                             channel.write(ByteBuffer.wrap(("你好,阿坤!").getBytes()), nullnew CompletionHandler<Integer, Object>() { 
  35.                                 @Override 
  36.                                 public void completed(Integer result, Object attachment) { 
  37.                                     // 关闭输出流 
  38.                                     try { 
  39.                                         channel.shutdownOutput(); 
  40.                                     } catch (IOException e) { 
  41.                                         e.printStackTrace(); 
  42.                                     } 
  43.                                 } 
  44.  
  45.                                 @Override 
  46.                                 public void failed(Throwable exc, Object attachment) { 
  47.                                     exc.printStackTrace(); 
  48.                                     try { 
  49.                                         channel.close(); 
  50.                                     } catch (IOException e) { 
  51.                                         e.printStackTrace(); 
  52.                                     } 
  53.                                 } 
  54.                             }); 
  55.                         } 
  56.  
  57.                         @Override 
  58.                         public void failed(Throwable exc, Object attachment) { 
  59.                             exc.printStackTrace(); 
  60.                             try { 
  61.                                 channel.close(); 
  62.                             } catch (IOException e) { 
  63.                                 e.printStackTrace(); 
  64.                             } 
  65.                         } 
  66.                     }); 
  67.                 } catch (Exception e) { 
  68.                     e.printStackTrace(); 
  69.                 } 
  70.                 // 继续异步等待接收客户端连接 
  71.                 server.accept(nullthis); 
  72.             } 
  73.  
  74.             @Override 
  75.             public void failed(Throwable exc, Object attachment) { 
  76.                 exc.printStackTrace(); 
  77.                 // 继续异步等待接收客户端连接 
  78.                 server.accept(nullthis); 
  79.             } 
  80.         }); 
  81.         // 等待所有连接都处理完毕 
  82.         group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); 
  83.     } 
  84.  

  • AioClient
  1. /** 
  2.  * @Author 三分恶 
  3.  * @Date 2023/5/1 
  4.  * @Description AIO客户端 
  5.  */ 
  6. public class AioClient { 
  7.  
  8.     public static void main(String[] args) throws Exception { 
  9.         // 创建异步Socket通道 
  10.         AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); 
  11.         // 异步连接服务器 
  12.         client.connect(new InetSocketAddress("127.0.0.1", 8888), nullnew CompletionHandler<Void, Object>() { 
  13.             // 创建ByteBuffer 
  14.             final ByteBuffer buffer = ByteBuffer.wrap(("你好,靓仔!").getBytes()); 
  15.  
  16.             @Override 
  17.             public void completed(Void result, Object attachment) { 
  18.                 // 异步发送消息给服务器 
  19.                 client.write(buffer, nullnew CompletionHandler<Integer, Object>() { 
  20.                     // 创建ByteBuffer 
  21.                     final ByteBuffer readBuffer = ByteBuffer.allocate(1024); 
  22.  
  23.                     @Override 
  24.                     public void completed(Integer result, Object attachment) { 
  25.                         readBuffer.clear(); 
  26.                         // 异步读取服务器发送的消息 
  27.                         client.read(readBuffer, nullnew CompletionHandler<Integer, Object>() { 
  28.                             @Override 
  29.                             public void completed(Integer result, Object attachment) { 
  30.                                 readBuffer.flip(); 
  31.                                 String msg = new String(readBuffer.array(), 0, result); 
  32.                                 System.out.println("收到服务端消息:" + msg); 
  33.                             } 
  34.  
  35.                             @Override 
  36.                             public void failed(Throwable exc, Object attachment) { 
  37.                                 exc.printStackTrace(); 
  38.                                 try { 
  39.                                     client.close(); 
  40.                                 } catch (IOException e) { 
  41.                                     e.printStackTrace(); 
  42.                                 } 
  43.                             } 
  44.                         }); 
  45.                     } 
  46.  
  47.                     @Override 
  48.                     public void failed(Throwable exc, Object attachment) { 
  49.                         exc.printStackTrace(); 
  50.                         try { 
  51.                             client.close(); 
  52.                         } catch (IOException e) { 
  53.                             e.printStackTrace(); 
  54.                         } 
  55.                     } 
  56.                 }); 
  57.             } 
  58.  
  59.             @Override 
  60.             public void failed(Throwable exc, Object attachment) { 
  61.                 exc.printStackTrace(); 
  62.                 try { 
  63.                     client.close(); 
  64.                 } catch (IOException e) { 
  65.                     e.printStackTrace(); 
  66.                 } 
  67.             } 
  68.         }); 
  69.         // 等待连接处理完毕 
  70.         Thread.sleep(1000); 
  71.         // 关闭输入流和Socket通道 
  72.         client.shutdownInput(); 
  73.         client.close(); 
  74.     } 

  • 看下运行结果
  1. =============AIO服务端启动========= 
  2. 客户端连接成功 
  3. 收到客户端消息:你好,靓仔! 

收到服务端消息:你好,阿坤!

可以看到,所有的操作都是异步进行,通过completed接收异步回调,通过failed接收错误回调。

而且我们发现,相较于之前的NIO而言,AIO其中少了Selector选择器这个核心组件,选择器在NIO中充当了协调者的角色。

但在Java-AIO中,类似的角色直接由操作系统担当,而且不是采用轮询的方式监听IO事件,而是采用一种类似于“订阅-通知”的模式。

一次搞懂Java三种IO模型

Java AIO简图

在AIO中,所有创建的通道都会直接在OS上注册监听,当出现IO请求时,会先由操作系统接收、准备、拷贝好数据,然后再通知监听对应通道的程序处理数据。

Java-AIO这种异步非阻塞式IO也是由操作系统进行支持的,在Windows系统中提供了一种异步IO技术:IOCP(I/O Completion Port,所以Windows下的Java-AIO则是依赖于这种机制实现。不过在Linux系统中由于没有这种异步IO技术,所以Java-AIO在Linux环境中使用的还是epoll这种多路复用技术进行模拟实现的。

因为Linux的异步IO技术实际上不太成熟,所以Java-AIO的实际应用并不是太多,比如大名鼎鼎的网络通信框架Netty就没有采用Java-AIO,而是使用Java-NIO,在代码层面,自行实现异步。

小结

那么这期我们就快速过了一下Java的三种IO机制,它们的特点,我们直接看下图:

一次搞懂Java三种IO模型

Java三种IO模型

我们也发现,虽然Java-NIO、Java-AIO,在性能上比Java-BIO要强很多,但是可以看到,写法上一个比一个难搞,不过好在基本也没人直接用Java-NIO、Java-AIO,如果要进行网络通信,一般都会采用Netty,它对原生的Java-NIO进行了封装优化,接下来,我们会继续走近Netty,敬请期待。

参考:

[1].《Netty权威指南》

[2].https://juejin.cn/post/7130952602350534693#heading-14

[3].https://www.jianshu.com/p/670033e5b916

原文地址:https://mp.weixin.qq.com/s/EMVS4dnHAgOfKg_OCQWEIw

延伸 · 阅读

精彩推荐