BIO(Blocking I/O) 和 NIO(Non‑Blocking I/O) 两种不同的 I/O 模型

一、BIO(Blocking I/O)

1. BIO 的概念

BIO = 同步阻塞 I/O

特点:

  • 每个连接 必须分配一个线程
  • 读写操作 会阻塞线程
  • 当没有数据时,线程会 一直等待

简单理解:

客户端请求 → 服务器线程处理 → 读数据 → 阻塞等待


2. BIO 工作流程

流程如下:

1 客户端连接服务器

2 服务器 ServerSocket.accept() 阻塞等待连接

3 创建新线程处理客户端

4 线程调用 InputStream.read() 阻塞等待数据

5 处理数据并返回结果

结构示意:

复制代码
客户端1  ──线程1──┐
客户端2  ──线程2──┤  BIO服务器
客户端3  ──线程3──┘

每个客户端一个线程。


3. BIO 优缺点

优点:

  • 编程简单
  • 容易理解
  • 代码结构清晰

缺点:

  • 线程开销大
  • 连接数一多就崩
  • 不适合高并发

举例:

如果 10000 个连接

需要

复制代码
10000 个线程

服务器很容易耗尽资源。


4. BIO Java 实现

服务器代码:

java 复制代码
import java.io.*;
import java.net.*;

public class BIOServer {

