Java 网络通信编程(9):从 BIO 到 NIO

一、BIO 和 NIO

在我们之前的网络通信编程项目中,我们用的是传统的BIO 模型。但随着互联网高并发、大流量的需求升级,BIO 的瓶颈愈发明显, NIO 成为了高并发编程的核心方案。那么什么是 BIO?什么是 NIO?二者的核心特点与本质区别又是什么?

1.BIO

BIO 全称 Blocking IO,是我们最常使用的同步阻塞式 IO。用户线程主动发起 IO 请求,并且需要等待 IO 操作完成才能继续执行,IO 过程中线程会被挂起,无法做其他任务。结合我们之前的编程经验,它的一些特点可以总结如下:

  • 阻塞贯穿全程:服务端 accept() 等待客户端连接时会阻塞;客户端连接后,read() / write() 读写数据时也会阻塞。
  • 一连接一线程:每一个客户端连接,服务端都需要创建一个独立线程专门处理,连接断开线程销毁。
  • 基于流(Stream)操作 :数据以字节流 / 字符流的形式传输,流是单向的。
  • 无缓冲区:数据直接在程序和 IO 设备之间传输,没有中间缓冲层。

这些特点带来了 BIO 的一些原生缺陷:并发连接数过高时,会创建大量线程,导致服务器内存溢出、CPU 负载过高,同时线程闲置率却极高,资源利用率极低,导致它仅适合连接数少、并发量低的场景。

2.NIO

NIO 全称 New IO ,也被称为 Non-blocking IO(同步非阻塞 IO)。 它弥补了 BIO 的性能缺陷,是主流高并发网络框架的底层基础。它的一些核心特点如下:

  • 非阻塞:线程发起连接、读写请求时,不会被挂起;如果没有数据 / 连接,会直接返回,线程可以去处理其他任务。
  • 多路复用 :一个线程可以管理成百上千个客户端连接,通过**选择器(Selector)**监听所有连接的事件,有事件才处理。
  • 基于缓冲区(Buffer) :NIO 最核心的设计,所有数据读写必须通过 Buffer,Buffer 是数据的载体,也是我们下一节重点讲解的内容。
  • 基于通道(Channel) :数据通过 Channel 传输,Channel 是双向的(可读可写),效率远高于 BIO 的单向流。

对比 BIO,NIO 有一些明显机制优势:首先它线程数量极少,资源占用极低,而且非阻塞和多路复用使其并发能力呈指数级提升,因此它适合高并发、长连接、大量客户端的场景。

二、Buffer

1.四个核心参数

NIO 所有数据读写必须经过 Buffer ,Buffer 是 NIO 程序中最常用、最基础 的组件,所以我们本期先来讲解 Buffer 的四个核心参数。

1.capacity(容量) :缓冲区的固定最大大小 ,创建缓冲区时指定,一旦确定无法修改。代表缓冲区最多能存储多少个数据单元。

2.limit(界限 / 限制): 数据操作的上限边界。

  • 写模式:表示最多可以写入多少数据
  • 读模式:表示最多可以读取多少有效数据,limit 之后的位置,禁止读写。

3.position(当前位置): 读写指针的下一个操作位置,初始值为 0。 每写入 / 读取一个数据,position 自动向后移动一位,始终指向待操作的位置。

4.mark(标记): 备忘标记位,用于记录指定的 position 位置 。 调用 mark() 记录位置,调用 reset() 可以直接回到标记点,初始值为 -1(未设置标记)

我们可以通过创建缓冲区和写入数据两步操作,直观观察参数的动态变化。

java 复制代码
public class BufferDemo {

    public static void main(String[] args) {
        // 1. 分配容量为10的字节缓冲区,默认【写模式】
        ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());

        // 2. 连续写入4个字节数据:A、B、C、D
        buffer.put((byte)'A');
        buffer.put((byte)'B');
        buffer.put((byte)'C');
        buffer.put((byte)'D');
        System.out.println(" ------------------------ ");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        // 剩余可操作空间:limit - position
        System.out.println("remaining:"+buffer.remaining());
        System.out.println(" ------------------------ ");
    }
}

