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

缘由

附一段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端代码);

  • ioi代码如下:
 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
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHhSbVuT-1575611299777)(http://www.lmy25.wang:10700/upload/2019/12/image-db116fd22f354fcaaa89f71d2fca3cc4.png)]
    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里面去,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LTohBvAH-1575611299778)(http://www.lmy25.wang:10700/upload/2019/12/image-fd6ed25d4f41411a8557786777460d0d.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QcW1CnBL-1575611299779)(http://www.lmy25.wang:10700/upload/2019/12/image-4d5f2d1cc5f0473a8f6a9d07a3351ad2.png)]
主要是做一些基础判断,通道是否打开,是否可写,是不是fileChannel实例,position校验,以及确定当前可操作的字节数

//count是我们需要的字节数,而Integer.MAX_VALUE是最大字节数,2g,如果超过
//需要while像之前那个循环操作,但是我们为啥之前限制是8m呢?
 int icount = (int)Math.min(count, Integer.MAX_VALUE);
  • 继续step over往下寻找
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-td9hq0qn-1575611299779)(http://www.lmy25.wang:10700/upload/2019/12/image-31441896ab2f4b568babfc65117b4442.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)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1UxbLXpp-1575611299780)(http://www.lmy25.wang:10700/upload/2019/12/image-a4b121b0a9f54c839bf85728079c6a7e.png)]
2. 进入 transferToDirectly(position, icount, target)方法,也在FileChannelImpl,可以看见方法返回了很多常量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GQQTVrzu-1575611299781)(http://www.lmy25.wang:10700/upload/2019/12/image-76415ceec73a40dc9979aeb1753a030c.png)]
点过去看看这些常量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aGz8l92G-1575611299781)(http://www.lmy25.wang:10700/upload/2019/12/image-3cb91c2d00184ea4910b16cabfad050f.png)]```java
我们返回的-6正是 UNSUPPORTED_CASE
3. 回到方法,这几个这是返回的地方,继续debug验证
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p8jn9aXU-1575611299782)(http://www.lmy25.wang:10700/upload/2019/12/image-588a6e7c085f4b4c82166c539770690a.png)]
发现在如下地方开始返回了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y0rTwYIG-1575611299782)(http://www.lmy25.wang:10700/upload/2019/12/image-d76e37b8403c470b811da74fa3db1ad3.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()方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yKiZIvUl-1575611299783)(http://www.lmy25.wang:10700/upload/2019/12/image-a3f1090014d5478ba7283a9da9f460de.png)]

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

记住direct变量传入的为false,进入open()方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5lvwI8q-1575611299783)(http://www.lmy25.wang:10700/upload/2019/12/image-632acb3daf014c8db2f7a100d3d8f824.png)]

接着进入FileChannelImpl构造器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UYOeSUEe-1575611299784)(http://www.lmy25.wang:10700/upload/2019/12/image-2577758d320c421c89fca329297a2e56.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方法实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Qjqv2eP-1575611299784)(http://www.lmy25.wang:10700/upload/2019/12/image-eb3343835ac146be9ad3dff045726e3a.png)]
sc即为target强转的对象,由于之前我们配置了sockerChannel为bloking的(4中),所以当前 sc.isBlocking()为true,那么只有fastFileTransfer为false了
6. 查看fastFileTransfer定义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsxqVLPa-1575611299784)(http://www.lmy25.wang:10700/upload/2019/12/image-f472e08076994afd95cea19b308ce152.png)]
还记得吗,在4中我们寻找nd的时候,最后nd来源

 this.nd = new FileDispatcherImpl();

即直接new了一个,而当前fastFileTransfer也是定义在FileDispatcherImpl中的,即这时候fastFileTransfer并没有赋值,因此即为默认值false
7. 回到FileChannel判断是否能使用zero-copy的反方transferToDirectly
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CSBL2FWw-1575611299785)(http://www.lmy25.wang:10700/upload/2019/12/image-f0702a7dc7584080bfe2a5f205e9048a.png)]
这时候就会返回UNSUPPORTED_CASE,即-6
8.之后回到我们transforTo的方法的第二个if,也就是产生返回值的if
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGo9blzs-1575611299785)(http://www.lmy25.wang:10700/upload/2019/12/image-9e1cf957233f45ae8aa015e13b3087da.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循环里面的方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0WJM6Tcv-1575611299786)(http://www.lmy25.wang:10700/upload/2019/12/image-56ebf4d07bdd45a99a00d0d92f3b169e.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定义,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-99d1o06z-1575611299787)(http://www.lmy25.wang:10700/upload/2019/12/image-266c317b161a485c9dad145bae766922.png)]
现在明白了吧,当不满足zero-copy的时候会尝试使用内存映射,而内存映射的限制MAPPED_TRANSFER_SIZE是8m,因此我们使用transferTo方法向socketChannel拷贝数据的时候会限制在8m,而向其他的一些通道Channel拷贝的时候是限制在2g,原因就是底层使用的机制不一致导致的。
10. 后面map就是具体的内存映射实现代码了,可以看见返回值dbb,即是8m
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-khZVCT6x-1575611299787)(http://www.lmy25.wang:10700/upload/2019/12/image-bc6084f7a1524031b81b73fb6394b568.png)]

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Vue.js是一个前端JavaScript框架,Spring Boot是一个Java后端框架。因此,文件上传到MySQL的步骤包括前端和后端两部分。 前端部分: 1. 在Vue组件中使用`<input type="file">`标签创建文件上传组件 2. 使用Vue.js的事件绑定来监听文件上传组件的change事件 3. 在change事件中获取文件对象并使用FormData对象将文件封装在表单数据中 4. 使用axios发送一个HTTP POST请求到后端服务器,请求中带上表单数据 后端部分: 1. 在Spring Boot中配置文件上传解析器,例如使用CommonsMultipartResolver 2. 在控制器中处理文件上传请求,通过`MultipartFile`对象获取上传文件 3. 使用Java I/O流将文件写入磁盘(或其他存储介质) 4. 使用JDBC或MyBatis将文件的相关信息(如文件名、大小、类型等)存储到MySQL数据库中 上面只是一个大致的流程,具体实现需要根据具体需求来细化和完善。 ### 回答2: Vue和Spring Boot都是热门的开发框架,它们结合起来可以很方便地实现文件上传到MySQL的功能。 在前端的Vue中,可以使用<input type="file">标签来选择文件。在上传文件时,可以使用FormData对象将文件数据转换成二进制形式,并通过axios或其他网络请求库发送到后端。 在后端的Spring Boot中,可以使用@RequestParam注解来接收前端传来的文件数据。Spring Boot提供了MultipartFile类来处理文件上传,可以使用其getSize()方法获取文件大小,getOriginalFilename()方法获取文件名等信息。需要注意的是,在应用中要配置上传文件的大小限制和文件存储路径等参数。 在将文件数据存储到MySQL数据库时,一种常见的做法是将文件内容转换为字节数组进行存储。可以使用MultipartFile类的getBytes()方法获取文件字节数组,并将其存储到数据库中的BLOB类型的字段中。 另一种方式是将文件存储到服务器的本地磁盘上,然后将文件路径存储到MySQL数据库中。可以使用MultipartFile类的transferTo()方法将文件保存到指定的目录中,在数据库中存储文件路径即可。 无论是哪种方式,都需要创建对应的数据库表结构,包含文件名、文件类型、文件大小、文件内容等字段。 总之,使用Vue和Spring Boot结合起来可以方便地实现文件上传到MySQL的功能。前端Vue负责文件选择和发送请求,后端Spring Boot负责接收文件数据并将其存储到MySQL数据库中。具体的实现方式可以根据项目需求和个人喜好进行调整。 ### 回答3: Vue是一种用于构建用户界面的JavaScript框架,而Spring Boot是Java开发的一种框架。要实现文件上传到MySQL数据库,可以使用以下方法: 1. 前端(Vue)部分: - 在Vue中创建一个文件上传的组件,该组件包含一个文件选择器或拖放区域。 - 使用File API来获取选择的文件,并将其存储为FormData对象。 - 使用Axios或其他HTTP库将FormData对象发送到后端。 2. 后端(Spring Boot)部分: - 创建一个RESTful API来处理文件上传的请求。这可以在Controller类中完成。 - 在Controller方法中获取上传的文件,可以使用`@RequestParam`注解来接收文件,并使用`MultipartFile`来处理文件。 - 使用JPA或JDBC等技术将文件的数据存储在MySQL数据库中。可以使用Spring Data JPA或MyBatis等框架来处理数据库操作。 需要注意的是,为数据库提供安全的文件上传功能是一个较复杂的过程。可以考虑以下几点: - 对上传文件的大小进行限制,以防止恶意攻击或过大的文件导致服务器资源耗尽。可以在前端和后端都进行限制。 - 对文件的内容进行验证和过滤,确保只有允许的文件类型和格式才能上传。 - 为上传文件生成一个唯一的文件名,以防止文件名冲突。 - 可以将上传的文件存储在服务器的临时目录中,并在数据库中存储文件的相关元数据(如文件名、路径等)。 最后,以上仅是提供一个基本的实现思路。具体的实现方式可能根据项目需求和技术栈的不同而有所变化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值