php实现多进程下载百度网盘文件

大家知道百度网盘下载对于非会员是有下载限速的, 最大速度基本上维持在 100kB/s以内,要下个电影啥的,那就有得等了. 之前还可以把网盘里的文件链接解析出来放到Uget或迅雷之类的下载工具里去多线程下载, 但是现在百度的文件服务器对ua做了校验, 没找到好用的可编辑http header的下载工具, 于是自己动手写一个了.基本上可以充分利用已有的带宽

download_demo.gif

PS: 请自行安装swoole拓展和Guzzle http包.
直接上代码吧:

<?php

include 'vendor/autoload.php';
// $service = new Service('http://peterq.cn/movie/api/video_redirect?fid=543468589252145', __DIR__);
$service = new Service('http://peterq.cn/movie/api/video_redirect?fid=364402848596280', __DIR__);
$service->start();

use GuzzleHttp\Client;


class Service
{

    /**
     * @var Client;
     */
    protected $client;

    protected $worker_pool; // 下载进程池

    protected $available_worker_queue; // 可用的进程队列

    protected $worker_number = 16; // 定义需要开多少个进程, 文件较小时, 并不一定全部用得上, 取决于你的分片大小

    protected $started = false; // 是否已经开始下载

    protected $url; // 下载链接

    protected $length; // 文件大小

    protected $dir; // 保存目录

    protected $filename; // 文件绝对路径

    protected $downloaded = 0; // 已下载字节数

    protected $speedArr = []; // 用来计算下载速度的数组

    protected $distributed = 0; // 对于要下载的文件, 已经分配到哪个位置了

    public function __construct($url, $dir)
    {
        $this->url = $url;
        $this->dir = realpath($dir);
    }

    public function start()
    {
        if ($this->started) return;
        $this->available_worker_queue = new SplQueue();
        $this->started = true;
        // 创建客户端
        $this->client = new Client([
            'headers' => [
                "Accept"           => "application/json, text/javascript, text/html, */*; q=0.01",
                "Accept-Encoding"  => "gzip, deflate, sdch",
                "Accept-Language"  => "en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2",
                "Referer"          => "http://pan.baidu.com/disk/home",
                "X-Requested-With" => "XMLHttpRequest",
                "User-Agent"       => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36",
                "Connection"       => "keep-alive",
            ],
        ]);
        // 设置进程名称
        swoole_set_process_name('download-master');
        echo 'master pid:' . posix_getpid() . PHP_EOL;
        // 创建多个下载进程
        for ($i = 0; $i < $this->worker_number; $i++) {
            $process = $this->createProcess($i);
            $this->worker_pool[$i] = $process;
            // 通信通道加入事件轮训, 进行异步通信
            swoole_event_add($process->pipe, function ($pipe) use ($process) {
                $data = $process->read();
                $data = unserialize($data);
                $this->handleChildMessage($process, $data['type'], $data['data']);
            });
            $process->start();
            $this->available_worker_queue->enqueue($process);
        }

        // 子进程退出回收
        swoole_process::signal(SIGCHLD, function ($sig) {
            static $exited = 0;
            // 必须为false,非阻塞模式
            while ($ret = swoole_process::wait(false)) {
                echo "child process exited, PID={$ret['pid']}\n";
                $exited++;
                if ($exited == count($this->worker_pool)) exit();
            }
        });

        $this->initDownload();

    }

    // 初始化下载
    protected function initDownload()
    {
        $resp = $this->client->request('GET', $this->url, [
            'stream'          => true,
            'read_timeout'    => 10,
        ]);
        // 处理重定向
        while (in_array($resp->getStatusCode(), [301, 302])) {
            $this->url = $resp->getBody()->read(1024);
            dump('redirect: ' . $this->url);
            $resp = $this->client->request('GET', $this->url, [
                'stream'          => true,
                'read_timeout'    => 10,
            ]);
        }
        if (!$resp->getHeader('Content-Disposition')) {
            dump('not a file download url');
        }
        $this->length = intval($resp->getHeader('Content-Length')[0]);
        $fname = $resp->getHeader('Content-Disposition')[0];
        $fname = substr($fname, strpos($fname, 'filename=') + strlen('filename='));
        $fname = urldecode($fname);
        $this->filename = $this->dir . '/' . $fname;
        dump([
            '文件' => $this->filename,
            '大小' => round($this->length / 1024 / 1024, 2) . 'MB'
        ]);
        file_put_contents($this->filename, '');
        $this->download();
    }

    // 启动下载
    protected function download()
    {
        while (
            $this->distributed < $this->length
            && $this->available_worker_queue->count()
            && $process = $this->available_worker_queue->dequeue()) {
            $this->distributeSegment($process);
        }
    }

    // 分配下一块区间给一个进程
    protected function distributeSegment($process)
    {
        // 分成 1 MB 一个段去下载
        $size = 1 * 1024 * 1024;
        $process->write(serialize([
            'type' => 'new-segment',
            'data' => [
                'url' => $this->url,
                'file' => $this->filename,
                'start' => $this->distributed,
                'length' => min($size, $this->length - $this->distributed),
            ]
        ]));
        $this->distributed += $size;
    }

    // 进程间通信处理
    protected function handleChildMessage($process, $type, $data)
    {
        method_exists($this, 'on' . ucfirst($type)) and $this->{'on' . ucfirst($type)}($process, $data);
    }

