BIO的问题

  • 之前使用的socket编程,主要是通过BIO(block)阻塞编程实现
  • 在实现多人聊天室的时候,由于是阻塞编程,那么主机需要为每个一个客户端创建一个接收数据的线程来等待用户发送数据
  • 可以使用线程池来进行处理,但是线程的数量是固定,它也是一个阻塞队列,线程是有上限,虽有可以1个线程负责多个客户端,但是在当前一个客户端连接的时候,这个线程只能为这个一个客户端服务,除非它断开image-20220519111418747.png
  • 所以最好解决方案就是要使用NIO(NIO是非阻塞编程(Not-Block))

    • 只需要提供一个将读取数据的线程,并设置成非阻塞
    • 为每一个客户端请求设置一个buffer缓存,客户端如果有消息就发送到缓存中
    • 使用唯一这一个线程轮询获取所有缓存中的消息,即多路复用image-20220519111443304.png

      NIO编程

      Buffer缓存区

  • 就是临时存储数据的内存空间,可以立即成一个数组

  • buffer基本类型
    • ByteBuffer 、CharBuffer 、ShortBuffer 、IntBuffer 、LongBuffer 、FloatBuffer 、DoubleBuffer
  • buffer基本使用image-20220519141647926.png

    • 创建一个内存空间 :::info ByteBuffer buf = ByteBuffer.allocate(1024); :::

    • 可以向buffer放入数据 :::info buf.put(“你好同学”.getBytes(StandardCharsets.UTF_8)); :::

    • 在读取buffer之前需要先进行反转 :::info buf.flip();//反转 :::

    • 反转之后才可以读取数据 :::info byte[] result = new byte[buf.limit()];//使用limit创建读取数据的数组,不能超过limit
      buf.get(result); :::

    • 设置可以重复读取 :::info buf.rewind(); :::

    • 需要重新读取数据的时候最好清除一下原有的数据 :::info buf.clear(); :::

      Channel通道

  • channel的主要作用就是从buffer中读取或者是写入数据

  • channel的使用和流有点类似
    • 通道即可读,也可以写;流是分成了写入和输出流
    • 通道是可以设置成非阻塞的;流是阻塞的
    • 通道主要是从buffer中读写数据
  • 各种通道
    • FileChannel:文件通道
    • DatagramChannel:udp通道
    • SocketChannel:一般的网络通信通道
    • ServerSocketChannel:专门监听连接的socket通道
  • 文件通道的基本使用 File file = new File(“a.txt”);

    • 实现文件的写入数据

      1. String data ="测试数据";
      2. //创建一个buffer,将数据放入到buffer中
      3. ByteBuffer buffer = ByteBuffer.allocate(1024);
      4. //将数据先放入到buffer中
      5. buffer.put(data.getBytes(StandardCharsets.UTF_8));
      6. //马上翻转
      7. buffer.flip();
      8. //通过文件打开输出流
      9. FileOutputStream fos = new FileOutputStream(file);
      10. //通过流获取一个通道
      11. FileChannel channel = fos.getChannel();
      12. //将buffer中的数据写入到文件中
      13. channel.write(buffer);
      14. //关闭通道
      15. channel.close();
    • 从文件中读取数据

      1. File file = new File("a.txt");
      2. //通过流打开channel
      3. FileInputStream fis = new FileInputStream(file);
      4. FileChannel fileChannel = fis.getChannel();
      5. //创建buffer
      6. ByteBuffer buffer = ByteBuffer.allocate(1024);
      7. //将文件中的数据,通过channel读取到buffer中
      8. fileChannel.read(buffer);
      9. buffer.flip();
      10. //将buffer中的数据输出
      11. System.out.println(new String(buffer.array(),0,buffer.remaining()));
  • 文件的复制功能

    1. //将a复制到b
    2. File afile = new File("a.txt");
    3. File bfile = new File("b.txt");
    4. //打开流
    5. FileInputStream fis = new FileInputStream(afile);
    6. FileOutputStream fos = new FileOutputStream(bfile);
    7. //打开通道
    8. FileChannel isChannel = fis.getChannel();
    9. FileChannel osChannel = fos.getChannel();
    10. //创建一个buffer
    11. ByteBuffer buffer = ByteBuffer.allocate(1024);
    12. int flag =0;
    13. while(flag !=-1){
    14. buffer.clear();//将缓存重置成初始状态
    15. flag = isChannel.read(buffer);
    16. buffer.flip();//反转
    17. osChannel.write(buffer);
    18. }
    19. isChannel.close();
    20. osChannel.close();
    21. fis.close();
    22. fos.close();
    23. System.out.println("复制成功");

    Selector选择器

  • 就是整个NIO的核心,由其实现了多路复用的功能

  • 将所有的channel注册到selector之上,由selector遍历所有出现了数据变化的channel来执行操作
  • 实现socket连接的操作

    • 服务器端

      1. public static void main(String[] args) throws IOException {
      2. //创建一个Selector
      3. Selector selector = Selector.open();
      4. //将需要监听的channel注册到selector之上
      5. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
      6. //让ServerSocketChannel绑定端口
      7. serverSocketChannel.bind(new InetSocketAddress(8989));
      8. //设置channel是非阻塞
      9. serverSocketChannel.configureBlocking(false);
      10. //主要用户接收连接的用户的
      11. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
      12. //接收进入到用户
      13. while(selector.select()>0){//会阻塞代码
      14. SocketChannel socketChannel = serverSocketChannel.accept();
      15. System.out.println("有人进来了");
      16. }
      17. }
    • 客户端直接连接服务器端

      1. public static void main(String[] args) throws IOException {
      2. //打开连接服务器的通道
      3. SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8989));
      4. }
  • 单项的消息发送

    • 服务器端通过selector判断channel变化的情况

      1. public static void main(String[] args) throws IOException {
      2. //创建一个Selector
      3. Selector selector = Selector.open();
      4. //将需要监听的channel注册到selector之上
      5. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
      6. //让ServerSocketChannel绑定端口
      7. serverSocketChannel.bind(new InetSocketAddress(8989));
      8. //设置channel是非阻塞
      9. serverSocketChannel.configureBlocking(false);
      10. //主要用户接收连接的用户的
      11. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
      12. System.out.println("服务器启动了");
      13. //接收进入到用户
      14. while(selector.select()>0){//会阻塞代码
      15. //每个有监听的channel发生变化的时候,会将所有发送的状态放入到一个集合中
      16. Iterator<SelectionKey> it = selector.selectedKeys().iterator();
      17. while(it.hasNext()){
      18. SelectionKey selectionKey = it.next();//获取当前channel变化的key
      19. if(selectionKey.isAcceptable()){//判断是有新的连接了
      20. System.out.println("有人进来了");
      21. //获取连接的channel
      22. SocketChannel socketChannel = serverSocketChannel.accept();//非阻塞
      23. socketChannel.configureBlocking(false);
      24. //将channel注册到selector
      25. socketChannel.register(selector,SelectionKey.OP_READ);
      26. }else if(selectionKey.isReadable()){//判断是读取状态
      27. //获取当前key对应的channel
      28. SocketChannel channel = (SocketChannel) selectionKey.channel();
      29. ByteBuffer buffer = ByteBuffer.allocate(1024);
      30. channel.read(buffer);//非阻塞
      31. buffer.flip();
      32. System.out.println(new String(buffer.array(),0,buffer.remaining()));
      33. }
      34. }
      35. //移除当前key的迭代器
      36. it.remove();
      37. }
      38. }
    • 客户端发送请求

      1. public static void main(String[] args) throws IOException {
      2. //打开连接服务器的通道
      3. SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8989));
      4. ByteBuffer buffer = ByteBuffer.allocate(1024);
      5. Scanner scanner = new Scanner(System.in);
      6. while (true){
      7. String msg = scanner.next();
      8. //将msg放入到buffer中
      9. buffer.put(msg.getBytes(StandardCharsets.UTF_8));
      10. buffer.flip();
      11. //通过channel将数据发送
      12. socketChannel.write(buffer);
      13. buffer.clear();//重置游标
      14. }
      15. }

      小结

      BIO

  • 阻塞编程,代码必须是一行一行执行,简单好操作

  • 在处理并发问题的情况下,需要被每个连接都安排一个线程,极大的影响的服务器的性能
  • 适用于线程固定的,对服务器资源要求较高的场景

    NIO

  • 非阻塞编程,主要使用是多路复用的概念,让每个线程通信消息,放入到buffer缓存中,然后连接buffer缓存的channel注册到selector上。

  • selector进行要轮询状态,会监听所有的注册的channel变化情况,一旦有发送变化就对其进行处理
  • selector在监听的过程中还是会阻塞
  • 适合线程伸缩大,并发高,对资源要求不多的情况下,处理业务时间要短

    AIO

  • 异步非编程,在NIO的基础上,并不是通过轮询/监听的方式,而是让buffer发送变化了之后,主动通知系统进行处理

  • 将缓存的数据存储在操作系统的内核中,处理完毕之后再通知用户线程
  • 适合对资源要求高,处理业务时间长的操作,比如文件上传image-20220519175010889.png