Java 的 I/O 演进

I/O 模型:就是用什么样的通道或者说是通信模式和架构进行数据的传输和接收,很大程度上决定了程序通信的性能,Java 共支持 3 种网络编程的/IO 模型:BIO、NIO、AIO 实际通信需求下,要根据不同的业务场景和性能需求决定选择不同的I/O模型

Java BIO

同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器 端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销

Java NIO

Java NIO :同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注 册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理

Java AIO

Java AIO(NIO.2) :异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用

BIO、NIO、AIO 适用场景分析

1、BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK 1.4 以前的唯一选择,但程序简单易理解。
2、NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。 编程比较复杂,JDK 1.4 开始支持。
3、AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作, 编程比较复杂,JDK 7 开始支持。

JAVA BIO 深入剖析

Java BIO 基本介绍

Java BIO 就是传统的 java io 编程,其相关的类和接口在 java.io。
BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需 要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。

Java BIO 工作机制

  1. 服务器端启动一个 ServerSocket,注册端口, 调用 accpet 方法监听客户端的 Socket 连接。
  2. 客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户端建立一个线程与之通讯。

    传统的 BIO 实例

    网络编程的基本模型是 Client/Server 模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定 IP 地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于 TCP 协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
    传统的同步阻塞模型开发中,服务端 ServerSocket 负责绑定 IP 地址,启动监听端口;客户端 Socket 负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。 基于 BIO 模式下的通信,客户端 - 服务端是完全同步,完全耦合的。

    客户端 demo

    1. public class Client {
    2. public static void main(String[] args) {
    3. try {
    4. //(1)创建一个 Socket 的通信管道,请求与服务端的端口连接。
    5. Socket socket = new Socket("localhost", 8888);
    6. //(2)从 Socket 通信管道中得到一个字节输出流。
    7. OutputStream outputStream = socket.getOutputStream();
    8. //(3)把字节流改装成自己需要的流进行数据的发送
    9. PrintStream printStream = new PrintStream(outputStream)
    10. //(4)开始发送消息
    11. printStream.println("hello server");
    12. printStream.flush();
    13. } catch (IOException e) {
    14. e.printStackTrace();
    15. }
    16. }
    17. }

    服务端 demo

    1. public class Server {
    2. public static void main(String[] args) {
    3. System.out.println("服务端启动");
    4. try {
    5. //(1)注册端口
    6. ServerSocket serverSocket = new ServerSocket(8888);
    7. //(2)开始在这里暂停等待接收客户端的连接,得到一个端到端的 Socket 管道
    8. Socket socket = serverSocket.accept();
    9. //(3)从 Socket 管道中得到一个字节输入流。
    10. InputStream inputStream = socket.getInputStream();
    11. //(4)把字节输入流包装成自己需要的流进行数据的读取。
    12. InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
    13. BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
    14. //(5)读取数据
    15. String res;
    16. while ((res = bufferedReader.readLine()) != null) {
    17. System.out.println(res);
    18. }
    19. } catch (IOException e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. }

    小结

  • 在以上通信中,服务端会一致等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态。
  • 同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!

    BIO 模式下多发和多收消息

    只需要在客户端案例中,加上反复按照行发送消息的逻辑即可

    客户端 demo

    1. public class Client {
    2. public static void main(String[] args) {
    3. try {
    4. //(1)创建一个 Socket 的通信管道,请求与服务端的端口连接。
    5. Socket socket = new Socket("localhost", 8888);
    6. //(2)从 Socket 通信管道中得到一个字节输出流。
    7. OutputStream outputStream = socket.getOutputStream();
    8. //(3)把字节流改装成自己需要的流进行数据的发送
    9. PrintStream printStream = new PrintStream(outputStream);
    10. //(4)开始发送消息,使用 Scanner
    11. Scanner scanner = new Scanner(System.in);
    12. String input = null;
    13. while ((input = scanner.nextLine()) != null) {
    14. printStream.println(input);
    15. printStream.flush();
    16. }
    17. } catch (IOException e) {
    18. e.printStackTrace();
    19. }
    20. }
    21. }

    服务端 demo

    1. public class Server {
    2. public static void main(String[] args) {
    3. System.out.println("服务端启动");
    4. try {
    5. //(1)注册端口
    6. ServerSocket serverSocket = new ServerSocket(8888);
    7. //(2)开始在这里暂停等待接收客户端的连接,得到一个端到端的 Socket 管道
    8. Socket socket = serverSocket.accept();
    9. //(3)从 Socket 管道中得到一个字节输入流。
    10. InputStream inputStream = socket.getInputStream();
    11. //(4)把字节输入流包装成自己需要的流进行数据的读取。
    12. InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
    13. BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
    14. //(5)读取数据
    15. String res;
    16. while ((res = bufferedReader.readLine()) != null) {
    17. System.out.println(res);
    18. }
    19. } catch (IOException e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. }

    小结

  • 本案例中确实可以实现客户端多发多收。

  • 但是服务端只能处理一个客户端的请求,因为服务端是单线程的。一次只能与一个客户端进行消息通信。

    BIO 模式下接收多个客户端

    在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型

    客户端 demo

    1. public class Client {
    2. public static void main(String[] args) {
    3. try {
    4. //(1)创建一个 Socket 的通信管道,请求与服务端的端口连接。
    5. Socket socket = new Socket("localhost", 8888);
    6. //(2)从 Socket 通信管道中得到一个字节输出流。
    7. OutputStream outputStream = socket.getOutputStream();
    8. //(3)把字节流改装成自己需要的流进行数据的发送
    9. PrintStream printStream = new PrintStream(outputStream);
    10. //(4)开始发送消息,使用 Scanner
    11. Scanner scanner = new Scanner(System.in);
    12. while (true) {
    13. printStream.println(scanner.nextLine());
    14. printStream.flush();
    15. }
    16. } catch (IOException e) {
    17. e.printStackTrace();
    18. }
    19. }
    20. }

    服务端 demo

    ```java public class Server { public static void main(String[] args) {
    1. System.out.println("服务端启动");
    2. try {
    3. // 注册端口
    4. ServerSocket serverSocket = new ServerSocket(8888);
    5. while (true) {
    6. // 开始在这里暂停等待接收客户端的连接,得到一个端到端的 Socket 管道
    7. Socket socket = serverSocket.accept();
    8. new ServerReadThread(socket).start();
    9. }
    10. } catch (IOException e) {
    11. e.printStackTrace();
    12. }
    } }

public class ServerReadThread extends Thread {

  1. private Socket socket;
  2. public ServerReadThread(Socket socket) {
  3. this.socket = socket;
  4. }
  5. @Override
  6. public void run() {
  7. try {
  8. // 从Socket管道中得到一个字节输入流。
  9. InputStream inputStream = socket.getInputStream();
  10. // 把字节输入流包装成自己需要的流进行数据的读取。
  11. InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
  12. BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
  13. // 读取数据
  14. String res;
  15. while ((res = bufferedReader.readLine()) != null) {
  16. System.out.println(res);
  17. }
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. }
  21. }

}

  1. <a name="5db9fd7c-2"></a>
  2. #### 小结
  3. - 每个 Socket 接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
  4. - 每个线程都会占用栈空间和 CPU 资源;
  5. - 并不是每个 Socket 都进行 IO 操作,无意义的线程处理;
  6. - 客户端的并发访问增加时。服务端将呈现 1:1 的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
  7. <a name="aeccd178"></a>
  8. ### 伪异步 I/O 编程
  9. 在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。<br />接下来我们采用一个伪异步 I/O 的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的 Socket 封装成一个 Task(该任务实现 java.lang.Runnable 线程任务接口)交给后端的线程池中进行处理。JDK 的线程池维护一个消息队列和 N 个活跃的线程,对消息队列中 Socket 任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
  10. <a name="8466d8d6-3"></a>
  11. #### 客户端 demo
  12. ```java
  13. public class Client {
  14. public static void main(String[] args) {
  15. try {
  16. //(1)创建一个 Socket 的通信管道,请求与服务端的端口连接。
  17. Socket socket = new Socket("localhost", 8888);
  18. //(2)从 Socket 通信管道中得到一个字节输出流。
  19. OutputStream outputStream = socket.getOutputStream();
  20. //(3)把字节流改装成自己需要的流进行数据的发送
  21. PrintStream printStream = new PrintStream(outputStream);
  22. //(4)开始发送消息,使用 Scanner
  23. Scanner scanner = new Scanner(System.in);
  24. while (true) {
  25. printStream.println(scanner.nextLine());
  26. printStream.flush();
  27. }
  28. } catch (IOException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }

服务端 demo

  1. // 线程池类
  2. public class HandlerSocketThreadPool {
  3. ExecutorService executor;
  4. public HandlerSocketThreadPool(int maxPoolSize, int queueSize){
  5. this.executor = new ThreadPoolExecutor(
  6. 3,
  7. maxPoolSize,
  8. 120L,
  9. TimeUnit.SECONDS,
  10. new ArrayBlockingQueue<Runnable>(queueSize) );
  11. }
  12. public void execute(Runnable task){
  13. this.executor.execute(task);
  14. }
  15. }
  16. // 启动 server socket 服务类
  17. public class Server {
  18. public static void main(String[] args) {
  19. System.out.println("服务端启动");
  20. try {
  21. // 注册端口
  22. ServerSocket serverSocket = new ServerSocket(8888);
  23. // 创建线程池
  24. HandlerSocketThreadPool handlerSocketThreadPool = new HandlerSocketThreadPool(10, 10);
  25. while (true) {
  26. // 开始在这里暂停等待接收客户端的连接,得到一个端到端的 Socket 管道
  27. Socket socket = serverSocket.accept();
  28. handlerSocketThreadPool.execute(new ReaderClientRunnable(socket));
  29. }
  30. } catch (IOException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. }
  35. // 处理 socket 类
  36. public class ReaderClientRunnable implements Runnable {
  37. private Socket socket;
  38. public ReaderClientRunnable(Socket socket) {
  39. this.socket = socket;
  40. }
  41. @Override
  42. public void run() {
  43. try {
  44. // 读取一行数据
  45. InputStream is = socket.getInputStream();
  46. // 转成一个缓冲字符流
  47. Reader fr = new InputStreamReader(is);
  48. BufferedReader br = new BufferedReader(fr);
  49. // 一行一行的读取数据
  50. String line = null;
  51. // 阻塞式的读取
  52. while ((line = br.readLine()) != null) {
  53. System.out.println("服务端收到了数据:" + line);
  54. }
  55. System.out.println("客户端下线了");
  56. } catch (Exception e) {
  57. e.printStackTrace();
  58. }
  59. }
  60. }

小结

  • 伪异步 io 采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
  • 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续 socket 的 i/o 消息都将在队列中排队。新的 Socket 请求将被拒绝,客户端会发生大量连接超时。

    BIO 上传文件

    客户端 demo

    1. public class Client {
    2. public static void main(String[] args) {
    3. try {
    4. // 创建一个Socket的通信管道,请求与服务端的端口连接。
    5. Socket socket = new Socket("localhost", 8888);
    6. // 从Socket通信管道中得到一个字节输出流。
    7. OutputStream outputStream = socket.getOutputStream();
    8. // 把字节流改装成自己需要的流进行数据的发送
    9. DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
    10. // 读取文件
    11. FileInputStream fileInputStream = new FileInputStream("test.txt");
    12. dataOutputStream.writeUTF(".txt");
    13. byte[] buff = new byte[1024];
    14. int len = 0;
    15. while ((len = fileInputStream.read(buff)) > 0) {
    16. dataOutputStream.write(buff, 0, len);
    17. }
    18. dataOutputStream.flush();
    19. } catch (IOException e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. }

    服务端 demo

    ```java public class Server { public static void main(String[] args) {

    1. System.out.println("服务端启动");
    2. try {
    3. // 注册端口
    4. ServerSocket serverSocket = new ServerSocket(8888);
    5. while (true) {
    6. // 开始在这里暂停等待接收客户端的连接,得到一个端到端的Socket管道
    7. Socket socket = serverSocket.accept();
    8. new ServerReadThread(socket).start();
    9. }
    10. } catch (IOException e) {
    11. e.printStackTrace();
    12. }

    } }

public class ServerReadThread extends Thread{

  1. private Socket socket;
  2. public ServerReadThread(Socket socket) {
  3. this.socket = socket;
  4. }
  5. @Override
  6. public void run() {
  7. try {
  8. // 从Socket管道中得到一个字节输入流。
  9. InputStream inputStream = socket.getInputStream();
  10. // 把字节输入流包装成自己需要的流进行数据的读取。
  11. DataInputStream dataInputStream = new DataInputStream(inputStream);
  12. String suffix = dataInputStream.readUTF();
  13. FileOutputStream fileOutputStream = new FileOutputStream(UUID.randomUUID() + suffix);
  14. byte[] buff = new byte[1024];
  15. int len = 0;
  16. while ((len = dataInputStream.read(buff)) > 0) {
  17. fileOutputStream.write(buff, 0, len);
  18. }
  19. fileOutputStream.flush();
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. }

}

  1. <a name="f621a1db"></a>
  2. ## JAVA NIO 深入剖析
  3. <a name="f608593d"></a>
  4. ### Java NIO 基本介绍
  5. - Java NIO(New IO)也有人称之为 java non-blocking IO 是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面**向缓冲区**的、基于**通道**的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。NIO 可以理解为非阻塞 IO,传统的 IO 的 read 和 write 只能阻塞执行,线程在读写 IO 期间不能干其他事情,比如调用 socket.read() 时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO 中可以配置 socket 为非阻塞模式。
  6. - NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
  7. - NIO 有三大核心部分:**Channel ( 通道) ,Buffer ( 缓冲区), Selector ( 选择器)**
  8. - Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
  9. - 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况,可以分配 20 或者 80 个线程来处理。不像之前的阻塞 IO 那样,非得分配 1000 个。
  10. <a name="ff8546ff"></a>
  11. ### NIO 和 BIO 的比较
  12. - BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
  13. - BIO 是阻塞的,NIO 则是非阻塞的
  14. - BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
  15. | NIO | BIO |
  16. | --- | --- |
  17. | 面向缓冲区(Buffer) | 面向流(Stream) |
  18. | 非阻塞(Non Blocking IO) | 阻塞IO(Blocking IO) |
  19. | 选择器(Selectors) | |
  20. <a name="9b889dcc"></a>
  21. ### NIO 三大核心原理
  22. NIO 有三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)**
  23. <a name="b6032316"></a>
  24. #### Buffer缓冲区
  25. 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API 更加容易操作和管理。
  26. <a name="56be989f"></a>
  27. #### **Channel(通道)**
  28. Java NIO 的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input 或 output)读写通常是单向的。 通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。
  29. <a name="6de2885f"></a>
  30. #### Selector选择器
  31. Selector 是 一个 Java NIO 组件,可以能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个 Channel,从而管理多个网络连接,提高效率。
  32. - 每个 channel 都会对应一个 Buffer
  33. - 一个线程对应 Selector , 一个 Selector 对应多个 channel(连接)
  34. - 程序切换到哪个 channel 是由事件决定的
  35. - Selector 会根据不同的事件,在各个通道上切换
  36. - Buffer 就是一个内存块 , 底层是一个数组
  37. - 数据的读取写入是通过 Buffer 完成的 , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写。
  38. - Java NIO 系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,需要获取 用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据
  39. <a name="ca53ff62"></a>
  40. ### NIO核心一:缓冲区(Buffer)
  41. <a name="920903aa"></a>
  42. #### 缓冲区(Buffer)
  43. 一个用于特定基本数据类 型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类.。Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
  44. <a name="6f459fe0"></a>
  45. #### Buffer 类及其子类
  46. **Buffer** 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同 ,有以下 Buffer 常用子类:
  47. - ByteBuffer
  48. - CharBuffer
  49. - ShortBuffer
  50. - IntBuffer
  51. - LongBuffer
  52. - FloatBuffer
  53. - DoubleBuffer
  54. 上述 Buffer 类他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个 Buffer 对象:
  55. ```java
  56. static XxxBuffer allocate(int capacity) // 创建一个容量为capacity 的 XxxBuffer 对象

缓冲区的基本属性

  • 容量 (capacity) :作为一个内存块,Buffer 具有一定的固定大小,也称为”容量”,缓冲区容量不能为负,并且创建后不能更改。
  • 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。 写入模式,限制等于 buffer 的容量。读取模式下,limit 等于写入的数据量
  • 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为 负,并且不能大于其限制。
  • 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这 个 position. 标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity

    Buffer常见方法

    1. Buffer clear() 清空缓冲区并返回对缓冲区的引用
    2. Buffer flip() 为将缓冲区的界限设置为当前位置,并将当前位置充值为 0
    3. int capacity() 返回 Buffer capacity 大小
    4. boolean hasRemaining() 判断缓冲区中是否还有元素
    5. int limit() 返回 Buffer 的界限(limit) 的位置
    6. Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
    7. Buffer mark() 对缓冲区设置标记
    8. int position() 返回缓冲区的当前位置 position
    9. Buffer position(int n) 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
    10. int remaining() 返回 position limit 之间的元素个数
    11. Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
    12. Buffer rewind() 将位置设为为 0 取消设置的 mark

    缓冲区的数据操作

    ```java Buffer 所有子类提供了两个用于数据操作的方法: get()、put()

取获取 Buffer 中的数据方法: get() :读取单个字节 get(byte[] dst):批量读取多个字节到 dst 中 get(int index):读取指定索引位置的字节(不会移动 position)

放到入数据到 Buffer 中: put(byte b):将给定单个字节写入缓冲区的当前位置 put(byte[] src):将 src 中的字节写入缓冲区的当前位置 put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

  1. <a name="a47f1e14"></a>
  2. #### 使用Buffer读写数据一般遵循以下四个步骤
  3. - 1.写入数据到 `Buffer`
  4. - 2.调用 `flip` 方法,转换为读取模式
  5. - 3.从 `Buffer` 中读取数据
  6. - 4.调用 `buffer#clear` 方法或者 `buffer.compact` 方法清除缓冲区
  7. <a name="24f60c15"></a>
  8. #### 案例演示
  9. ```java
  10. public class TestBuffer {
  11. @Test
  12. public void test3(){
  13. //分配直接缓冲区
  14. ByteBuffer buf = ByteBuffer.allocateDirect(1024);
  15. System.out.println(buf.isDirect());
  16. }
  17. @Test
  18. public void test2(){
  19. String str = "itheima";
  20. ByteBuffer buf = ByteBuffer.allocate(1024);
  21. buf.put(str.getBytes());
  22. buf.flip();
  23. byte[] dst = new byte[buf.limit()];
  24. buf.get(dst, 0, 2);
  25. System.out.println(new String(dst, 0, 2));
  26. System.out.println(buf.position());
  27. //mark() : 标记
  28. buf.mark();
  29. buf.get(dst, 2, 2);
  30. System.out.println(new String(dst, 2, 2));
  31. System.out.println(buf.position());
  32. //reset() : 恢复到 mark 的位置
  33. buf.reset();
  34. System.out.println(buf.position());
  35. //判断缓冲区中是否还有剩余数据
  36. if(buf.hasRemaining()){
  37. //获取缓冲区中可以操作的数量
  38. System.out.println(buf.remaining());
  39. }
  40. }
  41. @Test
  42. public void test1(){
  43. String str = "itheima";
  44. //1. 分配一个指定大小的缓冲区
  45. ByteBuffer buf = ByteBuffer.allocate(1024);
  46. System.out.println("-----------------allocate()----------------");
  47. System.out.println(buf.position());
  48. System.out.println(buf.limit());
  49. System.out.println(buf.capacity());
  50. //2. 利用 put() 存入数据到缓冲区中
  51. buf.put(str.getBytes());
  52. System.out.println("-----------------put()----------------");
  53. System.out.println(buf.position());
  54. System.out.println(buf.limit());
  55. System.out.println(buf.capacity());
  56. //3. 切换读取数据模式
  57. buf.flip();
  58. System.out.println("-----------------flip()----------------");
  59. System.out.println(buf.position());
  60. System.out.println(buf.limit());
  61. System.out.println(buf.capacity());
  62. //4. 利用 get() 读取缓冲区中的数据
  63. byte[] dst = new byte[buf.limit()];
  64. buf.get(dst);
  65. System.out.println(new String(dst, 0, dst.length));
  66. System.out.println("-----------------get()----------------");
  67. System.out.println(buf.position());
  68. System.out.println(buf.limit());
  69. System.out.println(buf.capacity());
  70. //5. rewind() : 可重复读
  71. buf.rewind();
  72. System.out.println("-----------------rewind()----------------");
  73. System.out.println(buf.position());
  74. System.out.println(buf.limit());
  75. System.out.println(buf.capacity());
  76. //6. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态
  77. buf.clear();
  78. System.out.println("-----------------clear()----------------");
  79. System.out.println(buf.position());
  80. System.out.println(buf.limit());
  81. System.out.println(buf.capacity());
  82. System.out.println((char)buf.get());
  83. }
  84. }

直接与非直接缓冲区

什么是直接内存与非直接内存
根据官方文档的描述:
byte byffer 可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM 将会在 IO 操作上具有更高的性能,因为它直接作用于本地系统的 IO 操作。而非直接内存,也就是堆内存中的数据,如果要作 IO 操作,会先从本进程内存复制到直接内存,再利用本地 IO 处理。
从数据流的角度,非直接内存是下面这样的作用链:

  1. 本地IO-->直接内存-->非直接内存-->直接内存-->本地IO

而直接内存是:

  1. 本地IO-->直接内存-->本地IO

很明显,在做 IO 处理时,比如网络发送大量数据时,直接内存会具有更高的效率。直接内存使用allocateDirect 创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在 JVM 之外的,因此它不会占用应用的内存。所以呢,当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。
使用场景

  • 有很大的数据需要存储,它的生命周期又很长
  • 适合频繁的 IO 操作,比如网络并发场景

    NIO核心二:通道(Channel)

    通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
    1、 NIO 的通道类似于流,但有些区别如下:

  • 通道可以同时进行读写,而流只能读或者只能写

  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲:

2、BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel) 是双向的,可以读操作,也可以写操作。
3、Channel 在 NIO 中是一个接口

  1. public interface Channel extends Closeable{}

常用的Channel实现类

  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • DatagramChannel:通过 UDP 读写网络中的数据通道。
  • SocketChannel:通过 TCP 读写网络中的数据。
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】

    FileChannel 类

    获取通道的一种方式是对支持通道的对象调用 getChannel() 方法。支持通道的类如下:

  • FileInputStream

  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket 获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道

    FileChannel的常用方法

    1. int read(ByteBuffer dst) Channel 中读取数据到 ByteBuffer
    2. long read(ByteBuffer[] dsts) Channel 中的数据“分散”到 ByteBuffer[]
    3. int write(ByteBuffer src) ByteBuffer 中的数据写入到 Channel
    4. long write(ByteBuffer[] srcs) ByteBuffer[] 中的数据“聚集”到 Channel
    5. long position() 返回此通道的文件位置
    6. FileChannel position(long p) 设置此通道的文件位置
    7. long size() 返回此通道的文件的当前大小
    8. FileChannel truncate(long s) 将此通道的文件截取为给定大小
    9. void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中

    案例1-本地文件写数据

    需求:使用前面学习后的 ByteBuffer (缓冲) 和 FileChannel (通道), 将 “hello,黑马Java程序员!” 写入到 data.txt 中.

    1. @Test
    2. public void write() throws IOException {
    3. // 1、字节输出流通向目标文件
    4. FileOutputStream fos = new FileOutputStream("data01.txt");
    5. // 2、得到字节输出流对应的通道Channel
    6. FileChannel channel = fos.getChannel();
    7. // 3、分配缓冲区
    8. ByteBuffer buffer = ByteBuffer.allocate(1024);
    9. buffer.put("hello,黑马Java程序员!".getBytes());
    10. // 4、把缓冲区切换成写出模式
    11. buffer.flip();
    12. channel.write(buffer);
    13. channel.close();
    14. System.out.println("写数据到文件中!");
    15. }

    案例2-本地文件读数据

    需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 data01.txt 中的数据读入到程序,并显示在控制台屏幕

    1. @Test
    2. public void read() throws Exception {
    3. // 1、定义一个文件字节输入流与源文件接通
    4. FileInputStream is = new FileInputStream("data01.txt");
    5. // 2、需要得到文件字节输入流的文件通道
    6. FileChannel channel = is.getChannel();
    7. // 3、定义一个缓冲区
    8. ByteBuffer buffer = ByteBuffer.allocate(1024);
    9. // 4、读取数据到缓冲区
    10. channel.read(buffer);
    11. buffer.flip();
    12. // 5、读取出缓冲区中的数据并输出即可
    13. String rs = new String(buffer.array(), 0, buffer.remaining());
    14. System.out.println(rs);
    15. }

    案例3-使用Buffer完成文件复制

    使用 FileChannel(通道) ,完成文件的拷贝。

    1. @Test
    2. public void copy() throws IOException {
    3. // 源文件
    4. File baseFile = new File("test.txt");
    5. // 目标文件
    6. File copyFile = new File("test-copy.txt");
    7. // 获取字节输入流与通道
    8. FileInputStream fileInputStream = new FileInputStream(baseFile);
    9. FileChannel inputStreamChannel = fileInputStream.getChannel();
    10. // 获取字节输出流与通道
    11. FileOutputStream fileOutputStream = new FileOutputStream(copyFile);
    12. FileChannel outputStreamChannel = fileOutputStream.getChannel();
    13. // 分配缓冲区
    14. ByteBuffer buffer = ByteBuffer.allocate(1024);
    15. while (true) {
    16. // 必须先清空缓冲然后再写入数据到缓冲区
    17. buffer.clear();
    18. // 开始读取一次数据
    19. int size = inputStreamChannel.read(buffer);
    20. if (size == -1) break;
    21. // 已经读取了数据 ,把缓冲区的模式切换成可读模式
    22. buffer.flip();
    23. // 把数据写出
    24. outputStreamChannel.write(buffer, buffer.remaining());
    25. }
    26. }

    案例4-分散 (Scatter) 和聚集 (Gather)

    分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去
    聚集写入(Gathering ):是指将多个 Buffer 中的数据“聚集”到 Channel

    1. //分散和聚集
    2. @Test
    3. public void scatterAndGather() throws IOException{
    4. RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
    5. //1. 获取通道
    6. FileChannel channel1 = raf1.getChannel();
    7. //2. 分配指定大小的缓冲区
    8. ByteBuffer buf1 = ByteBuffer.allocate(100);
    9. ByteBuffer buf2 = ByteBuffer.allocate(1024);
    10. //3. 分散读取
    11. ByteBuffer[] bufs = {buf1, buf2};
    12. channel1.read(bufs);
    13. for (ByteBuffer byteBuffer : bufs) {
    14. byteBuffer.flip();
    15. }
    16. System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
    17. System.out.println("-----------------");
    18. System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
    19. //4. 聚集写入
    20. RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
    21. FileChannel channel2 = raf2.getChannel();
    22. channel2.write(bufs);
    23. }

    案例5-transferFrom()

    从目标通道中去复制原通道数据

    1. @Test
    2. public void transferFrom() throws Exception {
    3. // 1、字节输入管道
    4. FileInputStream is = new FileInputStream("data01.txt");
    5. FileChannel isChannel = is.getChannel();
    6. // 2、字节输出流管道
    7. FileOutputStream fos = new FileOutputStream("data03.txt");
    8. FileChannel osChannel = fos.getChannel();
    9. // 3、复制
    10. osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size());
    11. isChannel.close();
    12. osChannel.close();
    13. }

    案例6-transferTo()

    把原通道数据复制到目标通道

    1. @Test
    2. public void transferTo() throws Exception {
    3. // 1、字节输入管道
    4. FileInputStream is = new FileInputStream("data01.txt");
    5. FileChannel isChannel = is.getChannel();
    6. // 2、字节输出流管道
    7. FileOutputStream fos = new FileOutputStream("data04.txt");
    8. FileChannel osChannel = fos.getChannel();
    9. // 3、复制
    10. isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
    11. isChannel.close();
    12. osChannel.close();
    13. }

    NIO核心三:选择器(Selector)

    选择器(Selector)概述

    选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel,Selector 是非阻塞 IO 的核心。

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector (选择器)

  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管 理多个通道,也就是管理多个连接和请求。
  • 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都 创建一个线程,不用去维护多个线程
  • 避免了多线程之间的上下文切换导致的开销

    选择 器(Selector)的应用

    创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。

    1. Selector selector = Selector.open();

    向选择器注册通道:SelectableChannel.register(Selector sel, int ops)

    1. //1. 获取通道
    2. ServerSocketChannel ssChannel = ServerSocketChannel.open();
    3. //2. 切换非阻塞模式
    4. ssChannel.configureBlocking(false);
    5. //3. 绑定连接
    6. ssChannel.bind(new InetSocketAddress(9898));
    7. //4. 获取选择器
    8. Selector selector = Selector.open();
    9. //5. 将通道注册到选择器上, 并且指定“监听接收事件”
    10. ssChannel.register(selector, SelectionKey.OP_ACCEPT);

    当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):

  • 读 : SelectionKey.OP_READ (1)

  • 写 : SelectionKey.OP_WRITE (4)
  • 连接 : SelectionKey.OP_CONNECT (8)
  • 接收 : SelectionKey.OP_ACCEPT (16)
  • 若注册时不止监听一个事件,则可以使用“位或”操作符连接。

    1. int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE

    NIO非阻塞式网络通信案例

    需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。

    客户端 demo:

    1. public class Client {
    2. public static void main(String[] args) throws IOException {
    3. SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", Server.PORT));
    4. socketChannel.configureBlocking(false);
    5. ByteBuffer buffer = ByteBuffer.allocate(1024);
    6. //4. 发送数据给服务端
    7. Scanner scan = new Scanner(System.in);
    8. while(scan.hasNext()){
    9. String str = scan.nextLine();
    10. buffer.clear();
    11. buffer.put(str.getBytes());
    12. buffer.flip();
    13. socketChannel.write(buffer);
    14. }
    15. //5. 关闭通道
    16. socketChannel.close();
    17. }
    18. }

    服务端 demo:

    1. public class Server {
    2. public static final int PORT = 9999;
    3. public static void main(String[] args) throws IOException {
    4. // 获取通道
    5. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    6. // 切换非阻塞模式
    7. serverSocketChannel.configureBlocking(false);
    8. // 绑定端口
    9. serverSocketChannel.bind(new InetSocketAddress(PORT));
    10. // 获取选择器
    11. Selector selector = Selector.open();
    12. // 将通道注册到选择器上,并且指定监听接收事件
    13. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    14. // 轮询式的获取选择器上已经准备就绪的事件
    15. while (selector.select() > 0) {
    16. // 获取所有事件
    17. Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
    18. while (selectionKeyIterator.hasNext()) {
    19. // 获取事件类型
    20. SelectionKey selectionKey = selectionKeyIterator.next();
    21. selectionKeyIterator.remove();
    22. if (selectionKey.isAcceptable()) {
    23. // 如果是客户端连接事件,将通道注册到选择器上
    24. SocketChannel socketChannel = serverSocketChannel.accept();
    25. socketChannel.configureBlocking(false);
    26. socketChannel.register(selector, SelectionKey.OP_READ);
    27. } else if (selectionKey.isReadable()) {
    28. // 如果是读事件
    29. SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
    30. ByteBuffer buffer = ByteBuffer.allocate(1024);
    31. int len = 0;
    32. while ((len = socketChannel.read(buffer)) > 0) {
    33. buffer.flip();
    34. System.out.println(new String(buffer.array(), 0, len));
    35. buffer.clear();
    36. }
    37. } else {
    38. System.out.println("无法完成请求处理");
    39. }
    40. }
    41. }
    42. }
    43. }

    JAVA AIO深入剖析

    AIO编程

  • Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

    1. AIO
    2. 异步非阻塞,基于NIO的,可以称之为NIO2.0
    3. BIO NIO AIO
    4. Socket SocketChannel AsynchronousSocketChannel
    5. ServerSocket ServerSocketChannel AsynchronousServerSocketChannel

    与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可, 这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
    即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道:

    1. AsynchronousSocketChannel
    2. AsynchronousServerSocketChannel
    3. AsynchronousFileChannel
    4. AsynchronousDatagramChannel

    BIO、NIO、AIO总结

    BIO、NIO、AIO:

  • Java BIO :同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

  • Java NIO :同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。
  • Java AIO(NIO.2) :异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。

BIO、NIO、AIO适用场景分析:

  • BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK 1.4 以前的唯一选择,但程序直观简单易理解。
  • NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK 1.4 开始支持。
  • AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。