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()
↓
处理事件
相关推荐
一 乐1 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长1 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn1 小时前
Spring 注解使用详解
java·spring
一条小小yu1 小时前
Spring IoC的理解
java·后端·spring
落魄实习生2 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈2 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
自强的小白3 小时前
核心功能(Service接口)
java·mybatis
乌暮4 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习
大猫和小黄5 小时前
深入理解 Java Optional:从设计哲学到工程实战
java