使用transferTo方法限制文件传输大小的原因分析

Mars 2019年12月06日 70次浏览

缘由

附一段transforTo方法的doc:

This method is potentially much more efficient than a simple loop
that reads from this channel and writes to the target channel.  Many operating systems can transfer bytes directly from the 
filesystem cache  to the target channel without actually copying them.

在oio和nio进行文件复制(zero-copy,直接从文件系统传输字节)效率对比的时候发现部分文件拷贝不全(以下只会展示client端代码);

  • oio代码如下:
 public static void main(String[] args) throws Exception {
        Socket socket = new Socket("localhost", 8899);
        //文件4g+
        String path = "E:\\CiKeXinTiao8AoDeSai\\DataPC_patch_01.forge";
        String p = "E:\\ggg6.7z";
        FileInputStream inputStream = new FileInputStream(p);

        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());

        byte[] bytes = new byte[4096];
        long readCount;
        long total = 0;

        long startTime = System.currentTimeMillis();

        while ((readCount = inputStream.read(bytes)) >= 0) {
            System.out.println("写"+readCount+"个字节.....");
            total += readCount;
            dataOutputStream.write(bytes);
        }
        //发送总字节数:1727150186,耗时:9386 zero copy 1563
        System.out.println("发送总字节数:" + total + "," + "耗时:" + (System.currentTimeMillis() - startTime));
        socket.close();
    }
  • nio代码如下:
 SocketChannel socketChannel = SocketChannel.open();
        //文件4g+
        //String path = "E:\\CiKeXinTiao8AoDeSai\\DataPC_patch_01.forge";
        //270m+
        String path = "E:\\CiKeXinTiao8AoDeSai\\ACOdyssey.exe";
        String p = "E:\\ggg6.7z";
        socketChannel.connect(new InetSocketAddress("localhost", 8900));
        socketChannel.configureBlocking(true);

        FileChannel fileChannel = new FileInputStream(p).getChannel();
        long currentCount = fileChannel.transferTo(0, size, socketChannel);

直接使用FileChannel的transferTo向sockerChannel复制数据,这时会发现,发送字节数并不等于文件总字节数。

解决

  • 首先解决文件发送不全的问题,采用while读
long startTime = System.currentTimeMillis();
        long size = fileChannel.size();
        long position = 0;
        long total = 0;
        while (position < size) {
            long currentNum = fileChannel.transferTo(position, fileChannel.size(), socketChannel);
            System.out.println("复制字节数:"+currentNum);
            if (currentNum <= 0) {
                break;
            }
            total += currentNum;
            position += currentNum;
        }
        System.out.println("发送总字节数:" + total + "  耗时:" + (System.currentTimeMillis() - startTime));

这时是可以全部复制完成的。

寻找原因

  • 第一时间判断是不是channel类型的原因,因此将sokcetChannel改为Filechannel测试
        long currentCount = fileChannel.transferTo(0, size, socketChannel);
	改为
        long currentCount = fileChannel.transferTo(0, size, new FileOutputStream(p).getChannel());

此时发现,小于2g是可以一次复制完的。

继续深究(fileChannel->socketChannel)

  • 时隔几个月,现在在自己需要用到,于是想起之前的问题,抱着好奇心(总不能一直死记着吧0.0),点开transferTo寻找原因
  1. 进入FileChannel的transferTo方法,来到FileChannel
    image
    2.在doc中有如下描述,大致是说传输方式根据通道类型和文件大小决定(渣英语翻译),记住这句话之后进入实现类FileChannelImpl
all of the requested bytes; whether or not it does so depends upon the
 natures and states of the channels.  Fewer than the requested number of
bytes are transferred if this channel's file contains fewer than
 {@code count} bytes starting at the given {@code position}, or if the
 target channel is non-blocking and it has fewer than {@code count}
bytes free in its output buffer.

3.这么多判断,鬼知道到哪个if里面去,
image

  • 使用debug调试
  1. 实现类入口加入debug断点,使用debug进入transferTo方法,targetChannel为SocketChannel

image.png
主要是做一些基础判断,通道是否打开,是否可写,是不是fileChannel实例,position校验,以及确定当前可操作的字节数

