• BIO、NIO、AIO


    一、概念

    阻塞和非阻塞: 是针对发起的IO请求操作后 是否立刻返回一个标志信息而不让请求线程等待,当数据准备未完成时,请求线程的状态:

    阻塞:往往需要等待数据准备好过后才处理其他的事情,否则一直阻塞等待。

    非阻塞:无论数据是否准备好,都不会阻塞等待。

    同步和异步:同步和异步一般是面向操作系统和应用程序对IO操作的层面上来区别的。

    同步时,应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某个方法上,直到数据准备就绪;或者采用轮询的策略,实时检查数据的就绪状态,如果就绪则获取数据。

    异步时,则所有的IO操作都交给操作系统处理,与我们的应用程序没有直接关系,当操作系统完成了IO读写操作时,会给我们的应用程序发送通知,我们应用程序直接拿走数据即可。

    二、BIO

    介绍:BIO(Blocking I/O) 同步阻塞IO,JDK1.4之前唯一选择, 只有这一种IO模型 ,在java.io包中。

    同步阻塞:阻塞线程直到获取到结果。

    问题:

    有多个客户端进行连接时, 如: client1, client2 进行连接, 由于client1先与服务端进行了连接, 此时服务端只能处理client1的请求, 什么时候client1处理完, 什么时候才能处理其他的请求, 服务端的线程一直处理client1的请求, 所以再有其他的请求, 无办法处理。

    Socket底层使用的是BIO, 同步阻塞的IO, 必须接收消息之后, 代码才能继续向下执行。

    服务端一个线程同时只能处理一个客户端的一个请求。

    解决办法:

    服务端收到一个客户端,就创建一个线程处理客户端的消息。

    缺点:

    每个客户端需要对应服务端的一个子线程。 所以有多少个客户端, 服务端就需要创建多少个子线程。如果客户端特别多,几万甚至几百万,服务器端就需要有几万甚至几百万的子线程。 由于每个子线程都有自己的独立线程区,这么多子线程可能就产生内存不足等问题。而且这么多的子线程也需要进行调度, 切换, 销毁这也是非常消耗性能的。

    虽然可以解决一个服务端处理多个客户端,但是因为过多的子线程导致系统资源消耗过多,线程切换导致性能下降也是我们不得不需要考虑的问题。可以使用线程池,在一定程度上提升程序性能,但是并没有解决根本问题。

    三、NIO

    NIO称为New IO(新的IO)又称Non-Blocking IO(非阻塞IO), 同步非阻塞IO模型,Java 从JDK1.4版本开始推出了NIO模型, 在java.nio包中。

    同步非阻塞:必须接收到消息程序才能结束,但不阻塞线程。

    优点:服务端的一个线程可以处理多个客户端请求, 决了使用BIO服务端创建过多子线程产生内存不足, 这么多的子线程也需要进行调度, 切换, 销毁也是非常消耗性能的问题。

    缺点:使用起来比较复杂。

    原理:

     每有一个客户端就会注册到选择器中,选择器轮询的监测每个客户端,当选择器检测到该客户端传输了数据,就会把该客户端连接到服务端。

    (1)Buffer(缓冲区)

    重要参数:

    1. position:当前的位置,表示进行下一个读写操作时的起始位置;

    2. limit:结束标记位置,表示进行读写操作时的(最大)结束位置;

    3. capacity:该ByteBuffer容量;

    4. mark: 自定义的标记位置, -1不自定义定标记位置;

    创建方式:

    1. package com.java.test;
    2. import java.nio.ByteBuffer;
    3. public class Test01 {
    4. public static void main(String[] args) {
    5. //(1)创建缓冲区,并设置缓冲区的容量,适用于输入
    6. ByteBuffer allocate = ByteBuffer.allocate(1024);
    7. //(2)创建缓冲区,适用于输出
    8. ByteBuffer wrap = ByteBuffer.wrap("abc".getBytes());
    9. //向缓冲区中添加数据
    10. allocate.put("aa".getBytes());
    11. //获取缓冲区的容量
    12. int capacity = allocate.capacity();
    13. //获取缓冲区的限制
    14. int limit = allocate.limit();
    15. //获取缓冲区添加下一个元素的脚标
    16. int position = allocate.position();
    17. //获取缓冲区存储数据的数组
    18. byte[] array = allocate.array();
    19. String s = new String(array, 0, position);
    20. System.out.println(s);
    21. }
    22. }

    重置ByteBuffer

    clear()

    内容不变, postion标志位置变为0, limit结束位置等于capacity容量大小 ​适用于写之前的操作

    1. public Buffer clear() {
    2. position = 0;
    3. limit = capacity;
    4. mark = -1;
    5. return this;
    6. }

     flip()

    内容不变, limit结束位置等于当前position标志位置, postion标志位置变为0 ​适用于读之前的操作

    1. public Buffer flip() {
    2. limit = position;
    3. position = 0;
    4. mark = -1;
    5. return this;
    6. }

    (2)Channel(管道)

    通道(Channel),它就像自来水管道一样,网络数据通过channel读取和写入,通道与流的不同之处在于通道是双向的,而流只在一个方向上移动(一个流必须是inputStream或者OutputStream的子类),而通道可以用于读写或者两者同时进行,最关键的是可以与多路复用器结合起来,有多种的状态位,方便多路复用器去识别。事实上通道分为两大类,一类是网络读写的(SelectableChannel),一类是用于文件操作的(FileChannel),我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。

    (3)Selector(多路复用器

    简单的说,就是Selector会不断的轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel,从而进行后续的IO操作。

    每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。

    SelectionKey.OP_ACCEPT: 服务端接收到客户端(服务端接收到了客户端连接), 可以使用isAcceptable()判断(服务端使用该状态)

    SelectionKey.OP_CONNECT: 客户端连接服务端(客户端和服务端连接成功), 可以使用isConnectable()判断(客户端使用该状态)

    SelectionKey.OP_READ: 通道中有内容的这状态(服务端与客户端都可以使用), 可以使用isReadable()判断(服务端, 客户端使用该状态)

    SelectionKey.OP_WRITE: 向通道中写入内容, 可以isWritable()判断(服务端, 客户端使用该状态)

    (4)NIO的使用

    连接

    1. package com.java.tcp;
    2. /*
    3. * 客户端
    4. * */
    5. import java.io.IOException;
    6. import java.net.InetSocketAddress;
    7. import java.nio.channels.SelectionKey;
    8. import java.nio.channels.Selector;
    9. import java.nio.channels.SocketChannel;
    10. public class Client {
    11. public static void main(String[] args) throws IOException {
    12. //获取客户端的管道
    13. SocketChannel sc = SocketChannel.open();
    14. //设置为非阻塞
    15. sc.configureBlocking(false);
    16. //连接服务端
    17. sc.connect(new InetSocketAddress("192.168.141.30",9090));
    18. //获取多路复用器
    19. Selector selector = Selector.open();
    20. //将客户端管道注册到多路复用器中,并设置监听状态
    21. sc.register(selector, SelectionKey.OP_CONNECT);
    22. //获取监听到的管道状态
    23. selector.select();
    24. }
    25. }
    1. package com.java.tcp;
    2. /*
    3. * 服务端
    4. * */
    5. import java.io.IOException;
    6. import java.net.InetSocketAddress;
    7. import java.nio.channels.SelectionKey;
    8. import java.nio.channels.Selector;
    9. import java.nio.channels.ServerSocketChannel;
    10. public class Server {
    11. public static void main(String[] args) throws IOException {
    12. //获取服务端的管道
    13. ServerSocketChannel scc = ServerSocketChannel.open();
    14. //设置为非阻塞
    15. scc.configureBlocking(false);
    16. //绑定与监听本机IP及指定端口号
    17. scc.bind(new InetSocketAddress("192.168.141.30", 9090));
    18. //获取多路复用器
    19. Selector selector = Selector.open();
    20. //将服务端管道注册到多路复用器中,并设置监听状态
    21. scc.register(selector, SelectionKey.OP_ACCEPT);
    22. //获取管道的状态,管道没有状态,阻塞
    23. selector.select();
    24. System.out.println("客户端已连接");
    25. }
    26. }
    1. package com.java.tcp;
    2. /*
    3. * 服务端
    4. * */
    5. import java.io.IOException;
    6. import java.net.InetSocketAddress;
    7. import java.nio.ByteBuffer;
    8. import java.nio.channels.*;
    9. import java.util.Iterator;
    10. import java.util.Scanner;
    11. import java.util.Set;
    12. public class Server {
    13. public static void main(String[] args) throws IOException {
    14. Scanner scanner = new Scanner(System.in);
    15. //获取服务端的管道
    16. ServerSocketChannel scc = ServerSocketChannel.open();
    17. //设置为非阻塞
    18. scc.configureBlocking(false);
    19. //绑定与监听本机IP及指定端口号
    20. scc.bind(new InetSocketAddress("192.168.141.30", 9090));
    21. //获取多路复用器
    22. Selector selector = Selector.open();
    23. //将服务端管道注册到多路复用器中,并设置监听状态
    24. scc.register(selector, SelectionKey.OP_ACCEPT);
    25. while (true) {
    26. //获取管道的状态,管道没有状态,阻塞
    27. selector.select();
    28. //获取监听到的服务端的所有转态
    29. Set<SelectionKey> selectionKeys = selector.selectedKeys();
    30. //获取Set集合的迭代器
    31. Iterator<SelectionKey> iterator = selectionKeys.iterator();
    32. //使用迭代器遍历该集合
    33. while (iterator.hasNext()){
    34. //获取集合中的每一项
    35. SelectionKey next = iterator.next();
    36. //判断状态为接收到了客户端
    37. if (next.isAcceptable()){
    38. //获取客户端管道
    39. SocketChannel sc = scc.accept();
    40. //设置为非阻塞
    41. sc.configureBlocking(false);
    42. //将客户端注册到多路复用器中,并将状态设置为读
    43. sc.register(selector,SelectionKey.OP_READ);
    44. }else if(next.isReadable()){
    45. //通过状态获取管道
    46. SocketChannel sc = (SocketChannel)next.channel();
    47. //接收客户端的消息
    48. ByteBuffer allocate = ByteBuffer.allocate(1024);
    49. sc.read(allocate);
    50. //获取缓冲区中的数据
    51. byte[] array = allocate.array();
    52. String s = new String(array, 0, allocate.position());
    53. System.out.println(s);
    54. //将客户端注册到多路复用器中,并将状态设置为写
    55. sc.register(selector,SelectionKey.OP_WRITE);
    56. }else if(next.isWritable()){
    57. //通过状态获取客户端管道
    58. SocketChannel sc = (SocketChannel) next.channel();
    59. System.out.println(sc);
    60. //创建传出数据的缓冲区
    61. String s = scanner.next();
    62. s = "服务端: "+s;
    63. ByteBuffer wrap = ByteBuffer.wrap(s.getBytes());
    64. //将缓冲区写入客户端管道
    65. sc.write(wrap);
    66. //将客户端注册到多路复用器中,并将状态设置为读
    67. sc.register(selector,SelectionKey.OP_READ);
    68. }
    69. //将处理过的管道状态删除
    70. iterator.remove();
    71. }
    72. }
    73. }
    74. }

    1. package com.java.tcp;
    2. /*
    3. * 客户端
    4. * */
    5. import java.io.IOException;
    6. import java.net.InetSocketAddress;
    7. import java.nio.ByteBuffer;
    8. import java.nio.channels.SelectionKey;
    9. import java.nio.channels.Selector;
    10. import java.nio.channels.SocketChannel;
    11. import java.util.Iterator;
    12. import java.util.Scanner;
    13. import java.util.Set;
    14. public class Client {
    15. public static void main(String[] args) throws IOException {
    16. Scanner scanner = new Scanner(System.in);
    17. //获取客户端的管道
    18. SocketChannel sc = SocketChannel.open();
    19. //设置为非阻塞
    20. sc.configureBlocking(false);
    21. //连接服务端
    22. sc.connect(new InetSocketAddress("192.168.141.30",9090));
    23. //获取多路复用器
    24. Selector selector = Selector.open();
    25. //将客户端管道注册到多路复用器中,并设置监听状态
    26. sc.register(selector, SelectionKey.OP_CONNECT);
    27. while (true) {
    28. //获取监听到的管道状态
    29. selector.select();
    30. //获取监听到的客户端管道的所有状态
    31. Set<SelectionKey> selectionKeys = selector.selectedKeys();
    32. Iterator<SelectionKey> iterator = selectionKeys.iterator();
    33. while (iterator.hasNext()){
    34. //获取每个状态
    35. SelectionKey next = iterator.next();
    36. //判断管道状态为连接到了服务器
    37. if(next.isConnectable()){
    38. //判断是否完全连接
    39. if (sc.isConnectionPending()) {
    40. //完成客户端和服务端管道的连接
    41. sc.finishConnect();
    42. }
    43. //向服务端发送消息
    44. ByteBuffer wrap = ByteBuffer.wrap("客户端1连接成功".getBytes());
    45. sc.write(wrap);
    46. //将客户端管道注册到多路复用器中,并将状态设置为读
    47. sc.register(selector,SelectionKey.OP_READ);
    48. } else if (next.isReadable()) {
    49. //接收服务端的消息
    50. ByteBuffer allocate = ByteBuffer.allocate(1024);
    51. sc.read(allocate);
    52. //获取缓冲区中的数据
    53. byte[] array = allocate.array();
    54. String s = new String(array, 0, allocate.position());
    55. System.out.println(s);
    56. //将客户端管道注册到多路复用器中,并将状态设置为写
    57. sc.register(selector, SelectionKey.OP_WRITE);
    58. } else if (next.isWritable()) {
    59. //创建缓冲区,将数据放到缓冲区中
    60. String s = scanner.next();
    61. s = "客户端1: " + s;
    62. ByteBuffer wrap = ByteBuffer.wrap(s.getBytes());
    63. //将缓冲区放到管道中
    64. sc.write(wrap);
    65. //将客户端管道注册到多路复用器中,并将状态设置为读
    66. sc.register(selector,SelectionKey.OP_READ);
    67. }
    68. //将处理过的状态删除
    69. iterator.remove();
    70. }
    71. }
    72. }
    73. }

    四、AIO

    AIO(asynchronous):异步非阻塞IO。

    在执行时,当前线程不会阻塞, 调用发起后, 不需要等待返回的结果(结果是以事件驱动的形式返回), 而且也不需要多线程就可以实现多客户端访问。

  • 相关阅读:
    【深入设计模式】迭代器模式模式—什么是迭代器模式?
    稀疏矩阵转十字链表
    R2 MSE Linear Regression
    甲骨文真的要开放Java EE?
    防御DDOS的方法是什么?
    05节-51单片机-模块化编程
    SpringBoot整合邮件发送
    MySQL第七讲·怎么利用聚合函数实现高效地分组统计?
    请陪伴Kimi和GPT成长
    C/C++笔试易错与高频题型&图解知识点(二)—— C++部分(持续更新中)
  • 原文地址:https://blog.csdn.net/weixin_53455615/article/details/126579687