当前位置: 首页 > news >正文

Java NIO 核心精讲(上):Channel、Buffer、Selector 详解与 ByteBuffer 完全指南

non-blocking IO 非阻塞IO

一、三大组件

1.1 Channel

1.1.1 定义

是一种读写数据的双向通道

1.1.2 分类

  • FileChannel

文件传输通道

  • DatagramChannel

UDP传输通道

  • SocketChannel

TCP传输通道,服务器和客户端都可以使用

  • ServerChannel

TCP传输通道,专用于服务器通道

1.2 Buffer

1.2.1 定义

内存缓冲区,用来缓冲读写数据,读写数据都需要通过buffer

1.2.2 分类

  • ByteBuffer

MappedByteBuffer

DirectByteBuffer

HeapByteBuffer

  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

1.3 Selector

1.3.1 定义

配合一个线程,来管理多个Channel,获取这些Channel上发生的事件,这些Channel工作在非阻塞模式下;

1.3.2 优缺点

适合连接数多的场景

适合流量低的场景

1.3.3 数据模型

如果selector发现channel有执行事件,通知thread执行,selector可以发现哪些channel有可执行事件

1.4 其他

1.4.1 多线程

1.4.1.1 数据模型

1.4.1.2 优缺点

内存占用高

线程切换成本高

只适合连接数少的场景

1.4.2 线程池

1.4.2.1 数据模型

1.4.2.2 优缺点

阻塞模式下,只能处理一个socket

仅适用短连接场景

二、ByteBuffer

2.1 正确使用步骤

  1. 向buffer中写数据,例如:调用 channel.read(buffer)
  2. 调用flip()切换至读模式
  3. 从buffer中读数据,例如:buffer.get()
  4. 调用clear()或compact()切换至写模式
    1. clear与compact的区别:
    2. clear:如果buffer中数据还没有读完,会清除,从头写入数据
    3. compact:如果buffer中数据还没有读完,会继续从未读取数据的位置继续写入
  1. 重复1~4步

2.2 代码案例

public static void main(String[] args) {// FileChannel// 1. 输入输出流, 2. RandomAccessFiletry (FileChannel channel = new FileInputStream("data.txt").getChannel()) {// 准备缓冲区,初始化大小为10字节ByteBuffer buffer = ByteBuffer.allocate(10);while(true) {// 从 channel 读取数据,向 buffer 写入int len = channel.read(buffer);log.debug("读取到的字节数 {}", len);if(len == -1) { // 没有内容了break;}// 打印 buffer 的内容buffer.flip(); // 切换至读模式while(buffer.hasRemaining()) { // 是否还有剩余未读数据byte b = buffer.get();log.debug("实际字节 {}", (char) b);}buffer.clear(); // 切换为写模式 buffer.compact()}} catch (IOException e) {e.printStackTrace();}}

2.3 流程分析

ByteBuffer 有以下重要属性

  • capacity 容量
  • position 偏移量
  • limit 读/写限制

一开始:

写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态

flip 动作发生后,position 切换为读取位置,limit 切换为读取限制

读取 4 个字节后,状态

clear 动作发生后,状态

compact 方法,是把未读完的部分向前压缩,然后切换至写模式

2.4 分配空间

两种方式:

  • ByteBuffer.allocate(16):分配堆内存

特点:读写效率低,受GC影响(每进行一次GC,信息都需要来回copy,因此,效率比较低)

  • ByteBuffer.allocateDirect(16):分配直接内存

特点:读写效率高(少一次拷贝),不会受GC影响,分配效率低(需要调用内核API,使用不当,容易造成内存溢出)

2.5 向Buffer写数据

两种方式:

使用channel的read的方式

int readBytes = channel.read(byteBuffer);

使用buffer的put的方式

buffer.put(127);
buffer.put((byte)127)

2.6 从Buffer读数据

两种方式:

使用channel的write的方式

int writeBytes = channel.write(buffer);

使用Buffer的get的方式

byte b = buffer.get();

get()方法会让position指针向后走,如果想重复读取数据:

1、可以使用rewind()方法将position重置为0

2、或者调用get(int index),获取索引index的内容,指针不会移动

2.7 mark & reset

mark:打标记

reset:重置到mark的位置(偏移量会发生变化)

案例如下:

byte[] buffer = {q,w,e,r,t,y,u};
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
buffer.mark(); // 加标记,索引2 的位置
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
buffer.reset(); // 将 position 重置到索引 2
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());

2.8 Buffer常用方法

2.8.1 字符串转ByteBuffer

1、通过字符串转字节,再设置ByteBuffer(需要转成读模式,才可以读取)

