BIO、NIO、Netty

忘是亡心i 2023-07-16 14:59 105阅读 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服务端:

  1. public class TCPServer {
  2. public static void main(String[] args) throws IOException {
  3. // 服务端socket连接
  4. ServerSocket ss = new ServerSocket(9999);
  5. while(true) {
  6. // 阻塞监听客户端连接
  7. Socket s = ss.accept();
  8. // 阻塞等待客户端写入数据到输入流
  9. InputStream is = s.getInputStream();
  10. byte[] b = new byte[1024];
  11. // 将用户写入的数据保存到数组中
  12. is.read(b);
  13. // 获取客户端IP
  14. String clientIP = s.getInetAddress().getHostAddress();
  15. System.out.println(clientIP + ":" + Arrays.toString(b));
  16. // 获取输出流
  17. OutputStream os = s.getOutputStream();
  18. // 写数据到输出流给客户端
  19. os.write("I am listening 9999 port!".getBytes());
  20. // 关闭
  21. s.close();
  22. }
  23. }
  24. }

TCP客户端:

  1. public class TCPClient {
  2. public static void main(String[] args) throws IOException {
  3. while(true) {
  4. // 打开客户端socket
  5. Socket s = new Socket("127.0.0.1", 9999);
  6. // 阻塞获取输出流
  7. OutputStream os = s.getOutputStream();
  8. System.out.println("please input:");
  9. Scanner sc = new Scanner(System.in);
  10. String msg = sc.nextLine();
  11. // 写数据到输出流
  12. os.write(msg.getBytes());
  13. // 获取输入流,读取服务端写的数据,阻塞
  14. InputStream is = s.getInputStream();
  15. byte[] b = new byte[1024];
  16. is.read(b);
  17. System.out.println("server response:" + Arrays.toString(b));
  18. s.close();
  19. }
  20. }
  21. }

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设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,通道负责传输数据,缓冲区负责存储数据。
在这里插入图片描述

缓冲区

缓冲区实际上是一个容器,是一个特殊数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。

根据数据类型不同,提供了相应类型的缓冲区:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer

缓冲区中的基本方法和属性如下示例:

  1. public class Test {
  2. public static void main(String[] args) {
  3. IntBuffer buf = IntBuffer.allocate(1024);
  4. /** * 属性: * position:当往缓冲区中写数据的时候,position指向当前缓冲区 * 可用位置头部;当读取数据模式时,position指向缓冲区数据的头部 * limit:当写数据时,limit指向缓冲区尾部;当读数据模式时,指向缓冲区 * 有数据区的尾部 * capacity:指向缓冲区尾部 */
  5. System.out.println("---allocate---");
  6. System.out.println(buf.position());
  7. System.out.println(buf.limit());
  8. System.out.println(buf.capacity());
  9. // 写数据到缓冲区
  10. buf.put(1);
  11. buf.put(2);
  12. System.out.println("---put---");
  13. System.out.println(buf.position());
  14. System.out.println(buf.limit());
  15. System.out.println(buf.capacity());
  16. // 切换到读模式
  17. buf.flip();
  18. System.out.println("---flip---");
  19. System.out.println(buf.position());
  20. System.out.println(buf.limit());
  21. System.out.println(buf.capacity());
  22. // 读取缓冲区中的数据
  23. int[] dst = new int[buf.limit()];
  24. buf.get(dst);
  25. System.out.println("---get---");
  26. System.out.println(buf.position());
  27. System.out.println(buf.limit());
  28. System.out.println(buf.capacity());
  29. // 重新读取缓冲区数据,使position重新执行数据头部
  30. buf.rewind();
  31. System.out.println("---rewind---");
  32. System.out.println(buf.position());
  33. System.out.println(buf.limit());
  34. System.out.println(buf.capacity());
  35. dst = new int[buf.limit()];
  36. buf.get(dst); // 如果不进行rewind()操作,则这里会抛异常:java.nio.BufferUnderflowException
  37. // 清空缓冲区,但是缓冲区中的数据依旧存在,只是数据不能再被读取
  38. buf.clear();
  39. System.out.println("---clear---");
  40. System.out.println(buf.position());
  41. System.out.println(buf.limit());
  42. System.out.println(buf.capacity());
  43. }
  44. }

结果:

  1. ---allocate---
  2. 0
  3. 1024
  4. 1024
  5. ---put---
  6. 2
  7. 1024
  8. 1024
  9. ---flip---
  10. 0
  11. 2
  12. 1024
  13. ---get---
  14. 2
  15. 2
  16. 1024
  17. ---rewind---
  18. 0
  19. 2
  20. 1024
  21. ---clear---
  22. 0
  23. 1024
  24. 1024

