js + php + apache 视频切片上传,无法上传48MB以上的文件。后续请求全部500,需要做哪些调整?

这个问题与服务器或者后端的关系更大吧,不过现在是自己做小项目,没法找后端对线。(我自己就是...)

问题描述与相关代码

主要有参考这里 https://www.shuzhiduo.com/A/KE5QmG605L/
文件上限2GB,文件切片每片为1MB,那么最多切片为2000片。前几次测试了小文件没遇到问题,上传更大些的文件后就发现,当上传完48片后,后续的所有请求都成500了。有换过不同的文件进行测试过,故目前只能上传48MB的文件。
后续又尝试了修改切片大小,将切片修改为了10MB每块依旧会产生问题,只能上传4块了,就是40MB。那么现在的状况就是不能上传48MB以上的文件。
修改apache和php配置有看过其他的回答尝试过,既然依旧有问题也许没改到点上吧。
请教各位是否有办法与建议,如果网络上有这方面的讲解或者这个问题的其他解法,请麻烦告诉我也可以节省时间重复回答。辛苦了!

JS

function videoFileUpload(){
    var xhr = new XMLHttpRequest();
    var form_data = new FormData();
    //每片切片1MB
    const LENGTH = 1024 * 1024 * 1;
    var start = 0;
    var end = start + LENGTH;
    var blob;
    var blob_num = 1;
    var is_stop = 0;

    //开始上传
    this.start = function(){
        //开始上传
        var file = upload_video_file;
        blob = cutFile(file);
        sendFile(blob,file);
        blob_num += 1;
    }
    //停止上传
    this.stop = function () {
        //后续在这里发送一个请求提示后台删除缓存切片文件
        is_stop = 1;
    }

    //切割文件
    function cutFile(file){
        var file_blob = file.slice(start,end);
        start = end;
        end = start + LENGTH;
        return file_blob;
    };
    //发送文件
    function sendFile(blob,file){
        //只有处于不停止状态才执行
        if(is_stop == 0){
            var total_blob_num = Math.ceil(file.size / LENGTH);
            form_data.append('file',blob);
            form_data.append('blob_num',blob_num);
            form_data.append('total_blob_num',total_blob_num);
            form_data.append('file_name',file.name);

            //原本是异步的,但是这会导致请求取消,只能取消异步
            xhr.open('POST','https://www.xxx.com/api/Video_file_upload.php',false);
            xhr.onreadystatechange = function () {

                //进行下一次发送
                var t = setTimeout(function(){
                    //不处于停止状态时才执行
                    if(start < file.size){
                        blob = cutFile(file);
                        sendFile(blob,file);
                        blob_num += 1;
                    }
                    else{
                        setTimeout(t);
                    }
                },1000);
            }
            xhr.send(form_data);
        }
    }
}

php

<?php
//实例化并获取系统变量传参
$upload = new Upload($_FILES['file']['tmp_name'],$_POST['blob_num'],$_POST['total_blob_num'],$_POST['file_name']);
//调用方法,返回结果
$upload->apiReturn();

class Upload{
    private $filepath = '../source/video'; //上传目录
    private $tmpPath; //PHP文件临时目录
    private $blobNum; //第几个文件块
    private $totalBlobNum; //文件块总数
    private $fileName; //文件名

    public function __construct($tmpPath, $blobNum, $totalBlobNum, $fileName){
        $this->tmpPath = $tmpPath;
        $this->blobNum = $blobNum;
        $this->totalBlobNum = $totalBlobNum;
        $this->fileName = $fileName;

        $this->moveFile();
        $this->fileMerge();
    }

    //判断是否是最后一块,如果是则进行文件合成并且删除文件块
    private function fileMerge(){
        if($this->blobNum == $this->totalBlobNum){
            $blob = '';

            for($i=1; $i<= $this->totalBlobNum; $i++){
                $blob .= file_get_contents($this->filepath.'/'. $this->fileName.'__'.$i);
            }
            file_put_contents($this->filepath.'/'. $this->fileName,$blob);
            $this->deleteFileBlob();
        }
    }

