Java中的IO操作分为三种模式:同步阻塞式(BIO),同步非阻塞式(NIO),异步非阻塞式(AIO),下面主要讲解BIO和NIO。

1.1 什么是BIO

     BIO(Block IO):面向流传输(input/output),同步阻塞式IO。

  • InputStream: 输入流(用于读取字节)
  • OutputStream: 输出流(用于写入字节)

在JDK1.4之前,Java网络编程中使用java.net包中的api,数据读写则使用Socket类中i / o流来传输。

     BIO概述:为了解决服务器单个线程I/O请求阻塞,服务器实现模式为一个连接请求一个线程,客户端只要有连接请求服务器就会启动一个线程去处理。

1.1.1 BIO处理方式

通过多线程实现在不同的线程中去处理不同客户端的I/O请求(1:1)。

初识BIO,NIO

1.1.2 服务端代码示例

  1. ServerSocket serverSocket = new ServerSocket(8888); // 监听端口8888
  2. try {
  3. while (true) {
  4. Socket socket = serverSocket.accept(); //阻塞式接收socket连接
  5. new Thread(() -> { //创建一个新的线程
  6. byte[] b = new byte[1024];
  7. try {
  8. InputStream in = socket.getInputStream();
  9. int len = in.read(b); // 读取客户端发送的数据
  10. System.out.println("接收到客户端数据:" + new String(b,0,len));
  11. OutputStream out = socket.getOutputStream();
  12. out.write("successful...".getBytes()); // 返回数据到客户端
  13. //关闭连接
  14. in.close(); out.close(); socket.close();
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }).start();
  19. }
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. } finally {
  23. serverSocket.close();
  24. }

 

1.1.3 存在的问题

    虽然使用多线程解决了多个客户端I/O请求服务端导致阻塞的问题,但是一个连接请求去启动一个线程,在高并发环境下,线程数量过多,会消耗过多的内存资源出现内存溢出,严重则导致服务器宕机。

    这里顺便提一下(伪异步IO):服务端采用固定大小的线程池和队列的方式来处理客户端的连接,无论多少个客户端连接都不会导致内存溢出等问题,但是当线程池中线程的数量达到了最大值时,新的客户端连接会一直在队列中等待(线程池释放某个客户端线程的资源)线程池有空闲位置;如果队列数量满时,会造成大量客户端请求连接等待超时。

 

2.1 什么是NIO

     NIO(官方New IO,又称Non Block IO):面向缓冲区(channel传输),同步非阻塞式IO,选择器(多路复用器)

  • Channel:用来传输字节数据,可以进行双向传输(read/write)

  • Buffer:缓存区用来存储字节数据

  • Selector:多路复用器,所有的客户端连接请求(channel)注册到选择器上面,单个线程处理多个channel连接

在JDK1.4,Windows下采用select模型,Linux下采用Linux IO模型 epoll(有兴趣可以了解下Linux IO模式);NIO编程直接使用java.nio包中的api,数据传输和读写则使用Channel和ByteBuffer。

     NIO概述:服务端实现模式为一个请求(read/write)一个线程,客户端有连接请求会被注册到Selector中,多路复用器轮询到连接有I/O请求时才会启动一个线程去处理数据。

2.1.1 处理方式

当Selector轮询到某个channel中有read/write请求时,才会去启动一个线程去处理。

Selector的好处在于:单线程来处理更多的客户端连接请求(M:1)。

初识BIO,NIO

2.1.2 服务端代码示例 

  1. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  2. serverSocketChannel.configureBlocking(false);//切换为非阻塞模式
  3. serverSocketChannel.bind(new InetSocketAddress(8888));//监听端口8888
  4. //获取选择器
  5. Selector selector = Selector.open();
  6. //将通道注册到选择器,等待连接
  7. serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
  8. //获取选择器中已经准备就绪的事件
  9. while (selector.select() > 0) {
  10. //获取当前选择器所有注册的监听事件
  11. Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
  12. while (selectionKeys.hasNext()) {
  13. //获取事件
  14. SelectionKey sk = selectionKeys.next();
  15. if (sk.isAcceptable()) {
  16. //获取客户端连接
  17. SocketChannel socketChannel = serverSocketChannel.accept();
  18. socketChannel.configureBlocking(false); //切换非阻塞模式
  19. socketChannel.register(selector,SelectionKey.OP_READ);//注册选择器为读模式
  20. }else if(sk.isReadable()){ //(读就绪)有可读数据
  21. new Thread(() -> {
  22. //获取当前选择器上读就绪状态的通道
  23. //读取客户端数据,这里省略
  24. }).start();
  25. }else if(sk.isWritable()){ //(写就绪)可写数据,一般不需要去注册该(可写)事件,在读取数据后写入即可
  26. new Thread(() -> {
  27. //获取当前选择器上写就绪状态的通道
  28. //写入数据到客户端,这里省略
  29. }).start();
  30. }
  31. selectionKeys.remove(); //移除通道中的事件
  32. }
  33. }
  34. //关闭通道
  35. serverSocketChannel.close();

2.1.3 存在的问题

     1.JDK提供的java.nio包进行NIO编程会比较复杂(需要熟练掌握nio包和Java多线程),ByteBuffer读写时需要使用flip()和clear()进行切换,包括字节数据出入站的编解码等问题,建议使用NIO框架(Netty,Mina)来进行开发。

     2.传输数据量过大,读写过程长时,由于同步需要等待整个操作完成才会返回,这时需要考虑业务场景来使用。

 

总结

区别:

      BIO:面向流传输,阻塞式IO。

      NIO:面向缓冲区,非阻塞式IO,选择器。

内存开辟:

      BIO:堆内存中,操作数据时需要先将堆内存拷贝到堆外内存。

      NIO:直接内存,减少了垃圾回收,加快复制的速度(如果存在内存泄漏(堆外内存泄漏),难以排查)。

使用场景:

      BIO:一个连接一个线程(一个线程处理一个连接),如果连接数少,它的延迟是最低的,适用于连接数少延迟低的场景。

      NIO:一个请求一个线程,阻塞数据处理但不阻塞数据接收,适用于数据量小并发高的场景。

 

 

相关文章: