11

文件分片上传之后端PHP合成文件

 4 years ago
source link: https://www.helloweba.net/php/634.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在上一节文章中,我们介绍了前端文件分片上传,了解vue-simple-uploader组件自带分片上传功能,大文件一片片依次上传到后端服务器后,后端程序要将分片合成一个完整的文件,那么PHP是如何处理合成分片的呢?请看本节讲解。

为了阅读和开发方便,我将文件上传系列相关文章章节列出来:

前端什么时候发送合并请求

在上一节文章中,我们知道,当所有的分片都上传完成时,会调用 onFileSuccess() 方法。

onFileSuccess(rootFile, file, response, chunk) {
    let resp = JSON.parse(response);
    if (resp.code === 0 && resp.merge === false) {
        console.log('上传成功,不需要合并');
    } else {
        axios.post('http://localhost:9999/up.php?action=merge', {
            filename: file.name,
            identifier: file.uniqueIdentifier,
            totalSize: file.size,
            totalChunks: chunk.offset + 1
        }).then(function(res){
            if (res.code === 0) {
                console.log('上传成功')
            } else {
                console.log(res.message);
            }
        })
        .catch(function(error){
            console.log(error);
        });
    }
},

从后台返回的 response 包含了是否需要合并的指令 merge ,如果 resp.merge === true ,那就发送合并请求,告诉后端可以合成分片了。如果上传的文件只有一片,就不需要合并。

Uplader.php

我们计划用PHP写一个处理上传的类,负责检测文件、接收上传分片、合并分片等。当中还要用到数据库存储文件信息,这些我们在后面章节完成,先看结构:

<?php
class Uploader
{
    private static $tmpDir = 'D:\www\helloweba\demo\files_tmp'; //分片临时文件目录
    private static $saveDir = 'D:\www\helloweba\demo\files'; //文件最终保存目录
    private static $mysql = null;
    public $fileInfo = [
        'identifier' => '',  //文件的唯一标识
        'chunkNumber' => 1, //当前是第几个分片
        'totalChunks' => 1,  //总分片数
        'filename' => '',  //文件名称
        'totalSize' => 0  //文件总大小
    ];

    //检测断点和md5
    public function checkFile()
    {
        //
    }

    //上传分片
    public function upload()
    {
        //
    }

    //合并文件
    public function merge()
    { 

    }

    //计算时间
    private function getmicrotime()
    {
        list($usec, $sec) = explode(" ",microtime());
        return ((float)$usec + (float)$sec);
    }
    
    //返回提示消息
    private function message($code, $msg)
    {
        $res = [
            'code' => $code,
            'message' => $msg
        ];
        return $res;
    }
}

我们先定义上传目录,整个目录可以是在你的web目录,也可以是web访问不到的目录,一个临时目录files_tmp/用来保存临时分片文件,一个是真正保存文件的目录files/,注意我们是在Wind平台运行,如果是为Linux下,路径应该写成像这样:/opt/data/files。此外这两个目录要有写权限。

上传分片

首先我们接收前端上传上来的分片文件,当然在正式接收上传分片前,应该检测文件是否已经上传过了,检测文件合法性等等,这些我们在后续文章中会讲到。我们先来看PHP如何接收分片文件。

public function upload()
{
    if (!empty($_FILES)) {
        $in = @fopen($_FILES["file"]["tmp_name"], "rb");
        if (!$in = @fopen($_FILES["file"]["tmp_name"], "rb")) {
            return $this->message(1002, '打开临时文件失败');
        }
    } else {
        if (!$in = @fopen("php://input", "rb")) {
            return $this->message(1003, '打开输入流失败');
        }
    }

    if ($this->fileInfo['totalChunks'] === 1) {
        //如果只有1片,则不需要合并,直接将临时文件转存到保存目录下
        $filename = $this->fileInfo['filename'];
        $saveDir = self::$saveDir . DIRECTORY_SEPARATOR . date('Y-m-d');
        if (!is_dir($saveDir)) {
            @mkdir($saveDir);
        }

        $uploadPath = $saveDir . DIRECTORY_SEPARATOR .$filename;
        $res['merge'] = false;
    } else { //需要合并
        $filePath = self::$tmpDir. DIRECTORY_SEPARATOR . $this->fileInfo['identifier']; //临时分片文件路径
        $uploadPath = $filePath . '_' . $this->fileInfo['chunkNumber']; //临时分片文件名
        $res['merge'] = true;
    }
    if (!$out = @fopen($uploadPath, "wb")) {
        return $this->message(1004, '文件不可写');
    }
    while ($buff = fread($in, 4096)) {
        fwrite($out, $buff);
    }
    @fclose($in);
    @fclose($out);
    
    $res['code'] = 0;
    return $res;
}

