坑 | NIO - [AsynchronousFileChannel + CompletionHandler]

§1 场景需求与难点

由来
填自己技术漏洞时扫到 AsynchronousFileChannel
涉及到一个 AsynchronousFileChannel + CompletionHandler 读文件的 case
但网上类似 case 大多从简实现,或干脆没有代码……
自己写一写吧,发现踩了坑,好大一溜儿坑…………………………

这个例子 不完善,切勿直接用于正式研发生产环境

需求
通过 AsynchronousFileChannel + CompletionHandler 读一个比较长的文本文件,正确打印文件内容
这个需求不应该对应具体的实际需求,在最后的 总结 中说

关键点说明

  • 下面所有问题都是基于 尽量贴近使用场景 搞出来的
  • 默认文件不能一轮 read 就读尽
    • 因为此示例使用的是 AsynchronousFileChannel,异步通道
    • 因此默认文件很大(否则用不到异步通道)
  • 因此带来了文件拼接的问题,这要求将读取到的分片按序、异步的存储起来
    • 不能直接输出,因为是文本文件,数据不完整时不保证可以正常对字节数组进行编码获得正确内容
      但是实际环境下的需求,可能就是配合异步写直接写出去了,可能反而没这么复杂
  • 核心问题有两个
    • AsynchronousFileChannel 是异步的
      • read 和 handler 是不同的线程,handler 实际上是个回调
      • 不同 read 分片对应的 handler 都是不同的线程
    • read 和 handler 的逻辑是属于不同对象的
      • 这导致 handler 在处理读取到的数据分片时,很难使用调用 read 的对象的数据
      • 而非异步场景下,这些数据里包括了分片读取文件时控制进度与结果的变量
  • 由此衍生出来的问题更恶心
    • 存储本身挺简单的,不特殊场景直接 ArrayList,每读一次就 add 一次即可,但 AsynchronousFileChannel 是异步的
      异步导致 read 触发 handler 的顺序不确定,这会导致 add 进去的字节数组分片是乱序的
      实际上通常最后一轮一定会乱序,因为通常最后一轮读取的数据少(前面都是整页整页的读,最后一轮不确定剩几个字节)
    • 同时 buffer 不能随意复用了,最好一直分配新的或使用池
      还是因为刚刚的原因,复用极可能导致不同轮次的 read 互相覆盖复用的 buffer 里的数据
    • 上述问题要求分片必须是可定位的,即 position 必须被记录,并需要在 handler 中正确使用
  • 为了实现功能做出如下设计
    • 分片 read 的过程控制和结果汇总,都交由 handler 处理
      • handler 处理时,需要相关控制流程与结果的变量,因此只能提供这些变量
      • 但因为接口是固定的,不能通过 handler 方法的入参提供,因此只能通过成员提供
      • 通过成员提供,意味着 handler 持有了控制流程与结果的数据
      • 谁持有数据,就由谁提供相关的处理方法,谁就具备相关的功能
    • 每次读取使用新的 buffer,在获取 buffer 之初明确其 position
    • 使用二维数组保证 read 分片有序
  • 还有一些其他问题
    比如 buffer 的 hashcode 是可变的,因此需要引入 System.identityHashCode
    System.identityHashCode 又存在碰撞的可能(虽然很小),因此需要及时清除处理完的 buffer 的 hash
    清除处理完的 buffer 的 hash 是在 handler 中进行的,说白了它是多线程的,于是引入 ConcurrentHashMap

§2 使用的文件

从以前的帖子里随手截取的一段

INDEX
§1 概述
§2 重要成员
§2.1 property
§2.2 method
§3 示例
§1 概述
缓冲区是一块内存空间,可以将它直观的理解为一个数组
这块内存空间直接和 Channel 相连接
可以向这块内存空间中写或读取数据
常用实现

ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer
常规使用流程

向 Buffer 中写入数据
调用 flip() 方法,将 Buffer 从写方向切换为读方向
从 Buffer 中读取数据
调用 clear() 或 compact() 方法清空 Buffer
clear() 清空整个 Buffer
compact() 清空 Buffer 中读过的部分
常规使用流程示例

012345678901234567890123456789

§3 实现

CompletionHandler 实现
用二维数组 汇总 文本文件的用于读场景的 CompletionHandler