    // 当下载进程下载一小块时, 通过此回调通知master进程
    protected function onRange(swoole_process $process, $data)
    {
        $this->downloaded += $data;
        static $lastClearTime = 0;
        $time = time();
        $this->speedArr[$time] = $this->speedArr[$time] ?? 0;
        $this->speedArr[$time] += $data;
        // 取过去 5 秒作为平均速度 作为速度显示, 粗略计算, 并不准确
        if ($time > $lastClearTime) {
            $lastClearTime = $time;
            foreach ($this->speedArr as $t => $size) {
                if ($t < $time - 5) unset($this->speedArr[$t]);
            }
        }
        $speed = array_sum($this->speedArr) / count($this->speedArr);
        $percent = $this->downloaded / $this->length * 100;
        $percent = round($percent, 2);
        $size = humanSize($this->downloaded);
        $speed = humanSize($speed);
        echo "\r\033[2K" . "已下载: $size, $percent%; 当前速度: $speed/s";
    }

    // 当分配给下载进程的下载任务完成时执行的回调
    protected function onTaskFinished($process, $data)
    {
        if ($this->distributed < $this->length)
            $this->distributeSegment($process);
        else {
            $this->available_worker_queue->enqueue($process);
            if ($this->available_worker_queue->count() == count($this->worker_pool)) {
                dump('文件下载完成');
                foreach ($this->worker_pool as $worker) {
                    $worker->write(serialize([
                        'type' => 'exit', 'data' => ''
                    ]));
                }
            }
        }
    }

    // 创建下载进程
    protected function createProcess($index = null)
    {
        $process = new swoole_process(function (swoole_process $process) use ($index) {
            swoole_set_process_name('download worker' . $index);
            echo sprintf('worker:%s, pid:%s', $index, posix_getpid()) . PHP_EOL;
            $downloader = null;
            // 通信通道加入事件轮训, 进行异步通信
            swoole_event_add($process->pipe, function ($pipe) use ($process, &$downloader) {
                $data = $process->read();
                $data = unserialize($data);
                $type = $data['type'];
                $data = $data['data'];
                // 这里会阻塞掉, 后续改进
                if ($type == 'new-segment') {
                    $downloader = new Downloader($process, $this->client, $data['url'], $data['file'], $data['start'], $data['length']);
                    $downloader->download();
                    $process->write(serialize([
                        'type' => 'taskFinished',
                        'data' => ''
                    ]));
                    $downloader = null;
                    return;
                }
                if ($type == 'exit') exit(0);
            });
        }, false, 2);

        return $process;
    }
}


// 下载器类
class Downloader
{

    protected $client; // guzzle实例

    protected $process; // 当前进程实例

    protected $file; // 文件名

    protected $url;

    protected $start; // 开始位置

    protected $length; // 下载长度

    protected $offset; // 已经下到哪一个位置了

    public function __construct(swoole_process $process, Client $client, $url, $file, $start, $length)
    {
        $this->process = $process;
        $this->client = $client;
        $this->url = $url;
        $this->file = $file;
        $this->start = $start;
        $this->length = $length;
    }


    public function download()
    {
        $this->offset = $this->start;
        $res = fopen($this->file, 'rb+');
        fseek($res, $this->start, SEEK_SET);
        $resp = $this->client->request('GET', $this->url, [
            'stream' => true,
            'headers' => [
                'Range' => 'bytes=' . $this->start . '-' . ($this->start + $this->length)
            ]
        ]);
        $loaded = 0;
        while (!$resp->getBody()->eof()) {
            // 5 kb 的下载
            $size = 1024 * 5;
            $data = $resp->getBody()->read($size);
            $loaded += strlen($data);
            fwrite($res, $data);
            $this->process->write(serialize([
                'type' => 'range',
                'data' => strlen($data)
            ]));
            if ($loaded >= $this->length) break; // eof 貌似不起作用, 手动退出
        }
        fclose($res);
        dump($this->length / 1024 / 1024 . 'MB下载完成');
    }
}

// 把文件大小从字节转换为合适的单位
function humanSize($size) {
    $units = ['B', 'KB', 'MB', 'GB'];
    foreach ($units as $unit) {
        if ($size > 1024)
            $size /= 1024;
        else break;
    }
    return round($size, 2) . $unit;
}


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • http://www.360doc.com/content/14/0508/23/4891348_37594709...
    Albert新荣阅读 29,128评论 0 32
  • 自恃小景攀西子,对面柳承近岸风。涟漪不及芦苇丛,春来潭水妆新衡。
    将相河阅读 177评论 0 3
  • 今天去博印堂參加分享會,心裡不得不感歎這是一隻充滿責任感、使命感的團隊! 這是一隻年輕的團隊,他們每個人的內心都住...
    夢瑤阅读 231评论 0 0
  • 今天,我们在昨天聊到的学会止盈的基础上再细聊一下定投如何止盈。 定投止盈看起来很难,但最终其实就是两个问题。 1、...
    有斗阅读 1,208评论 0 4
  • 自从人工智能诞生以来,人类似乎就没停止过对于人工智能未来的担忧,甚至包括霍金。这也从侧面佐证了,脑洞大开的编剧们在...
    比持卡丘阅读 216评论 0 0