直接缓冲区和非直接缓冲区

  • 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM内存中。
    通过allocate()方法分配非直接缓冲区
  • 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。
    通过allocateDirect()方法分配直接缓冲区
    可以提高数据读取效率,但是分配和销毁物理内存映射区会耗费性能

通过调用缓冲区对象的isDirect()方法来获取是那种缓冲区(直接缓冲区返回true,非直接缓冲区返回false)

非直接缓冲区:
数据先从物理磁盘读到内核地址空间,再copy到用户地址空间(JVM)
在这里插入图片描述
直接缓冲区:
NIO增加了直接缓冲区,去掉了内核地址空间和用户地址空间直接的copy,而是创建了一个物理内存映射文件,用来存储数据。
在这里插入图片描述

通道

类似于BIO中的流,如FileInputStream对象,通道用来建立IO源与目标(文件、网络套接字、硬件设备等)的一个连接,通道本身不能访问数据,而是只能与buffer进行交互。
BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,既可以用来进行读操作,也可以用来写操作。常用Channel类:

  • FileChannel:用于文件的数据读写
  • DatagramChannel:用于UDP的数据读写
  • ServerSocketChannel:用于服务端TCP的数据读写
  • SocketChannel用于客户端TCP的数据读写

使用NIO实现文件读写

  1. // 通过NIO实现文件IO
  2. public class TestNIO {
  3. // 往本地文件中写数据
  4. @Test
  5. public void test1() throws IOException {
  6. // 创建输出流
  7. FileOutputStream fos = new FileOutputStream("basic.txt");
  8. // 从流中得到一个通道
  9. FileChannel fc = fos.getChannel();
  10. // 创建一个缓冲区
  11. ByteBuffer buffer = ByteBuffer.allocate(1024);
  12. String str = "hello";
  13. // 把一个字节数组存入缓冲区
  14. buffer.put(str.getBytes());
  15. // 调整缓存区的position和limit的位置,切换到读模式
  16. buffer.flip();
  17. // 把缓冲区写到通道中
  18. fc.write(buffer);
  19. // 关闭
  20. fos.close();
  21. }
  22. // 从本地文件读数据
  23. @Test
  24. public void test2() throws IOException {
  25. File file = new File("basic.txt");
  26. // 创建输入流
  27. FileInputStream fis = new FileInputStream("basic.txt");
  28. // 获取管道
  29. FileChannel fc = fis.getChannel();
  30. // 创建缓冲区
  31. ByteBuffer buffer = ByteBuffer.allocate((int)file.length());
  32. // 从通道读取数据存入缓冲区
  33. fc.read(buffer);
  34. System.out.println(Arrays.toString(buffer.array()));
  35. }
  36. // 使用NIO实现文件复制
  37. @Test
  38. public void test3() throws IOException {
  39. // 创建输入缓冲区和输入缓冲区
  40. FileInputStream fis = new FileInputStream("basic.txt");
  41. FileOutputStream fop = new FileOutputStream("basic2.txt");
  42. // 获取输入缓冲区和输出缓冲区的管道
  43. FileChannel channel1 = fis.getChannel();
  44. FileChannel channel2 = fop.getChannel();
  45. // 缓冲区数据的复制(把输入缓冲区的数据写到输出缓冲区中)
  46. channel2.transferFrom(channel1, 0, channel1.size());
  47. fis.close();
  48. fop.close();
  49. }
  50. }

上面用到的FileChannel并不支持非阻塞操作,NIO的主要用途是进行网络IO。

网络NIO

Java NIO中的网络通道是非阻塞IO的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但是数据交换量不大的情况,例如一些技术通信的服务等等。

在Java中编写Socket服务器,通常有以下几种模式:

  • 一个客户端连接用一个线程
    优点:编程简单
    缺点:连接非常多时,分配的线程也会非常多,服务器可能会因为资源耗尽而崩溃
  • 把每一个客户端连接交给一个拥有固定数量线程的连接池
    优点:编程简单,可以处理大量连接
    缺点:线程的开销非常大,如果连接非常多,排队现象会比较严重
  • 使用java的NIO,用非阻塞的IO方式处理。这种模式可以用一个线程处理大量的客户端连接。

问题:

  • NIO如何做到非阻塞?
  • NIO如何做到一个线程可以处理多个客户端连接?

Java NIO能做到如上两点,主要是下面这四个核心类及其API。

Selector选择器