    //删除文件块
    private function deleteFileBlob(){
        for($i=1; $i<= $this->totalBlobNum; $i++){
            @unlink($this->filepath.'/'. $this->fileName.'__'.$i);
        }
    }

    //移动文件
    private function moveFile(){
        $this->touchDir();
        $filename = $this->filepath.'/'. $this->fileName.'__'.$this->blobNum;
        move_uploaded_file($this->tmpPath,$filename);
    }

    //API返回数据
    public function apiReturn(){
        if($this->blobNum == $this->totalBlobNum){
            if(file_exists($this->filepath.'/'. $this->fileName)){
                $data['code'] = 2;
                $data['msg'] = 'success';
                $data['file_path'] = 'http://'.$_SERVER['HTTP_HOST'].dirname($_SERVER['DOCUMENT_URI']).str_replace('.','',$this->filepath).'/'. $this->fileName;
            }
        }
        else{
            if(file_exists($this->filepath.'/'. $this->fileName.'__'.$this->blobNum)){
                $data['code'] = 1;
                $data['msg'] = 'waiting for all';
                $data['file_path'] = '';
            }
        }
        header('Content-type: application/json');
        echo json_encode($data);
    }

    //建立上传文件夹
    private function touchDir(){
        if(!file_exists($this->filepath)){
            return mkdir($this->filepath);
        }
    }
}

相关图片

网络请求,网络状态有关的图片



成功被上传到服务器的文件,从图可见只能上传到第48片,之前上传成功的文件容量都小于48MB。

image.png

日志

access.log

image.png

ssl_request.log

image.png

error.log

image.png

php的日志没有报错(在5月27日有做过几次测试,但日志没有发生更新)

image.png

阅读 3.1k
3 个回答

之前没有 Apache 的环境,看你上面一说,还真以为是 Apache 的问题,就没去复现,后面看到你补充说跟 FcgidMaxRequestLen,那肯定就不是 Apache 的问题了,抽时间专成搭了个 Apache 的环境,跑起来,然后发现是你前端代码的问题。

你前端代码里面,把 FormData 构造函数里面,FormData 只会在对象初始化的时候进行一次实例化,后续的 sendFile 里面就始终使用的是最开始的那一个 FormData 对象,而 FormData 的 append 方法又比较特殊,即使键名重复了,浏览器仍然会放进去,就导致你的每一个切片,都累计了前面切片的。

  • 切片 1

image.png

  • 切片 2

image.png

  • 切片 3

image.png

最终就导致了发送的大小远超了 Apache 的 FcgidMaxRequestLen 设定值,最后 Apache 响应了 500

image.png


切片上传的目的,就是为了在服务端设置较小的允许大小的情况下上传大体积的文件,而你这里直接去改 Apache 的配置,就跟切片上传没有关系了都

要修复这个 BUG,只需要把 new FormData 的操作放到 sendFile 内部去,或者将 append 改为 set ,另外 xhr 对象最好也是放到里面去重新初始化。

另外,你的 setTimeout 也存在 bug,只不过刚好有 if 条件兜住,else 里面应该是 clearTimeout(t) 才对。


修复后的前端代码:

