Swoole实验室:7-使用Websocket上传文件(1)

平时我们上传文件使用的是HTTP方式上传,今天我来给大家分享一下使用HTML5的websocket方式上传文件,后端使用Swoole的Websocket模块接收处理客户端上传的数据并保存为文件。本文实例是一个基础实例,后面我会专门给大家讲解更复杂更实战的文件上传实例。

服务端

我们继续使用Swoole实验室:1-使用Composer构建项目构建好的项目,新建文件\src\App\Uploader.php:

<?php 
namespace Helloweba\Swoole;

use swoole_websocket_server;

class Uploader
{
    protected $ws;
    protected $host = '0.0.0.0';
    protected $port = 9505;
    // 进程名称
    protected $taskName = 'swooleUploader';
    // PID路径
    protected $pidFile = '/run/swooleUploader.pid';
    // 设置运行时参数
    protected $options = [
        'worker_num' => 4, //worker进程数,一般设置为CPU数的1-4倍  
        'daemonize' => true, //启用守护进程
        'log_file' => '/data/log/swoole.log', //指定swoole错误日志文件
        'log_level' => 3, //日志级别 范围是0-5,0-DEBUG,1-TRACE,2-INFO,3-NOTICE,4-WARNING,5-ERROR
        'dispatch_mode' => 1, //数据包分发策略,1-轮询模式
    ];
 

    public function __construct($options = [])
    {
        $this->ws = new swoole_websocket_server($this->host, $this->port);

        if (!empty($options)) {
            $this->options = array_merge($this->options, $options);
        }
        $this->ws->set($this->options);

        $this->ws->on("open", [$this, 'onOpen']);
        $this->ws->on("message", [$this, 'onMessage']);
        $this->ws->on("close", [$this, 'onClose']);
    }

    public function start()
    {
        // Run worker
        $this->ws->start();
    }

    public function onOpen(swoole_websocket_server $ws, $request)
    {
        // 设置进程名
        cli_set_process_title($this->taskName);
        //记录进程id,脚本实现自动重启
        $pid = "{$ws->master_pid}\n{$ws->manager_pid}";
        file_put_contents($this->pidFile, $pid);

        echo "server: handshake success with fd{$request->fd}\n";
        $msg = '{"msg": "connect ok"}';
        $ws->push($request->fd, $msg);
    }

    public function onMessage(swoole_websocket_server $ws, $frame)
    {
        $opcode = $frame->opcode;
        if ($opcode == 0x08) {
            echo "Close frame received: Code {$frame->code} Reason {$frame->reason}\n";
        } else if ($opcode == 0x1) {
            echo "Text string\n";
        } else if ($opcode == 0x2) {
            echo "Binary data\n"; //
        } else {
            echo "Message received: {$frame->data}\n";
        }
        $filename = './files/aaa.jpg';
        file_put_contents($filename, $frame->data);
        echo "file path : {$filename}\n";
        $ws->push($frame->fd, 'upload success');
    }
    public function onClose($ws, $fid)
    {
        echo "client {$fid} closed\n";
        foreach ($ws->connections as $fd) {
            $ws->push($fd, $fid. '已断开!');
        }
    }
}
知识兔

$frame->opcode,WebSocket的OpCode类型,可以通过它来判断传输的数据是文本内容还是二进制数据。

新建public/uploadServer.php,用于启动服务端脚本:

<?php 
require dirname(__DIR__) . '/vendor/autoload.php';

use Helloweba\Swoole\Uploader;

$opt = [
    'daemonize' => false
];
$ws = new Uploader($opt);
$ws->start();
知识兔

客户端

在本地站点建立客户端文件upload.html。只需在页面中放置一个文件选择控件和一个用于输出上传信息的div#log。

<input type="file" id="myFile">
<div id="log"></div>
知识兔

当选择好本地文件后,触发onchange事件,这个时候客户端尝试与服务端建立websocket连接,然后开始读取本地文件,读取完成后将数据发送给服务端。

$('#myFile').on('change', function(event) {
    var ws = new WebSocket("ws://192.168.1.31:9505");

    ws.onopen = function() {
        log('已连接上!');
    }
    ws.onmessage = function(e) {
        log("收到服务器消息:" + e.data + "'\n");
        if (e.data == 'connect ok') {
            log('开始上传文件');
        } 
        if (e.data == 'upload success') {
            log('上传完成');
            ws.close();
        } else {
            var file = document.getElementById("myFile").files[0];

            var reader = new FileReader();
            reader.readAsArrayBuffer(file);

            reader.onload = function(e) {
                ws.send(e.target.result);
                log('正在上传数据...');
            }
        }
    }
    ws.onclose = function() {
        console.log('连接已关闭!');
        log('连接已关闭!');
    }
});
//在消息框中打印内容
function log(text) {
    $("#log").append(text+"<br/>");
}
知识兔

这里讲一下HTML5的FileReader 对象,FileReader允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。FileReader提供了几种读取文件的方法:

reader.readAsArrayBuffer(file|blob):用于启动读取指定的 Blob 或 File 内容。读取文件后,会在内存中创建一个ArrayBuffer对象(二进制缓冲区),将二进制数据存放在其中。当读取操作完成时,readyState 变成 DONE(已完成),并触发 loadend 事件,同时 result 属性中将包含一个 ArrayBuffer 对象以表示所读取文件的数据。通过此方式,可以直接在网络中传输二进制内容。此外对于大文件我们可以分段读取二进制内容上传。

reader.readAsDataURL(file|blob):该方法会读取指定的 Blob 或 File 对象。读取操作完成的时候,readyState 会变成已完成(DONE),并触发 loadend 事件,同时 result 属性将包含一个data:URL格式的字符串(base64编码)以表示所读取文件的内容。

FileReader.readAsText(file|blob):可以将 Blob 或者 File 对象转根据特殊的编码格式转化为内容(字符串形式)。当转化完成后, readyState 这个参数就会转换 为 done 即完成态, event("loadend") 挂载的事件会被触发,并可以通过事件返回的形参得到中的 FileReader.result 属性得到转化后的结果。

FileReader.readAsBinaryString():读取文件内容为二进制字符串,已废除,不要用了。

实验

运行服务端

php uploadServer.php
知识兔

运行客户端

在本地站点目录,打开upload.html。选择图片上传,即刻显示如下信息:

202203131823176671680000

查看服务端输出:

202203131823191186060001

这时候检查服务器上对应目录下会出现一个aaa.jpg的文件。

Swoole也提供了Websocket客户端,可以使用websocket在不同服务端传输文件。

我们会发现使用Websocket确实把文件上传成功,但是实验中并没有考虑大文件的上传,加入有一个很大的日志文件或者视频文件需要上传到服务器上,那就需要采取分片上传,并考虑断点上传的问题,在下一节文章实验中,我们将具体探讨使用Websocket上传大文件,敬请关注。

计算机