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

Netty从0到1系列之Channel

文章目录

  • 二、Channel
    • 2.1 为什么需要 Channel?传统 IO 的局限
    • 2.2 Channel核心特性
    • 2.3 Channel的主要实现类
    • 2.4 核心原理:Channel 与操作系统的关系
    • 2.5 FileChannel代码示例
      • 2.5.1 读取数据
      • 2.5.2 写入数据
      • 2.5.3 使用 FileChannel 复制文件
      • 2.5.4 Transfer
    • 2.6 ⚙️Path和Paths
    • 2.7 🔧File类Files
      • 2.7.1 常用方法
      • 2.7.2 基本使用代码
      • 2.7.3 遍历文件目录
      • 2.7.4 删除目录
      • 2.7.5 拷贝文件
    • 2.8 Channel与Stream对比
    • 2.9 Channel优缺点总结
      • 2.9.1 ✅ 优点
      • 2.9.2 ❌ 缺点
    • 2.10 最佳实践与经验总结

二、Channel

Channel 是 Java NIO 的核心组件之一,它是数据传输的“管道”,用于在 Java 应用程序与 I/O 设备(如文件、网络套接字)之间高效地传输数据。与传统的 InputStreamOutputStream 不同,Channel 是双向的、非阻塞的,并且总是与 Buffer 配合使用

在这里插入图片描述

2.1 为什么需要 Channel?传统 IO 的局限

❌ 传统 IO(Stream)的问题

特性问题
单向性InputStream 只能读,OutputStream 只能写
阻塞式读写操作会阻塞线程,直到完成
逐字节处理效率低,不适合大数据量传输
无缓冲集成无法直接与 Buffer 协作

✅ Channel 的优势

特性优势
双向性同一个 Channel 可读可写(如 SocketChannel
支持非阻塞模式可配合 Selector 实现 I/O 多路复用
批量传输Buffer 配合,一次读写多个字节
支持文件传输优化FileChannel.transferTo() 实现零拷贝

🎯 目标:构建高性能、高并发的 I/O 系统。

2.2 Channel核心特性

✅ 1. 双向通信

  • 与 Stream 不同,大多数 Channel(如 SocketChannel)支持 读和写
  • 可以从中读取数据,也可以向其中写入数据(但需要结合缓冲区 Buffer)。一个 FileChannel 可以同时读写,而无需像流那样创建 FileInputStreamFileOutputStream 两个对象。
channel.read(buffer); // 读数据
channel.write(buffer); // 写数据

✅ 2. 非阻塞模式

  • 可通过 configureBlocking(false) 设置为非阻塞。
  • 在非阻塞模式下,read()write() 立即返回,不会阻塞线程。
  • 在此模式下,调用读写方法会立刻返回,线程不会被挂起。这使得网络编程可以实现单线程管理多通道,这是实现高并发的关键。

✅ 3. 与 Buffer 协作

  • 所有数据必须通过 Buffer 传输。
  • 不支持直接读写字节数组。
  • 数据总是从通道读到缓冲区,或从缓冲区写入通道。这种基于块(Block)的操作效率远高于流的逐字节操作。

核心关系图:数据通过 Channel 与 Buffer 进行交互

写入
put
写入通道
读取到
get
数据源
文件/网络/等
Channel
通道
应用程序
Buffer
缓冲区

2.3 Channel的主要实现类

Channel 类用途是否可注册到 Selector
FileChannel文件读写❌(不支持非阻塞)
SocketChannelTCP 客户端
ServerSocketChannelTCP 服务端
DatagramChannelUDP 通信
Pipe.SinkChannel / Pipe.SourceChannel线程间通信

2.4 核心原理:Channel 与操作系统的关系

🖥️ 底层实现原理

Java 的 Channel 是对操作系统 文件描述符(File Descriptor) 的封装。每个 Channel 对应一个内核级的 fd。

Java Application
SocketChannel
JVM NIO 层
操作系统内核
网卡/磁盘

当调用 channel.read(buffer) 时:

  • JVM 调用系统调用 read(fd, buf, len)

  • 内核从设备读取数据到内核缓冲区

  • 数据从内核缓冲区拷贝到用户空间的 ByteBuffer

  • 返回读取字节数

2.5 FileChannel代码示例

2.5.1 读取数据

直接调用Channel当中提供的read()方法.其读取的实现逻辑由其子类实现: FileChannelImpl

public int read(ByteBuffer dst) throws IOException {ensureOpen(); // 确保文件已经打开if (!readable) // 是否可读throw new NonReadableChannelException();synchronized (positionLock) { // 阻塞式读取操作if (direct)Util.checkChannelPositionAligned(position(), alignment);int n = 0;int ti = -1;try {beginBlocking();ti = threads.add();if (!isOpen())return 0;do {n = IOUtil.read(fd, dst, -1, direct, alignment, nd);} while ((n == IOStatus.INTERRUPTED) && isOpen());return IOStatus.normalize(n);} finally {threads.remove(ti);endBlocking(n > 0);assert IOStatus.check(n);}}
}

2.5.2 写入数据

推荐使用如下姿势进行写入操作.

ByteBuffer buffer =  ;
buffer.put(data); // 存入数据
buffer.flip(); // 切换读取模式while(buffer.hasRemaining()){// 如果buffer当中还有数据,此时要将数据写入.因为有些时候,并不能将数据一次性的写入到buffer当中, 所以得调用: write方法buffer.write(buffer)
}

