BIO、NIO、Netty 忘是亡心i 2023-07-16 14:59 53阅读 0赞 ### 文章目录 ### * * BIO * NIO * * * 通道(Channel)和缓冲区(Buffer) * 缓冲区 * 直接缓冲区和非直接缓冲区 * 通道 * 使用NIO实现文件读写 * 网络NIO * * Selector选择器 * SelectionKey * serverSocketChannel * SocketChannel * NIO和BIO的区别 * Netty * * * 线程模型和异步模型 ## BIO ## BIO(basic IO或者block IO),主要应用于文件IO和网络IO,BIO和NIO最大的不同是BIO是阻塞式的,而NIO是非阻塞式的。 下面是一个BIO实现基于TCP网络IO的例子: TCP服务端: public class TCPServer { public static void main(String[] args) throws IOException { // 服务端socket连接 ServerSocket ss = new ServerSocket(9999); while(true) { // 阻塞监听客户端连接 Socket s = ss.accept(); // 阻塞等待客户端写入数据到输入流 InputStream is = s.getInputStream(); byte[] b = new byte[1024]; // 将用户写入的数据保存到数组中 is.read(b); // 获取客户端IP String clientIP = s.getInetAddress().getHostAddress(); System.out.println(clientIP + ":" + Arrays.toString(b)); // 获取输出流 OutputStream os = s.getOutputStream(); // 写数据到输出流给客户端 os.write("I am listening 9999 port!".getBytes()); // 关闭 s.close(); } } } TCP客户端: public class TCPClient { public static void main(String[] args) throws IOException { while(true) { // 打开客户端socket Socket s = new Socket("127.0.0.1", 9999); // 阻塞获取输出流 OutputStream os = s.getOutputStream(); System.out.println("please input:"); Scanner sc = new Scanner(System.in); String msg = sc.nextLine(); // 写数据到输出流 os.write(msg.getBytes()); // 获取输入流,读取服务端写的数据,阻塞 InputStream is = s.getInputStream(); byte[] b = new byte[1024]; is.read(b); System.out.println("server response:" + Arrays.toString(b)); s.close(); } } } ## NIO ## NIO(New IO或者是non-blocking IO),NIO和BIO作用和目的相同,但是他们的实现方式不同。 * BIO采用流的方式处理数据;NIO采用块的方式处理数据,这种一块处理数据的方式 IO效率比流IO效率高很多 * BIO是阻塞式的,NIO是非阻塞式的 * BIO流的方式是单向的,即有一个输入流和一个输出流;NIO采用的缓冲区是双向的,数据可以从缓冲区写入到通道中,也可以从通道读入到缓冲区 在面向缓冲区的NIO中,磁盘\\网络数据和应用程序进行传输要建立**通道**,通道可以想象成一条铁路,用于两个地点的连接,实际上是通过火车运输的。类似的,通道也是,它只是磁盘/网络文件和程序之间建立的一个连接,本身不传输数据,传输数据用的是缓冲区(类似火车)。通道是双向的,数据放到缓冲区中进行传输,磁盘/网络数据传递给应用程序,应用程序取出数据后可以放入新的数据到缓冲区中,然后缓冲区又把数据发送到磁盘/网络上。 #### 通道(Channel)和缓冲区(Buffer) #### **NIO系统的核心在于:通道(Channel)、缓冲区(Buffer)和选择器(selector)**。通道表示打开到IO设备(如文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,**通道负责传输数据,缓冲区负责存储数据。** ![在这里插入图片描述][20200321214023923.png] #### 缓冲区 #### 缓冲区实际上是一个容器,是一个特殊数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。 根据数据类型不同,提供了相应类型的缓冲区: * ByteBuffer * CharBuffer * ShortBuffer * IntBuffer * LongBuffer * FloatBuffer 缓冲区中的基本方法和属性如下示例: public class Test { public static void main(String[] args) { IntBuffer buf = IntBuffer.allocate(1024); /** * 属性: * position:当往缓冲区中写数据的时候,position指向当前缓冲区 * 可用位置头部;当读取数据模式时,position指向缓冲区数据的头部 * limit:当写数据时,limit指向缓冲区尾部;当读数据模式时,指向缓冲区 * 有数据区的尾部 * capacity:指向缓冲区尾部 */ System.out.println("---allocate---"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); // 写数据到缓冲区 buf.put(1); buf.put(2); System.out.println("---put---"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); // 切换到读模式 buf.flip(); System.out.println("---flip---"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); // 读取缓冲区中的数据 int[] dst = new int[buf.limit()]; buf.get(dst); System.out.println("---get---"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); // 重新读取缓冲区数据,使position重新执行数据头部 buf.rewind(); System.out.println("---rewind---"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); dst = new int[buf.limit()]; buf.get(dst); // 如果不进行rewind()操作,则这里会抛异常:java.nio.BufferUnderflowException // 清空缓冲区,但是缓冲区中的数据依旧存在,只是数据不能再被读取 buf.clear(); System.out.println("---clear---"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); } } 结果: ---allocate--- 0 1024 1024 ---put--- 2 1024 1024 ---flip--- 0 2 1024 ---get--- 2 2 1024 ---rewind--- 0 2 1024 ---clear--- 0 1024 1024 #### 直接缓冲区和非直接缓冲区 #### * 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM内存中。 通过allocate()方法分配非直接缓冲区 * 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。 通过allocateDirect()方法分配直接缓冲区 可以提高数据读取效率,但是分配和销毁物理内存映射区会耗费性能 > 通过调用缓冲区对象的isDirect()方法来获取是那种缓冲区(直接缓冲区返回true,非直接缓冲区返回false) 非直接缓冲区: 数据先从物理磁盘读到内核地址空间,再copy到用户地址空间(JVM) ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70] 直接缓冲区: NIO增加了直接缓冲区,去掉了内核地址空间和用户地址空间直接的copy,而是创建了一个物理内存映射文件,用来存储数据。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70 1] #### 通道 #### 类似于BIO中的流,如FileInputStream对象,通道用来建立IO源与目标(文件、网络套接字、硬件设备等)的一个连接,通道本身不能访问数据,而是只能与buffer进行交互。 BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,既可以用来进行读操作,也可以用来写操作。常用Channel类: * FileChannel:用于文件的数据读写 * DatagramChannel:用于UDP的数据读写 * ServerSocketChannel:用于服务端TCP的数据读写 * SocketChannel用于客户端TCP的数据读写 #### 使用NIO实现文件读写 #### // 通过NIO实现文件IO public class TestNIO { // 往本地文件中写数据 @Test public void test1() throws IOException { // 创建输出流 FileOutputStream fos = new FileOutputStream("basic.txt"); // 从流中得到一个通道 FileChannel fc = fos.getChannel(); // 创建一个缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); String str = "hello"; // 把一个字节数组存入缓冲区 buffer.put(str.getBytes()); // 调整缓存区的position和limit的位置,切换到读模式 buffer.flip(); // 把缓冲区写到通道中 fc.write(buffer); // 关闭 fos.close(); } // 从本地文件读数据 @Test public void test2() throws IOException { File file = new File("basic.txt"); // 创建输入流 FileInputStream fis = new FileInputStream("basic.txt"); // 获取管道 FileChannel fc = fis.getChannel(); // 创建缓冲区 ByteBuffer buffer = ByteBuffer.allocate((int)file.length()); // 从通道读取数据存入缓冲区 fc.read(buffer); System.out.println(Arrays.toString(buffer.array())); } // 使用NIO实现文件复制 @Test public void test3() throws IOException { // 创建输入缓冲区和输入缓冲区 FileInputStream fis = new FileInputStream("basic.txt"); FileOutputStream fop = new FileOutputStream("basic2.txt"); // 获取输入缓冲区和输出缓冲区的管道 FileChannel channel1 = fis.getChannel(); FileChannel channel2 = fop.getChannel(); // 缓冲区数据的复制(把输入缓冲区的数据写到输出缓冲区中) channel2.transferFrom(channel1, 0, channel1.size()); fis.close(); fop.close(); } } 上面用到的FileChannel并不支持非阻塞操作,NIO的主要用途是进行网络IO。 #### 网络NIO #### Java NIO中的网络通道是非阻塞IO的实现,基于事件驱动,**非常适用于服务器需要维持大量连接,但是数据交换量不大的情况**,例如一些技术通信的服务等等。 在Java中编写Socket服务器,通常有以下几种模式: * 一个客户端连接用一个线程 优点:编程简单 缺点:连接非常多时,分配的线程也会非常多,服务器可能会因为资源耗尽而崩溃 * 把每一个客户端连接交给一个拥有固定数量线程的连接池 优点:编程简单,可以处理大量连接 缺点:线程的开销非常大,如果连接非常多,排队现象会比较严重 * 使用java的NIO,用非阻塞的IO方式处理。这种模式可以用一个线程处理大量的客户端连接。 问题: * NIO如何做到非阻塞? * NIO如何做到一个线程可以处理多个客户端连接? Java NIO能做到如上两点,主要是下面这四个核心类及其API。 ##### Selector选择器 ##### 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样就可以只用一个单线程去管理多个通道,即多个连接。这样使得只有连接真正有读写事件发生时,才会调用函数来进行读写操作,**大大减小了系统开销**,**并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了线程之间的上下文切换导致的开销**。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70 2] 该类的常用方法: ![在这里插入图片描述][20200327160302899.png] ##### SelectionKey ##### SelectionKey代表了Selector和serverSocketChannel的注册关系,可以理解为读、写、连接事件,一共有四种: * int OP\_ACCEPT,有新的网络连接可以accept,值为16 * int OP\_CONNECT,代表连接已经建立,值为8 * int OP\_READ、int OP\_WRITE,代表读、写操作,值为1和4 该类的常用方法: ![在这里插入图片描述][20200327160441754.png] ##### serverSocketChannel ##### serverSocketChannel用来在服务器端监听新的客户端Socket连接。 该类的常用方法: ![在这里插入图片描述][20200327160601733.png] ##### SocketChannel ##### 网络IO通道,具体负责进行读写操作。NIO总是把缓冲区的数据写入通道,或者把通道里的数据读出到缓冲区。 该类的常用方法: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70 3] 示例: 服务器端: import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; public class NIOServer { public static void main(String[] args) throws IOException { // 打开服务端负责监听客户端连接的管道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 打开一个选择器,监听客户端的行为 Selector selector = Selector.open(); // 给服务端绑定端口 serverSocketChannel.bind(new InetSocketAddress(9999)); // 设置NIO为非阻塞模型 serverSocketChannel.configureBlocking(false); // 将serverSocketChannel注册到选择器中国 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); Iterator<SelectionKey> iterator; // 开始处理客户端的连接、读、写操作 while (true) { if (selector.select(2000) == 0) { System.out.println("server:没有客户端事件,服务端可以处理其他的任务"); continue; } // 获取selectionKey集合的迭代器,遍历这个集合 // 这里一定要使用迭代器,因为涉及到一遍遍历一遍删除的情况 iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); // 客户端连接事件 if(key.isAcceptable()) { SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } // 读取客户端数据 if(key.isReadable()) { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); channel.read(buffer); System.out.println("客户端数据:"+new String(buffer.array())); } // 将处理过的事件从集合中移除 selector.selectedKeys().remove(key); } } } } 客户端: import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class NIOClient { public static void main(String[] args) throws IOException { // 得到一个网络通道 SocketChannel channel = SocketChannel.open(); // 设置NIO为非阻塞模式 channel.configureBlocking(false); // 服务端的ip和端口号 InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999); // 尝试连接 if(! channel.connect(address)) { // 一直尝试连接 while (! channel.finishConnect()) { // 连接的过程中,客户端可以处理其他任务,这就是NIO非阻塞模型的优势 System.out.println("客户端连接服务端的同时,可以处理其他任务"); } } String msg = "hello server"; ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes()); // 将缓冲区数据写入通道 channel.write(byteBuffer); // 不让客户端程序退出 System.in.read(); } } ## NIO和BIO的区别 ## * BIO采用流的方式处理数据;NIO采用块的方式处理数据,这种一块处理数据的方式 IO效率比流IO效率高很多 * BIO流的方式是单向的,即有一个输入流和一个输出流;NIO采用的缓冲区是双向的,数据可以从缓冲区写入到通道中,也可以从通道读入到缓冲区 * BIO是阻塞式的,NIO是非阻塞式的 * BIO方式适合用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中 * NIO方式使用于连接数目多且连接比较短的架构,如聊天服务器,并发局限于应用中,编程复杂 > 还有一种IO模型是AIO,A表示异步Synchronized,即异步非阻塞模型。对于NIO同步非阻塞模型中,需要轮询检查是否有客户端事件需要处理。而对于AIO异步非阻塞模型中,客户端有事件发生时会去通知服务端进行处理,而不用服务端不停地轮询检测。AIO方式适用于连接数目多且连接比较长的架构,如相册服务器,充分调用OS参与并发操作,编程比较复杂。 ## Netty ## Netty是一个Java开源框架,Netty提供异步的、基于事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠的网络IO程序。Netty基于NIO开发的。Netty框架的目的就是使业务逻辑和网络编码部分分离出来。 #### 线程模型和异步模型 #### 讲解Netty之前,先来了解下Java的线程模型: * 单线程模型,服务端只用一个线程通过多路复用搞定所有的IO操作(连接、读、写),编码简单,但是如果客户端连接数量较多,将无法支持,如上面网络NIO的示例,只有一个selector线程处理客户端的连接、读、写事件 * 线程池模型 ,服务端采用一个线程专门处理客户端的连接请求,采用一组线程(放在线程池中)负责IO操作。在绝大多数场景下,该模型都能够满足使用。 * Netty模型,改进线程池模型。采用了两个线程池,一个线程池(称为Boos Group)中的一组线程负责客户端的连接请求,另一个线程池(称为Worker Group)中的一组线程负责客户端的IO操作。NioEventLoop表示一个不断循环执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的secket网络通道。NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终是由IO程序NioEventLoop负责。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70 4] 上面就是Netty的线程模型,下面来说说Netty的异步模型。 Netty的异步模型是建立在future和callback上的。future的核心思想是:假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适。那么可以在调用fun的时候,立马返回一个Future,然后程序可以继续往下执行其他线程,后续可以通过future去监控方法fun的处理过程。当fun方法执行完成返回后,通过callback方法去接受返回并处理相应逻辑。 [20200321214023923.png]: /images/20230528/de079c65e593481b9ad04e6b70ee319d.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70]: /images/20230528/1dbf874259024c9aa229d1e88bfc75bd.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70 1]: /images/20230528/1afdb6e7e8ed4d488898dc4a1c6c52bc.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70 2]: /images/20230528/3899102c8ac2446ea1fa258cba6e5ea0.png [20200327160302899.png]: /images/20230528/bdc25150a1ec401f938ae18ecbc85d66.png [20200327160441754.png]: /images/20230528/0574545816a942a5b92e394453f6b0d0.png [20200327160601733.png]: /images/20230528/55e0f0d6e04f4cb6ad100e409c66b40d.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70 3]: /images/20230528/7637bd35ac1340e787130c25ebac5106.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0lUXzEw_size_16_color_FFFFFF_t_70 4]: /images/20230528/0096fdaa96b44cf08aab3e59dd5d5611.png
还没有评论,来说两句吧...