PHP实现大文件断点续传上传

在日常的业务开发过程中会遇到大文件的上传,会因为PHP自身的一些限制,或是上传过程中网络中断、断电而导致上传失败,也存在如果在上传到90%的时候不小心关掉了浏览器,或者是手一抖摁了F5,完了,一切还得从头再来。前端可以选择用百度Fex团队开发的WebUpload。
首先处理PHP方面的,修改下配置文件php.ini:

file_upload = on ; 是否通过HTTP上传文件的开关,默认ON即时开。
upload_max_filesize = 8M ; 即允许上传文件大小的最大值,默认为2M。
post_max_size = 8M ; 指通过表单POST给PHP的所能接收的最大值,包括表单里的所有值 ,默认为8M。
max_execution_time = 600 ; 每个PHP页面运行的最大时间值(秒),默认30秒。
max_input_time = 600 ; 每个PHP页面接收数据所需的最大时间,默认60秒。
memory_limit = 8M ; 每个PHP页面所吃掉的最大内存,默认8M。

把上述参数修改后,设置上传的文件临时存放目录 upload_tmp_dir = /tmp , 在网络所允许的正常情况下,就可以上传大一点的文件了。

传统的表单提交文件或是HTML5的FormData都是将文件“整块”提交,服务端取到该文件后再进行转移、重命名等操作,因此,无法实时保存文件的已上传部分。而且在http协议下,我们无法保持浏览器与服务端的长连接,不能以文件流的形式来提交。
所以有以下思路:

  1. 浏览器记住(如localStorage)最近一次成功传输的位置;当再次上传这个图片的时候,直接从浏览器存储的位置开始穿。
  2. 浏览器不做任何事,在上传之前先去后台走一遍,看看目前此文件是否存在,以及存在的大小,返回给浏览器,然后浏览器再决定上传的起始位置。
    也就是说,对上传的文件进行分割,每次只上传一小片。服务端接收到文件后追加原来部分,最后合并成完整的文件。
    每次上传文件片前先获取已上传的文件大小,确定本次应切割的位置
    每次上传完成后更新已上传文件大小的记录
    标识客户端和服务端的文件,保证不会把A文件的内容追加到B文件上
    整体的思路如下:
    1. 客户端:
      • 获取文件md5(MD5是文件唯一标识,用来判断是否存在此文件,并且用作分片的文件夹名)
      • 将文件分片,验证分片是否上传过,上传过直接跳过当前分片
      • 上传分片到MD5的文件夹(保存文件名建议按分片序号来,因为分片的顺序很重要)
      • 最后一个分片上传完成后发送合并分片请求并由服务器返回文件信息
        1. 服务端:
      • 获取md5文件夹下的文件数量并返回用作分片验证
      • 接收文件分片并保存到文件md5的文件夹,文件名称使用分片序号:如0.mp4,1.mp4
      • 合并分片时将md5文件夹下的所有文件按文件名顺序提取并写入到最终的文件内
      • 写入完成获取最终文件hash并判断是否存在,存在则返回已存在文件,删除当前文件,不存在则写入数据库并返回文件信息
        实际还要加入许多验证,比如客户端获取到md5后马上要验证文件是否存在,存在就不上传,直接使用文件信息,不存在则上传
        分片上传前也要验证,不过分片的跳过规则需要注意,服务器只需要返回已有的分片数量,客户端根据已有的分片和当前分片索引即可判断是否应该跳过,因为分片是按顺序上传的。

        工作原理/技术要点
         如果我们有一个10M的文件,每次切割上传1M,那么是需要发10次请求来完成的。在http协议下,只能这么搞。断点上传分三步来完成:
        选择一个文件后,获取该文件在服务器上的大小,通过本地存储或自定义的函数来获取。
        根据已上传大小切割文件,发出n次请求不断向服务器提交文件片,服务端不断追加文件内容
        当已上传文件大小达到文件总大小时,上传结束

         首选是文件的分割,HTM5新增了Blob数据类型,并且提供了一个可以分割数据的方法:slice(),其用法和字符串、数组的slice()方法一样,可以截取一个二进制文件的一部分。而且XMLHttpRequest level2(也就是Ajax 2.0)中最大的变化之一就是对二进制数据的支持。
        此时,我们想一次性传一个80M的文件,每20M作为一个请求发送出去,后台再把这些二进制数据拼合成一个完整文件。
        slice(0,20) ;  slice(20,40) ;  slice(60)
        80*1024*1024B,我们每次传1024*1024B,也就是1M,假设传了79M,结果大脚一抖,电源关掉有木有!
        用户重新开机,决定再次传这个80M的视频。当用户选择这个文件后,我们先去后台走一圈,把当前已经传好的文件大小反馈给客户端(Ajax1.0就可以),JS拿反馈大小和源文件大小一比对,从残缺位置 slice , file.slice(79*1024*1024) 接着之前的只传1M就OK啦!

         其次是文件片的保存与追加,先用file_get_contents获取文件的二进制格式,再file_put_content每次将文件追加,可以这么写:file_put_contents('uploads/'.$filename,file_get_contents($_FILES['file']['tmp_name'],FILE_APPEND));要注意一下,通过FormData对象上传的文件对象,在PHP中也是通过$_FILES全局对象获取的,还有为了避免上传后文件中文的乱码,用一下iconv。
         接下来我们还需要实时保存已上传文件的大小,以便于下次上传前进行正确切割。使用HTML5的localStorage是一种方法,将已上传的大小保存在本地,下次上传前先从本地读取。不过这种方式是很局限的,抛开用户可能通过各种管家清除掉本地数据不讲,假如用户在A页面上传了一个文件的50%,然后在B页面想把该文件上传到另一个地方,结果从本地一读已上传50%了,直接从51%的位置开始上传了,显然是个错误。问题就在于本地不能存太多的信息,通过File API只能获取到文件的原始名称,无法正确的与服务器上的文件正确匹配。所以真正在项目中用,还得依靠服务端来保存这些数据。
        在服务端保存数据
         用户在使用上传的时候可能有各种你意想不到的操作,这里发挥一下想象描述用户可能的行为:
        用一台机器使用不同账号登录,上传同一个文件
        文件上传了一部分,然后修改了文件内容,再次上传
        文件上传完成100%,再次上传该文件
        同一个页面有多个上传按钮,上传同一个文件,或在不同页面上传同一个文件

         仅仅上面四条,是不是情况就够复杂了?再加上你系统还有自己的业务逻辑,所以在服务端保存已上传文件数据是非常有必要的,而且保存数据和获取数据的函数都要自定义。
         向后台的某个地址发送一个请求,传递文件名和文件的最后修改时间为参数,后台根据这两个参数来找到与前台所选择的文件对应的服务器上的文件,将服务器返回的文件大小return出去。为什么要传递这两个参数呢?我们在前台无法知道服务器上的这个文件的名称,所以使用原始文件名作为一个辅助标识。为了防止用户在两次上传间隔修改了文件,我们把文件的最后修改时间也传给服务端,让服务端进行比较,若时间不对应则返回已上传大小为0,重新上传此文件。数据库中需要一张表来记录每个文件的情况,包含的字段大致有:
        字段       描述
        uid       用户ID
        id      文件ID标识(唯一)
        lenSvr    服务器文件大小
        lenLoc    本地文件大小
        blockOffset  文件块偏移(在整个文件中的位置)
        blockSize   文件块大小
        blockIndex   文件块索引(基于1)
        blockMd5   文件块MD5
        complete   当前文件是否已经传完

        总之最终的目的就是要找到前台选择的文件在服务器上真正对应的文件,并将已上传大小正确返回。文件上传完,可以用sha1来验证完整性。


        参与相关文章:
        https://www.zhangxinxu.com/wordpress/2013/11/xmlhttprequest-ajax-localstorage-%E6%96%87%E4%BB%B6%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0/
        http://blog.ncmem.com/wordpress/2019/12/10/web%e4%b9%8b%e5%a4%a7%e6%96%87%e4%bb%b6%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0/
        前端实现文件的断点续传:https://cloud.tencent.com/developer/article/1326932?from=information.detail.php%E4%B8%8A%E4%BC%A0%E5%AE%9E%E7%8E%B0%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0
        PHP中使用TUS协议来实现大文件的断点续传:https://learnku.com/php/t/26050
        https://cloud.tencent.com/developer/article/1448826?from=information.detail.php%E4%B8%8A%E4%BC%A0%E5%AE%9E%E7%8E%B0%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0
        使用PHP的创始人 Rasmus Lerdorf 写的APC扩展模块来实现:https://blog.csdn.net/weixin_45525177/article/details/103005801

猜你喜欢

转载自blog.51cto.com/bin06/2630817