不管是读取还是写入,则必须要进行关闭操作调用相关的close方法即可;

2.5.3 使用 FileChannel 复制文件

package cn.tcmeta.nio;import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class FileChannelExample {public static void main(String[] args) {// 定义源文件和目标文件路径String srcFile = "source.txt";String destFile = "dest.txt";// 使用 try-with-resources 确保资源正确关闭try (RandomAccessFile sourceFile = new RandomAccessFile(srcFile, "r");RandomAccessFile destFile2 = new RandomAccessFile(destFile, "rw");// 1. 获取源文件和目标文件的 FileChannelFileChannel sourceChannel = sourceFile.getChannel();FileChannel destChannel = destFile2.getChannel()) {// 2. 分配一个字节缓冲区 (这里大小是 1024 字节)ByteBuffer buffer = ByteBuffer.allocate(1024);// 3. 循环读取和写入while (sourceChannel.read(buffer) != -1) {// 切换缓冲区为读模式(limit=position, position=0)buffer.flip();// 将缓冲区中的数据写入目标通道destChannel.write(buffer);// 清空缓冲区,为下一次读取做准备(position=0, limit=capacity)// 注意:compact() 方法也可以,它会保留未读数据buffer.clear();}// 4. 强制将数据刷到磁盘destChannel.force(true);System.out.println("文件复制完成!");} catch (IOException e) {e.printStackTrace();}}
}

日志输出

在这里插入图片描述
在这里插入图片描述

2.5.4 Transfer

public abstract long transferTo(long position, long count,WritableByteChannel target)throws IOException;
public abstract long transferFrom(ReadableByteChannel src,long position, long count)throws IOException;

Transfer实现文件复制

/*** 使用Channel进行文件复制操作* TransferFrom, 拷贝* TransferTo* 效率高一些.比传统的文件输入输出流高.利用了操作系统的零拷贝进行了优化.* 此种方式,最大只能传输2G的数据.*/
@Test
public void copyFileTest() {try (FileChannel fromChannel = new FileInputStream(WORLD_PATH_02).getChannel();FileChannel toChannel = new FileOutputStream(WORLD_PATH_03).getChannel()) {fromChannel.transferTo(0, fromChannel.size(), toChannel);} catch (Exception e) {e.printStackTrace();}
}

Transfer实现文件分段传输

/*** 优化一下,可以传输更多的数据,采用的是分批传输的思想.*/
@Test
public void copyFileTestMore() {try (FileChannel fromChannel = new FileInputStream(WORLD_PATH_02).getChannel();FileChannel toChannel = new FileOutputStream(WORLD_PATH_03).getChannel()) {long size = fromChannel.size(); // 获取待传输文件的大小for (long left = size; left > 0; ){// transferTo, 返回值表示,已经传输的数据的字节数.left -= fromChannel.transferTo(size - left, left, toChannel);}fromChannel.transferTo(0, fromChannel.size(), toChannel);} catch (Exception e) {e.printStackTrace();}
}

2.6 ⚙️Path和Paths

可用于在文件系统中查找文件的对象。它通常表示与系统相关的文件路径。

A Path 表示分层路径,该路径由一系列目录和文件名元素组成,这些元素由特殊分隔符或分隔符分隔。还可能存在标识文件系统层次

构的根组件。距离目录层次结构根目录最远的 name 元素是文件或目录的名称。其他 name 元素是目录名称。A Path 可以表示根

根和名称序列,也可以表示一个或多个名称元素。如果 A Path 仅由一个空的 name 元素组成,则认为该路径为空路径。使用空路

径访问文件等同于访问文件系统的默认目录。

这里着重聊一下Paths工具类的使用, 以下是常用的方法总结

方法名描述
getFileName()获取文件名
getFileName().toString()获取文件名,作为字符串
getRoot()获取根路径
getParent()获取父路径
getFileName(int index)获取指定索引的文件名
getNameCount()获取文件名的数量
toAbsolutePath()获取绝对路径
toUri()获取 URI
normalize()规范化路径
resolve(Path other)解析另一个路径
relativize(Path other)将当前路径相对化另一个路径
startsWith(Path other)判断当前路径是否以另一个路径开头
endsWith(Path other)判断当前路径是否以另一个路径结尾
isAbsolute()判断路径是否是绝对路径
isDirectory()判断路径是否是目录
isRegularFile()判断路径是否是普通文件
exists()判断路径是否存在
isHidden()判断路径是否是隐藏文件
toFile()获取文件对象
toRealPath()获取真实路径
createFile()创建文件
createDirectory()创建目录
delete()删除文件或目录
copyTo(Path target)复制文件或目录
move(Path target)移动文件或目录
append(String text)追加文本到文件
write(String text)写入文本到文件
readAllLines()读取文件中的所有行
readAllBytes()读取文件中的所有字节

示例代码

public class PathExample {public static void main(String[] args) {// 创建 Path 对象Path path = Paths.get("/home/user/file.txt");// 获取文件名String fileName = path.getFileName().toString();System.out.println("文件名:" + fileName);// 获取扩展名String extension = path.getFileName().toString().substring(fileName.lastIndexOf(".") + 1);System.out.println("扩展名:" + extension);// 判断文件是否存在boolean exists = Files.exists(path);System.out.println("文件是否存在:" + exists);// 创建文件try {Files.createFile(path);} catch (IOException e) {e.printStackTrace();}// 删除文件try {Files.delete(path);} catch (IOException e) {e.printStackTrace();}}
}

2.7 🔧File类Files

2.7.1 常用方法

Files常用方法总结

方法名描述
createFile(Path path)创建文件
createDirectory(Path path)创建目录
delete(Path path)删除文件或目录
copy(Path source, Path target)复制文件或目录
move(Path source, Path target)移动文件或目录
readAllLines(Path path)读取文件中的所有行
readAllBytes(Path path)读取文件中的所有字节
write(Path path, String content)写入字符串到文件
write(Path path, byte[] bytes)写入字节数组到文件
walk(Path start, int maxDepth)遍历文件树

2.7.2 基本使用代码

public class FilesExample {public static void main(String[] args) {// 创建文件try {Files.createFile(Paths.get("/home/user/file.txt"));} catch (IOException e) {e.printStackTrace();}// 创建目录try {Files.createDirectory(Paths.get("/home/user/dir"));} catch (IOException e) {e.printStackTrace();}// 删除文件try {Files.delete(Paths.get("/home/user/file.txt"));} catch (IOException e) {e.printStackTrace();}// 删除目录try {Files.delete(Paths.get("/home/user/dir"));} catch (IOException e) {e.printStackTrace();}// 复制文件try {Files.copy(Paths.get("/home/user/source.txt"), Paths.get("/home/user/target.txt"));} catch (IOException e) {e.printStackTrace();}// 移动文件try {Files.move(Paths.get("/home/user/source.txt"), Paths.get("/home/user/target.txt"));} catch (IOException e) {e.printStackTrace();}// 读取文件中的所有行try {List<String> lines = Files.readAllLines(Paths.get("/home/user/file.txt"));for (String line : lines) {System.out.println(line);}} catch (IOException e) {e.printStackTrace();}// 写入字符串到文件try {Files.write(Paths.get("/home/user/file.txt"), "Hello, world!".getBytes());} catch (IOException e) {e.printStackTrace();}// 写入字节数组到文件try {Files.write(Paths.get("/home/user/file.txt"), "Hello, world!".getBytes(), StandardOpenOption.APPEND);} catch (IOException e) {e.printStackTrace();}// 遍历文件树try {Files.walk(Paths.get("/home/user"), 1).forEach(System.out::println);} catch (IOException e) {e.printStackTrace();}}
}

2.7.3 遍历文件目录

public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)throws IOException
{return walkFileTree(start,EnumSet.noneOf(FileVisitOption.class),Integer.MAX_VALUE,visitor);
}

FileVisitor接口:

public interface FileVisitor<T> {FileVisitResult visitFile(T file, BasicFileAttributes attrs)throws IOException;FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)throws IOException;FileVisitResult visitFileFailed(T file, IOException exc)throws IOException;FileVisitResult postVisitDirectory(T dir, IOException exc)throws IOException;
}

在这里插入图片描述

String rootPath = "D:\\tom";
Files.walkFileTree(Paths.get(rootPath), new SimpleFileVisitor<Path>(){/*** 进入遍历的目录之前调用*/@Overridepublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {System.out.println(dir);return super.preVisitDirectory(dir, attrs);}/*** 访问文件成功*/@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {System.out.println("\t\t" + file.getFileName());return super.visitFile(file, attrs);}/*** 访问文件失败*/@Overridepublic FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {return super.visitFileFailed(file, exc);}/*** 退出目录之时的回调函数* @return* @throws IOException*/@Overridepublic FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {return super.postVisitDirectory(dir, exc);}
});
}

2.7.4 删除目录

目录如果包含了文件,则不能直接进行删除的.必须先删除掉目录的文件,再将目清除掉.以往我们使用递归进行删除,此时我们可以使用工具类Files当中提供的进行删除操作.

/*** 批量删除文件和目录.利用Files工具当中提供的方法.* 注册删除的时候,不会走回收站.所以慎重一些.删除的文件无法恢复.*/
@Test
public void batchDeleteFolderTest() throws IOException {String root = "C:\\Users\\ldcig\\Desktop\\mybatis-flex-test-master";Files.walkFileTree(Paths.get(root), new SimpleFileVisitor<Path>(){/*** 直接访问文件操作* @throws IOException*/@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {Files.delete(file);System.out.printf("删除 文件 [%s] 成功 \n", file.getFileName());return super.visitFile(file, attrs);}/*** 访问文件之后的操作.* @throws IOException*/@Overridepublic FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {System.out.printf("----------- 删除 目录 [%s] 成功 \n", dir.getFileName());Files.delete(dir);return super.postVisitDirectory(dir, exc);}});
}

2.7.5 拷贝文件

利用Files类提供的api可以快速的实现文件拷贝操作.

// 该方法的返回值是: Stream流.可以方便快速的操作了.利用Stream提供的相关api.
public static Stream<Path> walk(Path start, FileVisitOption... options) throws IOException {return walk(start, Integer.MAX_VALUE, options);
}

拷贝文件示例代码

/*** 拷贝文件* 简单方便.*/
@Test
public void copyTest() throws IOException {String srcPath = "你的路径\\mybatis-flex-test-master";String targetPath = "你的路径\\mybatis-flex-test-master-copy";Files.walk(Paths.get(srcPath)).forEach(p -> {// 获取目标目录String targetName = p.toString().replace(srcPath, targetPath);Path target = Paths.get(targetName); // 拷贝的目标目录.if(Files.isDirectory(p)){// 如果是目录,则直接创建目录, 如果目录不存在,则直接创建目录即可;try {// 创建目录Files.createDirectory(target);} catch (IOException e) {throw new RuntimeException(e);}}else if(Files.isRegularFile(p)){System.out.printf(" ----------- 当前文件是: %s \n", p);try {Files.copy(p, target);} catch (IOException e) {throw new RuntimeException(e);}}});
}

2.8 Channel与Stream对比

在这里插入图片描述

2.9 Channel优缺点总结

2.9.1 ✅ 优点

优点说明
高性能批量传输,减少系统调用
支持非阻塞可用于高并发网络编程
双向通信同一个连接可读可写
零拷贝支持transferTo() 提升文件传输性能
内存映射大文件处理更高效

2.9.2 ❌ 缺点

缺点说明
编程复杂需手动管理 Buffer 状态
不能直接操作字节数组必须通过 Buffer 中转
FileChannel 不支持非阻塞无法注册到 Selector
资源管理严格必须显式关闭 Channel

2.10 最佳实践与经验总结

实践说明
使用 try-with-resources自动关闭 Channel
合理设置 Buffer 大小通常 4KB ~ 64KB
避免频繁小数据写入合并写操作
使用 force() 确保落盘重要数据调用 fileChannel.force(true)
优先使用 transferTo()大文件复制性能最佳
结合 Selector 使用网络编程中实现单线程多连接

⚠️ 常见错误

// ❌ 错误:忘记 flip()
buffer.put(data);
channel.write(buffer); // 写的是 position 到 limit,可能是空的// ✅ 正确:
buffer.put(data);
buffer.flip(); // 切换为读模式
channel.write(buffer);

Channel 的核心价值

维度说明
核心作用数据传输的“高速公路”
关键优势双向、非阻塞、高效、可扩展
适用场景文件处理、网络通信、高性能服务
底层本质操作系统文件描述符的封装
性能优化零拷贝、内存映射、批量传输

💡 一句话总结

  • Channel 是 Java NIO 的“数据管道”,它通过 与 Buffer 协作、支持非阻塞、实现零拷贝,为构建高性能 I/O 系统提供了底层支撑。掌握 Channel 是深入理解 Netty、Redis、Kafka 等高性能系统的基础。
http://www.xdnf.cn/news/1448029.html

相关文章:

  • 【 线段树】P12347 [蓝桥杯 2025 省 A 第二场] 栈与乘积|普及+
  • 基于 HTML、CSS 和 JavaScript 的智能图像灰度直方图分析系统
  • HTML全屏功能实现汇总
  • npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR!
  • 求单源最短路(Dijkstra 算法-迪杰斯特拉算法,SPFA)
  • 【Unity基础】两个关于UGUI中Text对非英文字体支持的问题
  • SpringAI应用开发面试全流程:技术原理、架构优化与企业场景解析
  • 复写零(双指针)
  • JavaScript学习最后一章节(小练习)
  • 如何解决虚拟机网络连接问题:配置固定 IP 篇
  • Spring Authorization Server 1.5.2 使用YML配置的方式,最常用法总结
  • 【算法--链表】141.环形链表(通俗讲解链表中是否有环)
  • 分布式AI算力系统番外篇-----超体的现世《星核》
  • 强化学习中的模仿学习是什么?
  • 相关性分析与常用相关系数
  • react的 hooks 是如何存储的
  • HTML第七课:发展史
  • Streamlit 数据看板模板:非前端选手快速搭建 Python 数据可视化交互看板的实用工具
  • 如何画时序图、流程图
  • android集成unity后动态导入 assetsBundle
  • Android创建demo脚本
  • CSS中使用 HSL(Hue, Saturation, Lightness) 动态生成色值
  • Linux 对目录授予用户读写权限的方法
  • 信创MySQL到达梦数据库的SQL语法转换技术解析
  • AWK命令完全指南:从理论到实战的文本处理利器
  • Spring Boot + Nacos 配置中心示例工程
  • tcpdump用法
  • Best Video网页官网入口 – 免费在线网页视频解析下载器
  • 认识HTML
  • 用资产驱动方法构建汽车网络安全档案