当前位置: 首页 > 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.lryc.cn/news/626007.html

相关文章:

  • 【3-3】流量控制与差错控制
  • Linux资源管理
  • JUC之CompletableFuture【上】
  • Orbbec---setBoolProperty 快捷配置设备行为
  • 设备树下的LED驱动实验
  • 从数据表到退磁:Ansys Maxwell中N48磁体磁化指南
  • 谷歌为什么要将Android的页面大小(Page Size)从传统的4KB升级至16KB
  • Go 进阶学习路线
  • 测试 Next.js 应用:工具与策略
  • 仲裁器设计(三)-- Weighted Round Robin 权重轮询调度
  • ASP4644稳压器的特性分析与系统测试方法研究
  • GPT-4.1旗舰模型:复杂任务的最佳选择及API集成实践
  • 【RustFS干货】RustFS的智能路由算法与其他分布式存储系统(如Ceph)的路由方案相比有哪些独特优势?
  • 2025杭电多校第九场 乘法逆元、阿斯蒂芬、计算几何 个人题解
  • 宿主获取插件View流程原理 - fetchViewByLayoutName
  • LWIP协议栈实现ARP协议
  • Python脚本每天爬取微博热搜-终版
  • Spring Cloud 微服务架构:Eureka 与 ZooKeeper 服务发现原理与实战指南 NO.1
  • Stream API-怎么理解流
  • Day13_【DataFrame数据组合merge连接】【案例】
  • Redis(11)如何通过命令行操作Redis?
  • 反向代理实现服务器联网
  • 人工神经网络MATLAB工具箱指南
  • Selenium自动化测试入门:cookie处理
  • electron进程间通信- 渲染进程与主进程双向通信
  • 如何用给各种IDE配置R语言环境
  • UGUI源码剖析(10):总结——基于源码分析的UGUI设计原则与性能优化策略
  • Ubuntu 和麒麟系统创建新用户 webapp、配置密码、赋予 sudo 权限并禁用 root 的 SSH 登录的详细
  • Python os 模块与路径操作:从基础到实战应用
  • 《AI 与人类创造力:是替代者还是 “超级协作者”?》​