2、使用Charset的方式 (会自动转成读模式)

3、使用wrap的方式 Nio的工具类 (会自动转成读模式)

public static void main(String[] args) {// 1. 字符串转为 ByteBufferByteBuffer buffer1 = ByteBuffer.allocate(16);buffer1.put("hello".getBytes());debugAll(buffer1);// 2. CharsetByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello");debugAll(buffer2);// 3. wrapByteBuffer buffer3 = ByteBuffer.wrap("hello".getBytes());debugAll(buffer3);}

2.8.2 ByteBuffer转字符串

public static void main(String[] args) {// 1. 字符串转为 ByteBufferByteBuffer buffer1 = ByteBuffer.allocate(16);buffer1.put("hello".getBytes());debugAll(buffer1);// 2. CharsetByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello");debugAll(buffer2);// 3. wrapByteBuffer buffer3 = ByteBuffer.wrap("hello".getBytes());debugAll(buffer3);// 4. 转为字符串String str1 = StandardCharsets.UTF_8.decode(buffer2).toString();System.out.println(str1);buffer1.flip();String str2 = StandardCharsets.UTF_8.decode(buffer1).toString();System.out.println(str2);}

2.9 分散读集中写

思想:减少数据在 ByteBuffer间的copy次数,间接提高性能;

3.0 黏包、半包

黏包产生的原因:因为客户端要提高效率,所以会把数据合并发送

半包产生的原因:因为服务器端ByteBuffer的大小限制

解决方法案例:

public static void main(String[] args) {/*网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为Hello,world\nI'm zhangsan\nHow are you?\n变成了下面的两个 byteBuffer (黏包,半包)Hello,world\nI'm zhangsan\nHow are you?\n现在要求你编写程序,将错乱的数据恢复成原始的按 \n 分隔的数据*/// 模拟产生黏包半包,分两次接收数据ByteBuffer source = ByteBuffer.allocate(32);// 第一次接收并处理source.put("Hello,world\nI'm zhangsan\nHo".getBytes());split(source);// 第二次接收并处理source.put("w are you?\n".getBytes());split(source);}/**** @param source 接收的ByteBuffer**/private static void split(ByteBuffer source) {// 转读模式source.flip();// 遍历接收到数据的长度for (int i = 0; i < source.limit(); i++) {// 找到一条完整消息 source.get(index),source的position不发生变化if (source.get(i) == '\n') {int length = i + 1 - source.position();// 把这条完整消息存入新的 ByteBufferByteBuffer target = ByteBuffer.allocate(length);// 从 source 读,向 target 写for (int j = 0; j < length; j++) {target.put(source.get());}debugAll(target);}}source.compact();}

http://www.dtcms.com/a/339590.html

相关文章:

  • 数字政务安全实战:等保2.0下OA系统的身份认证与数据防护
  • 微软AD国产化替换倒计时——不是选择题,而是生存题
  • 三次握手四次挥手
  • 决策树算法详解
  • Orange的运维学习日记--47.Ansible进阶之异步处理
  • ESP32应用——HTTP client(ESP-IDF框架)
  • STM32之MCU和GPIO
  • AT_abc397_f [ABC397F] Variety Split Hard
  • 高速传输的关键:8B/10B编码学习记录
  • 应用控制技术与内容审计技术
  • 系统架构设计师-操作系统-避免死锁最小资源数原理模拟题
  • 寻找旋转排序数组中的最小值
  • 黄金本周想法
  • 给类或实例打上标识即类的元数据标签方便程序在运行时对其进行分类、识别、筛选
  • 32K上下文开源语音理解、40分钟深度交互——Voxtral-Small-24B-2507本地部署教程
  • GCC编译输出中text,data,bss和dec的含义
  • 构建自主企业:AgenticOps 的技术蓝图
  • 基于 STM32 单片机的远程老人监测系统设计
  • 科大讯飞语音服务之:BNF文件
  • 基于用户画像的个性化匹配模型
  • 【Proteus仿真】【51单片机】基于51单片机自动售货机12864屏幕
  • 这是关于Oracle碎片的文章
  • 空间智能赋能低空经济建设
  • 录音转文字:怎么快速精转提取重点?
  • 【弦乐教程】四 / 五部和声铺底写作指南 —— 从声部配置到禁忌规避
  • 8.19笔记
  • 训练数据转 LMDB 格式:必要性解析与核心优势全指南
  • 假设检验的原理
  • PDR与RSSI融合定位入门:从原理到实践
  • 如何用Prometheus和FastAPI打造任务监控的“火眼金睛”?