public class ByteMatricTextReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
    private long capacity; // file total size
    private long window; // buffer size

    // byte[] array for each read
    private byte[][] metrix = null;
    // position for file
    private long position = 0;
    private Charset charset = StandardCharsets.UTF_8;
    // buffer 的一致性 hash 与 position 的映射,防字节数组乱序
    private Map<Integer,Long> bufferPositions = new ConcurrentHashMap<>();

    // 整个文件是否还没读完
    public boolean isNotDone(){
        return position < capacity;
    }
    // 获取整体读取结果
    public String text(){
        return text(this.charset);
    }
    // 获取整体读取结果
    public String text(Charset charset){
        return new String(ArrayUtil.addAll(metrix), charset);
    }
    @Override
    public void completed(Integer result, ByteBuffer buffer) {
        System.out.println(Thread.currentThread().getName());
        buffer.flip();
        /* *******************************
         * 每一个 buffer 读取后的动作
         * 及时移除:
         * 帮助 GC
         * 防 System.identityHashCode 碰撞
         ******************************* */
        metrix[(int) (bufferPositions.remove(System.identityHashCode(buffer))/window)] = ArrayUtil.sub(buffer.array(),0,buffer.limit());
        buffer.clear();
    }

    // 任意一次读失败,都肯能导致不能拼接出完整文件,直接异常
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        throw new IllegalStateException("error read: cause can not to assemble the complete target file");
    }

    /* *******************************
     * 以下是工具
     ******************************* */
    // 提交 position,注释的部分可以适用于调用 read 的部分也是多线程的场景
    public ByteBuffer commit(){
//        long pos = position.getAndAdd(window);
//        ByteBuffer buf = ByteBuffer.allocate((int) window);
//        bufferPositions.put(System.identityHashCode(buf),pos);
        ByteBuffer buf = ByteBuffer.allocate((int) window);
        bufferPositions.put(System.identityHashCode(buf),position);
        position+=window;
        return buf;
    }

    // 获取 buffer 对应的文件的 position
    public long pos(ByteBuffer buf){
        return bufferPositions.get(System.identityHashCode(buf));
    }
    // 创建,保持 NIO 风格
    public static ByteMatricTextReadCompletionHandler open(long capacity,long window){
        return new ByteMatricTextReadCompletionHandler(capacity,window);
    }
    /* *******************************
     * 以下是构造
     ******************************* */
    public ByteMatricTextReadCompletionHandler(long capacity,long window) {
        this.capacity = capacity;
        this.window = window;
        this.metrix = new byte[(int) ((capacity + window-1)/window)][];
    }
}

真正读取的方法
这里到是相对简洁的多

public void readByHandler(String path, String file, int size) {
    try (
            AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get(path+file), StandardOpenOption.READ)
    ){
        ByteMatricTextReadCompletionHandler handler = ByteMatricTextReadCompletionHandler.open(channel.size(),100);
        while(handler.isNotDone()){
            System.out.println(Thread.currentThread().getName());
            ByteBuffer buf = handler.commit();
            channel.read(buf, handler.pos(buf),buf,handler);
        }
        // 不是必须的,但没有这句,测试用例等不到文件读完
        TimeUnit.SECONDS.sleep(1);
        System.out.println(handler.text());
    } catch (Exception e){ /* 异常处理 */ }
}

§4 效果 & 坑

异步效果
可以看到一堆 main 是 readByHandler 中打印的
其他的乱七八糟的是 ByteMatricTextReadCompletionHandler 的回调中打印的
侧面说明 AsynchronousFileChannel 的异步效果:read 和 handler 异步,handler 与 handler 之间异步

在这里插入图片描述

读取效果
在这里插入图片描述

总结

一个正常的 JDK 按理说不应该提供实际使用中这么不友好的 API
因此,一个较大概率的可能是,AsynchronousFileChannel + CompletionHandler 并不是用来处理类似场景的
可能的实际场景:

  • 分片传输,甚至可以用传输的长度代替 position
  • 分片统计,不需要再在意上面因为顺序带来的困难了
  • 分片操作逻辑文件,操作系统里的一个文件,但其实逻辑上是很多文件放在一个 data 包里
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值