【Java】网络通信IO模型

Java网络编程中的IO(Input/Output)模型是管理计算机对外部数据读取和写入操作的重要机制。Java提供了多种IO模型来满足不同的网络通信需求。

一、阻塞IO(BIO,Blocking I/O)

  • 概念:
    • 阻塞IO是最简单和直观的一种IO模型。在BIO模型中,当用户线程发起系统调用时,内核会一直等待,直到有数据可读或可写,才会返回结果。
  • 特点:
    • 同步阻塞:服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务器端就需要启动一个线程进行处理。如果这个连接不做任何事情,会造成不必要的线程开销。
    • 简单易用:BIO模型的编程流程相对简单,容易理解和实现。
    • 资源消耗大:在高并发情况下,每个线程都会阻塞,导致系统性能急剧下降,且对服务器资源要求较高。
  • 适用场景:
    • 适用于连接数目比较小且固定的架构。
    • 常用于文件操作或较少并发连接的网络服务。
  • 示例:Java传统Socket模型,线程在读写时会被阻塞。
    // 客户端
    try (Socket socket = new Socket("localhost", 8080);
         BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
          
          
        // 阻塞直到数据到达
        String response = in.readLine();
        System.out.println("Response: " + response);
    }
    
    // 服务端
    try (ServerSocket serverSocket = new ServerSocket(8080);
         Socket clientSocket = serverSocket.accept(); // 阻塞等待连接
         PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
          
          
        out.println("Hello from blocking server!");
    }
    