<!doctype html>
<html lang="zh-hans">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<input type="file" id="files">
<hr>
<button id="up">Go!</button>
<script>

    var fileUpload = new videoFileUpload();
    up.onclick = _ => fileUpload.start();

    function videoFileUpload() {
        //每片切片1MB
        const LENGTH = 1024 * 1024;
        var start = 0;
        var end = start + LENGTH;
        var blob;
        var blob_num = 1;
        var is_stop = 0;

        //开始上传
        this.start = function () {
            //开始上传
            var file = files.files[0];
            blob = cutFile(file);
            sendFile(blob, file);
            blob_num += 1;
        }
        //停止上传
        this.stop = function () {
            //后续在这里发送一个请求提示后台删除缓存切片文件
            is_stop = 1;
        }

        //切割文件
        function cutFile(file) {
            var file_blob = file.slice(start, end);
            start = end;
            end = start + LENGTH;
            return file_blob;
        };

        //发送文件
        function sendFile(blob, file) {
            //只有处于不停止状态才执行
            if (is_stop == 0) {
                var xhr = new XMLHttpRequest();
                var form_data = new FormData();

                var total_blob_num = Math.ceil(file.size / LENGTH);
                form_data.append('file', blob);
                form_data.append('blob_num', blob_num);
                form_data.append('total_blob_num', total_blob_num);
                form_data.append('file_name', file.name);

                //原本是异步的,但是这会导致请求取消,只能取消异步
                xhr.open('POST', '/upload.php', false);
                xhr.onreadystatechange = function () {

                    //进行下一次发送
                    var t = setTimeout(function () {
                        //不处于停止状态时才执行
                        if (start < file.size) {
                            blob = cutFile(file);
                            sendFile(blob, file);
                            blob_num += 1;
                        } else {
                            clearTimeout(t);
                        }
                    }, 1000);
                }
                xhr.send(form_data);
            }
        }
    }
</script>
</body>
</html>

另外你的后端代码里面,在合并文件的时候,打开了文件读取后再进行的合并,这样会导致内存不足导致大文件在合并时失败,你应该找一些合适的方式去处理。


最后,我还想说,你参照的那个站,是一个内容农场,建议屏蔽。

错误回答
以下是曾经的错误回答,就当做解题的一部分吧。
问题根源是前端bug累计切片导致请求的大小越变越大,发送到后边超出了请求长度限制所以500了。修改了请求长度上限正好给他解限了,所以可以正常发送,在表面上显得可以正常运行了。(具体可见采纳答案)

我找到了解决办法。Apache需要在httpd.conf修改FcgidMaxRequestLen(默认是51200000即50MB),把这个改大,要上传多大就改多大。不能因为是切片上传就设小。
更大容量文件的还没测试,有测试过设上限为65MB。

关于后端文件合并的优化
原本的代码将所有切片读取出来存在$blob中再写入目标文件,可能有爆内存的问题

private function fileMerge(){
    if($this->blobNum == $this->totalBlobNum){
        $blob = '';

        for($i=1; $i<= $this->totalBlobNum; $i++){
            $blob .= file_get_contents($this->filepath.'/'. $this->fileName.'__'.$i);
        }
        file_put_contents($this->filepath.'/'. $this->fileName,$blob);
        $this->deleteFileBlob();
    }
}

改进的代码,采用随用随关的方式。
打开一个切片,读取一个切片,追加至目标文件,关闭切片文件,最后关闭目标文件的方法。

private function fileMerge(){
    if($this->blobNum == $this->totalBlobNum){
        //创建目标文件
        $target_file = fopen($this->filepath.'/'. $this->fileName,"wb");
        for($i=1; $i<= $this->totalBlobNum; $i++){
            //读取切片文件上限1MB,写入目标文件后关闭
            $block_file = fopen($this->filepath.'/'. $this->fileName.'__'.$i,"rb");
            $blob = fread($block_file,1024*1024);
            fwrite($target_file, $blob);
            fclose($block_file);
        }
        //关闭目标文件,删除切片文件
        fclose($target_file);
        $this->deleteFileBlob();
    }
}

其他可以注意的地方
php和apache的配置与网络状况是否通畅,这方面可以检查一下
我这边通过这个方式成功了,不过这个问题会我想还会有其他情况,欢迎大家讨论这个问题,可以给其他遇到问题的朋友参考。

参考的一个回答(这个例子不包含切片上传) https://www.pbhtml.com/464.html

1.检查PHP和APACHE的配置文件,看看最大上传值是多少?
2.检查PHP和APACHE的配置文件,看看最大执行时间是多少?是不是存在超时的可能?
3.检查服务器和客户端之间的网络状况是否畅通?可以在本机配置下PHP环境然后测试下能否上传大文件?

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