Netty从0到1系列之Channel

『Java分布式系统开发:从理论到实践』征文活动 10w+人浏览 202人参与

二、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. 获取源文件和目标文件的 FileChannel
             FileChannel 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>(){

    /**
     * 进入遍历的目录之前调用
     */
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        System.out.println(dir);
        return super.preVisitDirectory(dir, attrs);
    }

    /**
     * 访问文件成功
     */
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        System.out.println("\t\t" + file.getFileName());
        return super.visitFile(file, attrs);
    }

    /**
     * 访问文件失败
     */
    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
        return super.visitFileFailed(file, exc);
    }


    /**
     * 退出目录之时的回调函数
     * @return
     * @throws IOException
     */
    @Override
    public 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
         */
        @Override
        public 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
         */
        @Override
        public 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 等高性能系统的基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值