前端是通过 multipart/form-data; 将文件以二进制形式传给PHP,所以我们用 $_FILES 接收文件信息。

接收到文件后,我们判断这个文件是否就只有1个分片,如果只有1个分片就没必要再合成了,直接将该分片保存到files/下,并且告诉前端不需要合并文件: $res['merge'] = false;

如果是有多个分片,那就将这些分片保存到临时目录下,分片的命名应该是“文件唯一标识_当前分片”,如abcd_1,标识文件abcd的第一个分片,这样我们接下来合并文件就好办了。

合并文件

合并之前,先检查下该文件的所有分片是否都上传完毕,就是检测分片文件是否都存在。

public function merge()
{        
    $filePath = self::$tmpDir. DIRECTORY_SEPARATOR . $this->fileInfo['identifier'];

    $totalChunks = $this->fileInfo['totalChunks']; //总分片数
    $filename = $this->fileInfo['filename']; //文件名

    $done = true;
    //检查所有分片是否都存在
    for ($index = 1; $index <= $totalChunks; $index++ ) {
        if (!file_exists("{$filePath}_{$index}")) {
            $done = false;
            break;
        }
    }
    if ($done === false) {
        return $this->message(1005, '分片信息错误');
    }
    //如果所有文件分片都上传完毕,开始合并
    $timeStart = $this->getmicrotime(); //合并开始时间
    $saveDir = self::$saveDir . DIRECTORY_SEPARATOR . date('Y-m-d');
    if (!is_dir($saveDir)) {
        @mkdir($saveDir);
    }

    $uploadPath = $saveDir . DIRECTORY_SEPARATOR .$filename;

    if (!$out = @fopen($uploadPath, "wb")) {
        return $this->message(1004, '文件不可写');
    }
    if (flock($out, LOCK_EX) ) { // 进行排他型锁定
        for($index = 1; $index <= $totalChunks; $index++ ) {
            if (!$in = @fopen("{$filePath}_{$index}", "rb")) {
                break;
            }
            while ($buff = fread($in, 4096)) {
                fwrite($out, $buff);
            }
            @fclose($in);
            @unlink("{$filePath}_{$index}"); //删除分片
        }
       
        flock($out, LOCK_UN); // 释放锁定
    }
    @fclose($out);
    $timeEnd = $this->getmicrotime(); //合并完成时间

    $res['code'] = 0;
    $res['time'] = $timeEnd - $timeStart; //合并总耗时

    return $res; 
}

如果分片文件都存在,开始合并所有分片,现将要最终合并的文件锁定,然后遍历所有分片,将分片文件依次写入合并的文件中,最后释放锁定。

每个分片被合并后,应当立即删除该分片。

这里我测试用了计算合并过程的耗时,真实应用可以将计时代码去掉。

合并大文件

我用自己的机器测试(8G内存,SSD),上传了一个约800MB的文件,2M一个分片,约400个分片,合并总耗时3秒钟,合并一个3G的文件耗时30秒钟。也就是说文件越大,分片越多,合成文件所花费的时间越长。但是通过观察内存变化,上面的代码在合并文件时内存消耗很低。那如果是特别大的文件,就会有大量分片,那这样的话合并过程是不是很耗时耗性能呢?

对于特大号的文件合并,有人提出建立一套算法,一个文件有N个分片,先建立一个序列,序列分成N个片段,每个分片占用一个片段,文件上传时就把对应的分片塞到对应的片段中,最终分片文件上传完了文件也就合成好了。这个方法也不错,将合并的时间分摊到每个分片上传上去了。

还有人提出,使用追加的方式将分片一片片往文件里塞,整个方法不可取,因为如果设置并发数大的话,不能保证文件是否按分片顺序合成的,最终有可能得到的文件是个乱序的不可用的文件。

那么我给大家建议使用Swoole来处理文件合成这一步,让耗时的操作在后台运行,不让前端等待,悄悄的在后台合成文件即可,如何?

好了,接下来我们要了解文件上传前计算MD5的操作以便实现秒传的功能,以及超大文件如何快速计算出md5值呢?敬请关注后续文章。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK