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 小时前
Effective C++ 条款05:了解 C++ 默默编写并调用哪些函数
java·linux·开发语言·c++·effective c++·编程范式
HackTwoHub1 小时前
关于文件上传漏洞深度绕过利用教程,突破命令执行限制
运维·安全·web安全·网络安全·系统安全·安全架构
Full Stack Developme1 小时前
G1回收器的工作机制
java·jvm
Web极客码1 小时前
如何用 Docker 容器与“看门狗”脚本安全驯服 OpenClaw
服务器·人工智能·ai编程
rcms152702692181 小时前
Matrox Genesis 63039620241采集卡
网络
砍材农夫1 小时前
物联网实战:Spring Boot + Netty 搭建 MQTT平台 | 多协议适配与模块化设计
java·spring boot·后端·物联网·spring
William.csj1 小时前
服务器——终端ssh可以连接进服务器,vscode连接不进去服务器的解决办法
服务器·vscode·ssh
网安小白的进阶之路1 小时前
B模块 安全通信网络 第二门课IPv6与WLAN 05
网络·安全
云烟成雨TD1 小时前
Spring AI 1.x 系列【41】接入高德 MCP 服务
java·人工智能·spring