zoukankan      html  css  js  c++  java
  • 大文件传输解决方案:分片上传 / 下载限速

    前言

    不少项目中会遇到上传下载视频、更新包、应用程序等文件,此类文件的共同点就是十分巨大,我在项目中遇到过 4G 左右的文件同时 100 多台机器下载,此时如果用 post 上传和下载想一下都不可能,但百度查的话都是说调整 php.ini 的 post 的限制,但这是一个可笑的解决方法,由此就需要用另一种解决方法 -- 分片上传和下载限速

    在此带大家用 php 实现一下,各种语言和框架同时适用,本次用到的是 php 的 laravel, 语言和实现的思路是一样的

    如果项目中用到的分片上传,个人建议找相对应的包如 (AetherUpload-Laravel)、有条件直接用 7 牛云、阿里云等大公司的分片上传服务

    分片上传

    原理

    1. 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
    2. 初始化一个分片上传任务,返回本次分片上传唯一标识;
    3. 按照一定的策略(串行或并行)发送各个分片数据块;
    4. 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。

    实现

    h5

    h5 实现部分,h5 部分实现了把文件的分割,在上传中,告诉服务端文件的总片数和当前是第几片,各个临时文件通过 http 请求发送出去

    <!doctype html>
    <html>
    <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>
        <style>
            #progress{
                width: 300px;
                height: 20px;
                background-color:#f7f7f7;
                box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);
                border-radius:4px;
                background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);
            }
    
            #finish{
                background-color: #149bdf;
                background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);
                background-size:40px 40px;
                display: inline-block;
                height: 20px;
            }
            form{
                margin-top: 50px;
            }
        </style>
    </head>
    <body>
    <p id="progress">
        <span id="finish" style=" 0%;" progress="0"></span>
    </p>
    <form action="">
        <input type="file" name="file" id="file">
        <input type="button" value="停止" id="stop">
    </form>
    <script>
        var fileForm = document.getElementById("file");
        var stopBtn = document.getElementById('stop');
        var upload = new Upload();
    
        fileForm.onchange = function(){
            upload.addFileAndSend(this);
        }
    
        stopBtn.onclick = function(){
            this.value = "停止中";
            upload.stop();
            this.value = "已停止";
        }
    
        function Upload(){
            var xhr = new XMLHttpRequest();
            var form_data = new FormData();
            const LENGTH = 1024 * 1024 *2;
            var start = 0;
            var end = start + LENGTH;
            var blob;
            var blob_num = 1;
            var is_stop = 0
    
            //对外方法,传入文件对象
            this.addFileAndSend = function(that){
                var file = that.files[0];
                blob = cutFile(file);
                sendFile(blob,file);
                blob_num  += 1;
            }
    
            //停止文件上传
            this.stop = function(){
                xhr.abort();
                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){
                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','http://vnn-admin.cc/Api/sliceUpload',false);
    
                xhr.onreadystatechange  = function () {
                    if (xhr.readyState==4 && xhr.status==200)
                    {
                        console.log(xhr.responseText);
                    }
    
                    var progress;
                    var progressObj = document.getElementById('finish');
                    if(total_blob_num == 1){
                        progress = '100%';
                    }else{
                        progress = Math.min(100,(blob_num/total_blob_num)* 100 ) +'%';
                        // console.log(progress);
                        // console.log('分割');
                    }
                    progressObj.style.width = progress;
                    var t = setTimeout(function(){
                        if(start < file.size && is_stop === 0){
                            blob = cutFile(file);
                            sendFile(blob,file);
                            blob_num  += 1;
                        }else{
                            setTimeout(t);
                        }
                    },1000);
                }
                xhr.send(form_data);
            }
        }
    
    </script>
    </body>
    </html>

    服务端

    服务端接收上传的文件片,并判断是否为最后一块,如果是就合并文件,删除上传的文件块

    /**
         * @Desc: 切片上传
         *
         * @param Request $request
         * @return mixed
         */
        public function sliceUpload(Request $request)
        {
            $file = $request->file('file');
            $blob_num = $request->get('blob_num');
            $total_blob_num = $request->get('total_blob_num');
            $file_name = $request->get('file_name');
    
            $realPath = $file->getRealPath(); //临时文件的绝对路径
    
            // 存储地址
            $path = 'slice/'.date('Ymd')  ;
            $filename = $path .'/'. $file_name . '_' . $blob_num;
    
            //上传
            $upload = Storage::disk('admin')->put($filename, file_get_contents($realPath));
    
            //判断是否是最后一块,如果是则进行文件合成并且删除文件块
            if($blob_num == $total_blob_num){
                for($i=1; $i<= $total_blob_num; $i++){
                    $blob = Storage::disk('admin')->get($path.'/'. $file_name.'_'.$i);
    //              Storage::disk('admin')->append($path.'/'.$file_name, $blob);   //不能用这个方法,函数会往已经存在的文件里添加0X0A,也就是
    换行符
                    file_put_contents(public_path('uploads').'/'.$path.'/'.$file_name,$blob,FILE_APPEND);
    
                }
               //合并完删除文件块
                for($i=1; $i<= $total_blob_num; $i++){
                    Storage::disk('admin')->delete($path.'/'. $file_name.'_'.$i);
                }
            }
    
            if ($upload){
                return $this->json(200, '上传成功');
            }else{
                return $this->json(0, '上传失败');
            }
    
        }

    下载限速

    原理

    1. 通过每秒限制输出的字节
    2. 关闭 buffer 缓存

    实现

    public function sliceDownload()
        {
    
            $path = 'slice/'.date('Ymd')  ;
    
            $filename = $path .'/'. '周杰伦 - 黑色幽默 [mqms2].mp3' ;
    
            //获取文件资源
            $file = Storage::disk('admin')->readStream($filename);
    
            //获取文件大小
            $fileSize = Storage::disk('admin')->size($filename);
    
            header("Content-type:application/octet-stream");//设定header头为下载
            header("Accept-Ranges:bytes");
            header("Accept-Length:".$fileSize);//响应大小
            header("Content-Disposition: attachment; filename=周杰伦 - 黑色幽默 [mqms2].mp3");//文件名
    
            //不设置的话要等缓冲区满之后才会响应
            ob_end_clean();//缓冲区结束
            ob_implicit_flush();//强制每当有输出的时候,即刻把输出发送到浏览器
            header('X-Accel-Buffering: no'); // 不缓冲数据
    
            $limit=1024*1024;
            $count=0;
    
            //限制每秒的速率
            while($fileSize-$count>0){//循环读取文件数据
                $data=fread($file,$limit);
                $count+=$limit;
                echo $data;//输出文件
                sleep(1);
            }
    
        }
    
    大文件传输解决方案:分片上传 / 下载限速 / 断点续传

    当你需要更大速度的时候调整 $limit 的数值即可

    总结

    至此关于分片上传和下载限速的原理和简单实现 Demo 已经说完,大应该了解怎么实现分片上传了吧,希望对大家有帮助,因为大文件上传和下载是实现中经常遇到的事情

    原文地址:https://learnku.com/articles/31108

  • 相关阅读:
    PHP调用WCF提供的方法
    关于git报 warning: LF will be replaced by CRLF in README.md.的警告的解决办法
    vue中引入mui报Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them的错误
    微信小程序报Cannot read property 'setData' of undefined的错误
    Vue那些事儿之用visual stuido code编写vue报的错误Elements in iteration expect to have 'v-bind:key' directives.
    关于xampp中无法启动mysql,Attempting to start MySQL service...的解决办法!!
    PHP的环境搭建
    新手PHP连接MySQL数据库出问题(Warning: mysqli_connect(): (HY000/1045): Access denied for user 'root'@'localhost' (using password: YES))
    手机号码、获得当前时间,下拉框,填写限制
    团队作业(五):冲刺总结
  • 原文地址:https://www.cnblogs.com/zhangbobo/p/11570745.html
Copyright © 2011-2022 走看看