二、非阻塞IO(NIO,Non-blocking I/O)

  • 概念:
    • NIO模型允许在不阻塞当前线程的情况下发起IO操作。如果操作无法立即完成,方法会返回特定值(如0或特定错误码),而不是阻塞。
  • 特点:
    • 非阻塞模式:线程在等待IO操作完成时不会被阻塞,可以继续执行其他任务。
    • 高效利用资源:通过减少线程的阻塞等待时间,提高了系统资源的利用率。
    • 需要轮询:由于非阻塞IO不会主动通知线程操作完成,因此需要线程反复检查或轮询IO操作的状态。
  • 核心组件:
    • Channel(通道):表示打开到IO设备(如文件、套接字)的连接,支持非阻塞读取和写入。
    • Buffer(缓冲区):用于存取数据的内存块,提供了方便访问该块内存的方法。
    • Selector(选择器):能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。
  • 适用场景:
    • 适用于连接数目多且连接时间较短的架构,如聊天服务器、弹幕系统等。
  • 示例:多人聊天室
    • 服务端
      package com.example.helloword;
      
      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.ByteBuffer;
      import java.nio.channels.*;
      import java.util.Iterator;
      import java.util.Set;
      
      /**
       * 服务端功能
       *  监听新连接:通过 ServerSocketChannel 接受客户端连接,并注册到 Selector。
       *  处理消息:读取客户端消息并广播给其他用户。
       *  上下线通知:当客户端连接或断开时,广播通知所有用户。
       *  异常处理:检测客户端异常断开并清理资源。
       */
      public class GroupChatServer {
              
              
          private static final int PORT = 6667;
          private ServerSocketChannel serverSocketChannel;
          private Selector selector;
      
          public GroupChatServer() {
              
              
              try {
              
              
                  selector = Selector.open();
                  serverSocketChannel = ServerSocketChannel.open();
                  serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
                  serverSocketChannel.configureBlocking(false);
                  serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
                  System.out.println("服务器启动,监听端口:" + PORT);
              } catch (IOException e) {
              
              
                  e.printStackTrace();
              }
          }
      
          public void listen() {
              
              
              try {
              
              
                  while (true) {
              
              
                      selector.select();
                      Set<SelectionKey> selectionKeys = selector.selectedKeys();
                      Iterator<SelectionKey> iterator = selectionKeys.iterator();
                      while (iterator.hasNext()) {
              
              
                          SelectionKey key = iterator.next();
                          iterator.remove(); // 防止重复处理
      
                          if (key.isAcceptable()) {
              
              
                              handleAccept(key);
                          } else if (key.isReadable()) {
              
              
                              handleRead(key);
                          }
                      }
                  }
              } catch (IOException e) {
              
              
                  e.printStackTrace();
              }
          }
      
          private void handleAccept(SelectionKey key) throws IOException {
              
              
              SocketChannel clientChannel = serverSocketChannel.accept();
              clientChannel.configureBlocking(false);
              clientChannel.register(selector, SelectionKey.OP_READ);
              String msg = "[" + clientChannel.getRemoteAddress() + "] 上线了";
              System.out.println(msg);
              broadcast(msg, clientChannel); // 广播上线通知
          }
      
          private void handleRead(SelectionKey key) {
              
              
              SocketChannel clientChannel = (SocketChannel) key.channel();
              ByteBuffer buffer = ByteBuffer.allocate(1024);
              try {
              
              
                  int len = clientChannel.read(buffer);
                  if (len > 0) {
              
              
                      String msg = new String(buffer.array(), 0, len).trim();
                      System.out.println("收到消息: " + msg);
                      broadcast(msg, clientChannel); // 广播消息
                  } else if (len == -1) {
              
               // 客户端正常关闭
                      handleDisconnect(clientChannel, "下线");
                  }
              } catch (IOException e) {
              
              
                  handleDisconnect(clientChannel, "异常断开");
              }
          }
      
          private void handleDisconnect(SocketChannel clientChannel, String reason) {
              
              
              try {
              
              
                  String msg = "[" + clientChannel.getRemoteAddress() + "] " + reason;
                  System.out.println(msg);
                  broadcast(msg, clientChannel);
                  clientChannel.close();
              } catch (IOException ex) {
              
              
                  ex.printStackTrace();
              }
          }
      
          private void broadcast(String msg, SocketChannel excludeChannel) throws IOException {
              
              
              for (SelectionKey key : selector.keys()) {
              
              
                  Channel targetChannel = key.channel();
                  if (targetChannel instanceof SocketChannel && targetChannel != excludeChannel) {
              
              
                      SocketChannel dest = (SocketChannel) targetChannel;
                      ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                      dest.write(buffer);
                  }
              }
          }
      
          public static void main(String[] args) {
              
              
              new GroupChatServer().listen();
          }
      }
      
    • 客户端
      package com.example.helloword;
      
      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.ByteBuffer;
      import java.nio.channels.SelectionKey;
      import java.nio.channels.Selector;
      import java.nio.channels.SocketChannel;
      import java.util.Scanner;
      import java.util.Set;
      
      /**
       * 客户端功能
       *  连接服务器:初始化时自动连接服务器。
       *  发送消息:读取用户输入并发送到服务器。
       *  接收消息:监听服务器广播的消息并实时显示。
       *  退出机制:输入 exit 退出群聊并关闭连接。
       */
      public class GroupChatClient {
              
              
          private static final String HOST = "127.0.0.1";
          private static final int PORT = 6667;
          private SocketChannel socketChannel;
          private Selector selector;
          private String username;
      
          public GroupChatClient() {
              
              
              try {
              
              
                  selector = Selector.open();
                  socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
                  socketChannel.configureBlocking(false);
                  socketChannel.register(selector, SelectionKey.OP_READ);
                  username = "用户" + socketChannel.getLocalAddress().toString().substring(1).replaceAll("[:.]", "");
                  System.out.println(username + " 已连接到服务器");
              } catch (IOException e) {
              
              
                  e.printStackTrace();
              }
          }
      
          public void sendMsg(String msg) {
              
              
              if ("exit".equalsIgnoreCase(msg)) {
              
              
                  try {
              
              
                      socketChannel.close();
                      selector.close();
                      System.out.println("已退出群聊");
                      System.exit(0);
                  } catch (IOException e) {
              
              
                      e.printStackTrace();
                  }
                  return;
              }
              try {
              
              
                  String formattedMsg = username + ": " + msg;
                  ByteBuffer buffer = ByteBuffer.wrap(formattedMsg.getBytes());
                  socketChannel.write(buffer);
              } catch (IOException e) {
              
              
                  e.printStackTrace();
              }
          }
      
          public void readMsg() {
              
              
              try {
              
              
                  while (true) {
              
              
                      selector.select();
                      System.out.println("==> readMsg");
                      Set<SelectionKey> keys = selector.selectedKeys();
                      for (SelectionKey key : keys) {
              
              
                          if (key.isReadable()) {
              
              
                              SocketChannel channel = (SocketChannel) key.channel();
                              ByteBuffer buffer = ByteBuffer.allocate(1024);
                              int len = channel.read(buffer);
                              if (len > 0) {
              
              
                                  String msg = new String(buffer.array(), 0, len).trim();
                                  System.out.println(msg);
                              } else if (len == -1) {
              
              
                                  System.out.println("服务端已关闭连接");
                                  key.cancel();       // 取消 SelectionKey 的注册
                                  channel.close();    // 关闭通道
                                  System.exit(0);
                              }
                          }
                      }
                      keys.clear();
                  }
              } catch (IOException e) {
              
              
                  System.out.println("与服务器断开连接");
                  System.exit(0);
              } finally {
              
              
                  // 关闭资源
                  try {
              
              
                      if (socketChannel != null) socketChannel.close();
                      if (selector != null) selector.close();
                  } catch (IOException ex) {
              
              
                      ex.printStackTrace();
                  }
              }
          }
      
          public static void main(String[] args) {
              
              
              GroupChatClient client = new GroupChatClient();
              // 启动线程监听服务器消息
              new Thread(client::readMsg).start();
      
              // 读取用户输入并发送
              Scanner scanner = new Scanner(System.in);
              while (true) {
              
              
                  String msg = scanner.nextLine();
                  client.sendMsg(msg);
              }
      
          }
      }
      
    • 允许开启多个客户端:(IDEA 2024)
      • Edit Confihurations
        在这里插入图片描述
      • Modify option --> Allow multiple instance
        在这里插入图片描述