//count是我们需要的字节数,而Integer.MAX_VALUE是最大字节数,2g,如果超过
//需要while像之前那个循环操作,但是我们为啥之前限制是8m呢?
 int icount = (int)Math.min(count, Integer.MAX_VALUE);
  • 继续step over往下寻找
    image.png
    注意标注的几个地方,可以看见我们传入的count=334258906,大概是318m,是符合的,再下面进行上一步说的长度判断,是小于int最大值2g的,因此没做处理,icount还是318m。再往下这两个if很重要,有点基础的应该知道是zero-copy和内存映射。
  1. 先看第一个if
 // Attempt a direct transfer, if the kernel supports it
//如果内核支持,将直接进行传输,即zero-copy
        if ((n = transferToDirectly(position, icount, target)) >= 0)
            return n;

我们debug是进行到了第二个id,因此第一个if是不满足的,接着进入第一个if方法,看看为什么不满足,首先记住transferToDirectly(position, icount, target)返回值(-6)
image.png
2. 进入 transferToDirectly(position, icount, target)方法,也在FileChannelImpl,可以看见方法返回了很多常量
image.png
点过去看看这些常量
image.png```java
我们返回的-6正是 UNSUPPORTED_CASE
3. 回到方法,这几个这是返回的地方,继续debug验证
image.png
发现在如下地方开始返回了
image.png

 if (!nd.canTransferToDirectly(sc))
                return IOStatus.UNSUPPORTED_CASE;

而nd定义是,使用本地方法读写,显然这里是没有使用本地方法读写的

    // Used to make native read and write calls
    private final FileDispatcher nd;
  1. 寻找nd初始化地方
    首先看调用transferTo的channel,该channel使用如下方法生成
 socketChannel.configureBlocking(true);
 FileChannel fileChannel = new FileInputStream(p).getChannel();

进入getchannel()方法
image.png

this.channel = fc = FileChannelImpl.open(fd, path, true,
                        false, false, this);

记住direct变量传入的为false,进入open()方法
image.png

接着进入FileChannelImpl构造器
image.png
发现nd是直接new出来的,并且当前direct为false

 this.nd = new FileDispatcherImpl();
  1. 确定了nd由来之后,接着回到我们debug的处
 SelectableChannel sc = (SelectableChannel)target;
            if (!nd.canTransferToDirectly(sc))
                return IOStatus.UNSUPPORTED_CASE;

进入canTransferToDirectly方法实现
image.png
sc即为target强转的对象,由于之前我们配置了sockerChannel为bloking的(4中),所以当前 sc.isBlocking()为true,那么只有fastFileTransfer为false了
6. 查看fastFileTransfer定义
image.png
还记得吗,在4中我们寻找nd的时候,最后nd来源

 this.nd = new FileDispatcherImpl();

即直接new了一个,而当前fastFileTransfer也是定义在FileDispatcherImpl中的,即这时候fastFileTransfer并没有赋值,因此即为默认值false
7. 回到FileChannel判断是否能使用zero-copy的反方transferToDirectly
image.png
这时候就会返回UNSUPPORTED_CASE,即-6
8.之后回到我们transforTo的方法的第二个if,也就是产生返回值的if
image.png

  // Attempt a mapped transfer, but only to trusted channel types
  // 大致意思是尝试使用内存映射,不懂的可以了解下nio的内存映射
        if ((n = transferToTrustedChannel(position, icount, target)) >= 0)
            return n;

由于在此地返回,那么n肯定是大于0的,进入transferToTrustedChannel()方法查看
9. 进行基本判断之后,就开始传输,注意while循环里面的方法
image.png

  long remaining = count;
    while (remaining > 0L) {
            long size = Math.min(remaining, MAPPED_TRANSFER_SIZE);
            try {
                MappedByteBuffer dbb = map(MapMode.READ_ONLY, position, size);

当进入while循环,会在remaining和MAPPED_TRANSFER_SIZE常量之间取最小值,remaining即是我们的count,好像是318m吧,接着查看常量MAPPED_TRANSFER_SIZE定义,
image.png
现在明白了吧,当不满足zero-copy的时候会尝试使用内存映射,而内存映射的限制MAPPED_TRANSFER_SIZE是8m,因此我们使用transferTo方法向socketChannel拷贝数据的时候会限制在8m,而向其他的一些通道Channel拷贝的时候是限制在2g,原因就是底层使用的机制不一致导致的。
10. 后面map就是具体的内存映射实现代码了,可以看见返回值dbb,即是8m
image.png