能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样就可以只用一个单线程去管理多个通道,即多个连接。这样使得只有连接真正有读写事件发生时,才会调用函数来进行读写操作,大大减小了系统开销并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了线程之间的上下文切换导致的开销
在这里插入图片描述
该类的常用方法:
在这里插入图片描述

SelectionKey

SelectionKey代表了Selector和serverSocketChannel的注册关系,可以理解为读、写、连接事件,一共有四种:

  • int OP_ACCEPT,有新的网络连接可以accept,值为16
  • int OP_CONNECT,代表连接已经建立,值为8
  • int OP_READ、int OP_WRITE,代表读、写操作,值为1和4

该类的常用方法:
在这里插入图片描述

serverSocketChannel

serverSocketChannel用来在服务器端监听新的客户端Socket连接。
该类的常用方法:
在这里插入图片描述

SocketChannel

网络IO通道,具体负责进行读写操作。NIO总是把缓冲区的数据写入通道,或者把通道里的数据读出到缓冲区。
该类的常用方法:
在这里插入图片描述

示例:
服务器端:

  1. import java.io.IOException;
  2. import java.net.InetSocketAddress;
  3. import java.nio.ByteBuffer;
  4. import java.nio.channels.*;
  5. import java.util.Iterator;
  6. public class NIOServer {
  7. public static void main(String[] args) throws IOException {
  8. // 打开服务端负责监听客户端连接的管道
  9. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  10. // 打开一个选择器,监听客户端的行为
  11. Selector selector = Selector.open();
  12. // 给服务端绑定端口
  13. serverSocketChannel.bind(new InetSocketAddress(9999));
  14. // 设置NIO为非阻塞模型
  15. serverSocketChannel.configureBlocking(false);
  16. // 将serverSocketChannel注册到选择器中国
  17. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
  18. Iterator<SelectionKey> iterator;
  19. // 开始处理客户端的连接、读、写操作
  20. while (true) {
  21. if (selector.select(2000) == 0) {
  22. System.out.println("server:没有客户端事件,服务端可以处理其他的任务");
  23. continue;
  24. }
  25. // 获取selectionKey集合的迭代器,遍历这个集合
  26. // 这里一定要使用迭代器,因为涉及到一遍遍历一遍删除的情况
  27. iterator = selector.selectedKeys().iterator();
  28. while (iterator.hasNext()) {
  29. SelectionKey key = iterator.next();
  30. // 客户端连接事件
  31. if(key.isAcceptable()) {
  32. SocketChannel socketChannel = serverSocketChannel.accept();
  33. socketChannel.configureBlocking(false);
  34. socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
  35. }
  36. // 读取客户端数据
  37. if(key.isReadable()) {
  38. SocketChannel channel = (SocketChannel) key.channel();
  39. ByteBuffer buffer = (ByteBuffer) key.attachment();
  40. channel.read(buffer);
  41. System.out.println("客户端数据:"+new String(buffer.array()));
  42. }
  43. // 将处理过的事件从集合中移除
  44. selector.selectedKeys().remove(key);
  45. }
  46. }
  47. }
  48. }

客户端:

  1. import java.io.IOException;
  2. import java.net.InetSocketAddress;
  3. import java.nio.ByteBuffer;
  4. import java.nio.channels.SocketChannel;
  5. public class NIOClient {
  6. public static void main(String[] args) throws IOException {
  7. // 得到一个网络通道
  8. SocketChannel channel = SocketChannel.open();
  9. // 设置NIO为非阻塞模式
  10. channel.configureBlocking(false);
  11. // 服务端的ip和端口号
  12. InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
  13. // 尝试连接
  14. if(! channel.connect(address)) {
  15. // 一直尝试连接
  16. while (! channel.finishConnect()) {
  17. // 连接的过程中,客户端可以处理其他任务,这就是NIO非阻塞模型的优势
  18. System.out.println("客户端连接服务端的同时,可以处理其他任务");
  19. }
  20. }
  21. String msg = "hello server";
  22. ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
  23. // 将缓冲区数据写入通道
  24. channel.write(byteBuffer);
  25. // 不让客户端程序退出
  26. System.in.read();
  27. }
  28. }

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负责。
    在这里插入图片描述
    上面就是Netty的线程模型,下面来说说Netty的异步模型。
    Netty的异步模型是建立在future和callback上的。future的核心思想是:假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适。那么可以在调用fun的时候,立马返回一个Future,然后程序可以继续往下执行其他线程,后续可以通过future去监控方法fun的处理过程。当fun方法执行完成返回后,通过callback方法去接受返回并处理相应逻辑。

发表评论

表情:
评论列表 (有 0 条评论,105人围观)

还没有评论,来说两句吧...

相关阅读