    public static void main(String[] args) throws Exception {

        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIO服务器启动");

        while (true) {

            Socket socket = serverSocket.accept(); // 阻塞等待连接
            System.out.println("客户端连接成功");

            new Thread(() -> {
                try {

                    InputStream in = socket.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));

                    String msg;

                    while ((msg = reader.readLine()) != null) { // 阻塞等待数据
                        System.out.println("收到:" + msg);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

客户端代码:

java 复制代码
import java.io.*;
import java.net.*;

public class BIOClient {

    public static void main(String[] args) throws Exception {

        Socket socket = new Socket("127.0.0.1", 8080);

        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

        out.println("hello BIO");

        socket.close();
    }
}

二、NIO(Non‑Blocking I/O)

1. NIO 的概念

NIO = 同步非阻塞 I/O

核心特点:

  • 一个线程可以处理多个连接
  • 使用 Selector(选择器)
  • 通过 事件驱动

核心组件:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

2. NIO 工作机制

传统 BIO:

java 复制代码
连接 → 创建线程

NIO:

java 复制代码
连接注册到 Selector
一个线程轮询事件

流程:

1 创建 ServerSocketChannel 2 设置 非阻塞模式 3 注册到 Selector 4 监听事件

  • ACCEPT
  • READ
  • WRITE 5 有事件时处理

结构:

java 复制代码
             ┌────客户端1
             │
Selector ────┼────客户端2
             │
             └────客户端3
              (一个线程)

3. NIO 三大核心组件

1 Channel(通道)

类似 Socket,但支持非阻塞。

常见:

java 复制代码
ServerSocketChannel
SocketChannel
FileChannel

2 Buffer(缓冲区)

NIO 必须使用 Buffer

常见:

java 复制代码
ByteBuffer
IntBuffer
CharBuffer

读取流程:

java 复制代码
channel → buffer → 程序

写入流程:

java 复制代码
程序 → buffer → channel

3 Selector(选择器)

Selector 用来 监听多个 Channel 事件

支持事件:

java 复制代码
SelectionKey.OP_ACCEPT
SelectionKey.OP_CONNECT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

三、NIO Java 实现

1 NIO服务器

java 复制代码
import java.net.*; 
// 导入网络相关类,例如 InetSocketAddress(用于表示IP+端口)

import java.nio.*; 
// 导入NIO缓冲区类,例如 ByteBuffer

import java.nio.channels.*; 
// 导入NIO通道相关类,例如 ServerSocketChannel、SocketChannel、Selector

import java.util.*; 
// 导入集合类,例如 Set、Iterator


public class NIOServer {

  public static void main(String[] args) throws Exception {

    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    // 创建一个服务器通道
    // 类似 BIO 里的 ServerSocket
    // 用于监听客户端连接

    serverChannel.socket().bind(new InetSocketAddress(8080));
    // 绑定端口 8080
    // InetSocketAddress 表示 IP + 端口
    // 默认监听本机地址

    serverChannel.configureBlocking(false);
    // 设置为 **非阻塞模式**
    // 如果不设置,默认是阻塞
    // NIO 必须使用非阻塞模式

    Selector selector = Selector.open();
    // 创建一个 Selector(选择器)
    // Selector 的作用:
    // 一个线程监听多个 Channel 的事件

    serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    // 将 serverChannel 注册到 selector 上
    // 并监听 **OP_ACCEPT 事件**
    // OP_ACCEPT 表示:有新的客户端连接

    System.out.println("NIO服务器启动");

    while (true) {

      selector.select(); 
      // 阻塞等待事件发生
      // 如果有事件(连接、读写)发生就返回
      // 没有事件会一直等待

      Set<SelectionKey> keys = selector.selectedKeys();
      // 获取所有已经发生事件的 key
      // 每个 key 代表一个 Channel 的事件

      Iterator<SelectionKey> iterator = keys.iterator();
      // 创建迭代器,用来遍历所有事件

      while (iterator.hasNext()) {

        SelectionKey key = iterator.next();
        // 获取当前事件

        if (key.isAcceptable()) {
        // 判断是否是 **客户端连接事件**

          ServerSocketChannel server =
              (ServerSocketChannel) key.channel();
          // 获取触发事件的 Channel
          // 这里就是 serverChannel

          SocketChannel channel = server.accept();
          // 接收客户端连接
          // 返回 SocketChannel
          // 类似 BIO 中的 socket = serverSocket.accept()

          channel.configureBlocking(false);
          // 将客户端 Channel 设置为非阻塞模式
          // 所有 NIO Channel 都必须是非阻塞

          channel.register(selector, SelectionKey.OP_READ);
          // 把客户端 Channel 注册到 selector
          // 并监听 **读事件**
          // 表示当客户端发送数据时触发

          System.out.println("客户端连接");

        }

        if (key.isReadable()) {
        // 判断是否是 **读事件**
        // 表示客户端发送了数据

          SocketChannel channel =
              (SocketChannel) key.channel();
          // 获取客户端 Channel

          ByteBuffer buffer = ByteBuffer.allocate(1024);
          // 创建一个缓冲区
          // 大小 1024 字节
          // 用来存放读取的数据

          int len = channel.read(buffer);
          // 从 channel 读取数据到 buffer
          // 返回读取的字节数
          // 可能返回:
          // >0 读取成功
          // 0 没有数据
          // -1 连接关闭

          if (len > 0) {

            buffer.flip();
            // 切换为 **读模式**
            // 因为 buffer 写入完数据后
            // 需要 flip 才能读取

            byte[] data = new byte[buffer.remaining()];
            // 创建字节数组
            // remaining() 表示 buffer 中剩余可读数据

            buffer.get(data);
            // 从 buffer 读取数据到 byte[]

            System.out.println("收到:" + new String(data));
            // 把字节转换为字符串并打印

          }

        }

        iterator.remove();
        // 移除当前事件
        // 防止下次循环重复处理

      }
    }
  }
}

2 NIO客户端

java 复制代码
import java.net.*;
import java.nio.*;
import java.nio.channels.*;

public class NIOClient {

    public static void main(String[] args) throws Exception {

        SocketChannel channel = SocketChannel.open();

        channel.connect(new InetSocketAddress("127.0.0.1", 8080));

        ByteBuffer buffer = ByteBuffer.wrap("hello NIO".getBytes());

        channel.write(buffer);

        channel.close();
    }
}

四、BIO vs NIO 对比

特性 BIO NIO
IO模式 阻塞 非阻塞
线程模型 一连接一线程 一个线程处理多个连接
扩展性
编程复杂度 简单 较复杂
适用场景 连接数少 高并发

几个真实可运行的 Java 示例来演示这段 NIO 服务器代码在不同情况下是如何处理的。每个例子都是一个客户端程序

先启动你的 NIOServer,然后运行下面不同的客户端代码。


示例1:一个客户端发送一条消息

客户端代码:

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client1 {

    public static void main(String[] args) throws Exception {

        SocketChannel channel = SocketChannel.open();

        channel.connect(new InetSocketAddress("127.0.0.1", 8080));

        ByteBuffer buffer = ByteBuffer.wrap("hello server".getBytes());

        channel.write(buffer);

        channel.close();
    }
}

运行流程:

1 客户端连接服务器

触发服务器代码:

java 复制代码
if (key.isAcceptable())

服务器打印:

java 复制代码
客户端连接

2 客户端发送数据

触发服务器代码:

java 复制代码
if (key.isReadable())

服务器读取:

java 复制代码
int len = channel.read(buffer);

服务器输出:

java 复制代码
收到:hello server

示例2:一个客户端连续发送多条数据

客户端代码:

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client2 {

    public static void main(String[] args) throws Exception {

        SocketChannel channel = SocketChannel.open();

        channel.connect(new InetSocketAddress("127.0.0.1", 8080));

        for (int i = 0; i < 5; i++) {

            String msg = "message-" + i;

            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());

            channel.write(buffer);

            Thread.sleep(1000);
        }

        channel.close();
    }
}

服务器输出可能是:

java 复制代码
客户端连接
收到:message-0
收到:message-1
收到:message-2
收到:message-3
收到:message-4

这里服务器循环触发:

java 复制代码
key.isReadable()

每次客户端发送数据都会触发一次。


示例3:多个客户端同时连接

客户端代码(启动多个):

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client3 {

    public static void main(String[] args) throws Exception {

        SocketChannel channel = SocketChannel.open();

        channel.connect(new InetSocketAddress("127.0.0.1", 8080));

        String msg = "client message";

        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());

        channel.write(buffer);

        Thread.sleep(5000);

        channel.close();
    }
}

同时运行 3次 Client3

服务器输出:

java 复制代码
客户端连接
客户端连接
客户端连接
收到:client message
收到:client message
收到:client message

这里发生的事情:

服务器只有 一个线程

但 Selector 同时管理:

java 复制代码
client1
client2
client3

代码处理流程:

java 复制代码
selector.select()
↓
selectedKeys()
↓
遍历每个事件

示例4:客户端发送大数据

客户端代码:

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client4 {

    public static void main(String[] args) throws Exception {

        SocketChannel channel = SocketChannel.open();

        channel.connect(new InetSocketAddress("127.0.0.1", 8080));

        StringBuilder sb = new StringBuilder();

        for(int i=0;i<500;i++){
            sb.append("data");
        }

        ByteBuffer buffer = ByteBuffer.wrap(sb.toString().getBytes());

        channel.write(buffer);

        channel.close();
    }
}

服务器处理代码:

java 复制代码
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);

这里服务器只读取 1024字节

如果数据很大,就会发生:

java 复制代码
多次 READ 事件

服务器可能输出多次:

java 复制代码
收到:data...
收到:data...

示例5:客户端断开连接

客户端代码:

java 复制代码
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

public class Client5 {

    public static void main(String[] args) throws Exception {

        SocketChannel channel = SocketChannel.open();

        channel.connect(new InetSocketAddress("127.0.0.1", 8080));

        channel.close();
    }
}

服务器执行:

java 复制代码
int len = channel.read(buffer);

返回:

java 复制代码
-1

表示:

java 复制代码
客户端关闭连接

正确服务器代码通常会写:

java 复制代码
if(len == -1){
    channel.close();
}

一个完整测试组合

可以这样测试:

启动服务器

java 复制代码
NIOServer

然后运行:

java 复制代码
Client1
Client2
Client3
Client4

服务器会不断触发:

java 复制代码
OP_ACCEPT
OP_READ

核心循环:

java 复制代码
selector.select()
↓
selectedKeys()
↓
处理事件
相关推荐
rannn_1111 小时前
【Redis|高级篇3】Redis最佳实践|键值设计、批处理优化、服务端优化、服务器优化、集群还是主从
java·服务器·redis·后端·缓存
matlabgoodboy1 小时前
留学生计算机cs作业辅导java SQL数据库 c语言编程 软件工程辅导
java·数据库·sql
aXin_ya1 小时前
微服务 第一天
java·运维·微服务
8Qi81 小时前
Elasticsearch 初识篇:核心概念与环境搭建
java·大数据·分布式·elasticsearch·搜索引擎·中间件
消失的旧时光-19432 小时前
Spring 核心思想解析:IoC 与 DI 一文讲透(从入门到工程理解)
java·ioc·di
小梁努力敲代码2 小时前
抽奖系统-测试报告
java·功能测试
devpotato2 小时前
人工智能(九)- Spring AI MCP客户端开发
java·mcp
疯狂打码的少年2 小时前
【Day14 Java转Python】从Java到Python——用Python重构一个Java小工具(文件批量重命名实战)
java·python·重构
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第十八期 - 命令模式】命令模式 —— 请求封装与撤销实现、优缺点与适用场景
java·后端·设计模式·软件工程·命令模式