三、异步IO(AIO,Asynchronous I/O)

  • 概念:
    • AIO模型允许在发起IO操作后不阻塞线程,并且在操作完成时通过回调或未来(Future)来通知应用程序。
  • 特点:
    • 异步操作:发起IO操作后立即返回,操作在后台进行,不会阻塞线程。
    • 回调通知:操作完成后,通过回调或Future通知应用程序,进一步减少了线程的阻塞。
    • 编程复杂:相比BIO和NIO,AIO的编程模型更为复杂。
      适用场景:
    • 适用于连接数目多且连接时间较长的架构,如相册服务器等。
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
    
    
    @Override
    public void completed(AsynchronousSocketChannel client, Void attachment) {
    
    
        server.accept(null, this); // 继续接收新连接
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    
    
            @Override
            public void completed(Integer result, ByteBuffer buffer) {
    
    
                // 处理读取的数据
            }
        });
    }
});

四、总结

模型 BIO NIO AIO
阻塞性 同步阻塞 同步非阻塞 异步非阻塞
线程模型 1 连接 1 线程 多路复用(单线程/线程池) 回调驱动
复杂度 中高
适用场景 低并发、简单应用 高并发、实时响应(如 Netty) 特定场景(如文件操作)
接口 Socket
ServerSocket
SocketChanne
ServerSocketChannel
AsynchronousSocketChannel
AsynchronousServerSocketChannel

实践建议:

  • BIO:仅用于教学或低负载场景。
  • NIO:大多数高并发场景的首选,结合 Netty 等框架简化开发。
  • AIO:谨慎使用,需评估操作系统支持与业务需求。

为什么主流框架(如 Netty)选择 NIO 而非 AIO?

  • NIO 的成熟度高,跨平台兼容性好。
  • Linux 的 AIO 实现不完全(如文件 AIO 稳定,网络 AIO 依赖线程池)。
  • NIO 通过优化(如 epoll 边缘触发)已能支撑百万级并发。