首先执行 ByteBuffer.allocate 开辟一个固定容量为 10 的字节缓冲区,它默认处于写模式。 接着执行 4 次 put() 方法,写入字符 A,B,C,D:每调用 1 次 put() ,position 自动 +1 (指针向后移动一位),写入 4 个数据后,position 从 0 变为 4,而 capacity 和 limit 始终保持不变 。此外新增方法 remaining() 计算剩余可操作空间

执行结果:

下面我们再来看一下 mark () 标记reset () 重置 操作。

java 复制代码
public class BufferDemo {

    public static void main(String[] args) {
        //
        ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());

        buffer.put((byte)'A');
        buffer.put((byte)'B');
        buffer.mark();
        buffer.put((byte)'C');
        buffer.put((byte)'D');
        System.out.println(" ------------------------ ");
//        System.out.println("capacity:"+buffer.capacity());
//        System.out.println("limit:"+buffer.limit());
//        System.out.println("position:"+buffer.position());
//        System.out.println("remaining:"+buffer.remaining());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position1:"+buffer.position());
        buffer.reset(); //回到标记位置
        System.out.println("position2:"+buffer.position());
        System.out.println("char : "+(char)buffer.get());
        System.out.println("remaining:"+buffer.remaining());
        System.out.println(" ------------------------ ");
    }
}

我们在写入字符 A、B 后调用 mark(),将此时的 position 位置记录下来作为标记点,后续继续写入 C、D 使 position 向后移动,再通过 reset() 方法,就能让 position 直接回到之前 mark() 标记的位置,这两个方法就像给缓冲区添加了书签,可以随时回到指定的操作位置,配合 get() 读取数据,读取的是当前 position=2 位置的字符 C。

2.Buffer 读写切换

我们在上文掌握了写模式 的基础上,新增了flip () 读模式切换、hasRemaining() 遍历读取、rewind() 重置指针三个核心操作,这是 NIO Buffer从写入到读取的必备流程。

java 复制代码
public class BufferDemo {

    public static void main(String[] args) {
        //
        ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());

        buffer.put((byte)'A');
        buffer.put((byte)'B');
        buffer.put((byte)'C');
        buffer.put((byte)'D');
        System.out.println(" ------------------------ ");
        System.out.println(" ------------------------ ");

        buffer.flip(); //切换成读数据模型
        //取出所有数据:计算缓冲区可读取数据个数
        while(buffer.hasRemaining()){
            System.out.println((char)buffer.get());
        }
        //重重 position
        buffer.rewind();
        System.out.println("rewind = "+(char)buffer.get());
    }
}

flip() 是 Buffer 从写模式转为读模式的方法,调用后会将 limit 设置为写入结束后的 position 值(本例中为 4),同时把 position 重置为 0,以此划定有效数据的可读范围。之后通过 hasRemaining() 判断是否存在未读数据,配合 get() 方法循环遍历读取,每读取一次数据 position 就自动向后移动一位,依次打印出 A、B、C、D。最后调用 rewind() 方法重置读取指针,该方法会将 position 重新置为 0 且保持 limit 不变,让我们直接从头再次读取缓冲区内容,因此能够获取到字符 A。

以上是对于 Buffer 基础的讲解,之后我们还将学习选择器、通道这些其他 NIO 组件基础。

相关推荐
亦暖筑序1 小时前
Java 8老系统Browser Agent实战:三层拦截把AI操作后台变成可审计流程
java·后端·设计模式
用户298698530144 小时前
Java 实现 Word 文档加密与权限解除
java·后端
Yeats_Liao5 小时前
14:Servlet中的页面跳转-Java Web
java·后端·架构
未秃头的程序猿5 小时前
告别"if-else地狱"!Java 21模式匹配,代码优雅了10倍
java·后端·面试
鹤望兰6755 小时前
字节跳动国际支付-后端开发-三面面经
java
Flittly5 小时前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
RainCity5 小时前
Java Swing 自定义组件库分享(十二)
java·笔记·后端
吃饱了得干活21 小时前
Spring Cloud Gateway 微服务网关:路由、断言、过滤器
java·spring cloud
lwx572801 天前
探秘InnoDB:搞懂它的内存、线程、磁盘与日志刷盘策略
java·后端
Flynt1 天前
从Spring Boot 4.0升到4.1,我在Maven和gRPC上栽了跟头
java·spring boot·后端