当前位置: 首页 > 文档资料 > Swoole 中文文档 >

WebSocket\Server

优质
小牛编辑
131浏览
2023-12-01

?> 通过内置的WebSocket服务器支持,通过几行PHP代码就可以写出一个异步IO的多进程的WebSocket服务器。

$server = new Swoole\WebSocket\Server("0.0.0.0", 9501);

$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
    echo "server: handshake success with fd{$request->fd}\n";
});

$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
    $server->push($frame->fd, "this is server");
});

$server->on('close', function ($ser, $fd) {
    echo "client {$fd} closed\n";
});

$server->start();
  • onRequest回调

    ?> WebSocket\Server继承自Http\Server,所以Http\Server提供的所有API和配置项都可以使用。请参考Http\Server章节。

    • 设置了onRequest回调,WebSocket\Server也可以同时作为http服务器
    • 未设置onRequest回调,WebSocket\Server收到http请求后会返回http 400错误页面
    • 如果想通过接收http触发所有websocket的推送,需要注意作用域的问题,面向过程请使用globalWebSocket\Server进行引用,面向对象可以把WebSocket\Server设置成一个成员属性

面向过程代码

$server = new Swoole\WebSocket\Server("0.0.0.0", 9501);
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
    echo "server: handshake success with fd{$request->fd}\n";
});
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
    $server->push($frame->fd, "this is server");
});
$server->on('close', function ($ser, $fd) {
    echo "client {$fd} closed\n";
});
$server->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
    global $server;//调用外部的server
    // $server->connections 遍历所有websocket连接用户的fd,给所有用户推送
    foreach ($server->connections as $fd) {
        // 需要先判断是否是正确的websocket连接,否则有可能会push失败
        if ($server->isEstablished($fd)) {
            $server->push($fd, $request->get['message']);
        }
    }
});
$server->start();

面向对象代码

class WebsocketTest
{
    public $server;

    public function __construct()
    {
        $this->server = new Swoole\WebSocket\Server("0.0.0.0", 9501);
        $this->server->on('open', function (Swoole\WebSocket\Server $server, $request) {
            echo "server: handshake success with fd{$request->fd}\n";
        });
        $this->server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
            echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
            $server->push($frame->fd, "this is server");
        });
        $this->server->on('close', function ($ser, $fd) {
            echo "client {$fd} closed\n";
        });
        $this->server->on('request', function ($request, $response) {
            // 接收http请求从get获取message参数的值,给用户推送
            // $this->server->connections 遍历所有websocket连接用户的fd,给所有用户推送
            foreach ($this->server->connections as $fd) {
                // 需要先判断是否是正确的websocket连接,否则有可能会push失败
                if ($this->server->isEstablished($fd)) {
                    $this->server->push($fd, $request->get['message']);
                }
            }
        });
        $this->server->start();
    }
}

new WebsocketTest();
  • 客户端

    • Chrome/Firefox/高版本IE/Safari等浏览器内置了JS语言的WebSocket客户端
    • 微信小程序开发框架内置的WebSocket客户端
    • 异步IOPHP程序中可以使用 Swoole\Coroutine\Http 作为WebSocket客户端
    • Apache/PHP-FPM或其他同步阻塞的PHP程序中可以使用swoole/framework提供的同步WebSocket客户端
    • WebSocket客户端不能与WebSocket服务器通信
  • 如何判断连接是否为WebSocket客户端

?> 通过使用 $server->connection_info($fd) 获取连接信息,返回的数组中有一项为 websocket_status,根据此状态可以判断是否为WebSocket客户端。

事件

?> WebSocket服务器除了接收 Swoole\ServerSwoole\Http\Server基类的回调函数外,额外增加了3个回调函数设置。其中:

  • onMessage回调函数为必选
  • onOpenonHandShake回调函数为可选

onHandShake

?> WebSocket建立连接后进行握手。WebSocket服务器会自动进行handshake握手的过程,如果用户希望自己进行握手处理,可以设置onHandShake事件回调函数。

onHandShake(Swoole\Http\Request $request, Swoole\Http\Response $response);
  • 提示

    • onHandShake事件回调是可选的
    • 设置onHandShake回调函数后不会再触发onOpen事件,需要应用代码自行处理
    • onHandShake中必须调用 response->status() 设置状态码为101并调用response->end()响应, 否则会握手失败.
    • 内置的握手协议为Sec-WebSocket-Version: 13,低版本浏览器需要自行实现握手
    • 可以使用server->defer调用onOpen逻辑
  • 注意

    !> 仅仅你需要自行处理handshake的时候再设置这个回调函数,如果您不需要“自定义”握手过程,那么不要设置该回调,用Swoole默认的握手即可。下面是“自定义”handshake事件回调函数中必须要具备的:

    $server->on('handshake', function (\swoole_http_request $request, \swoole_http_response $response) {
        // print_r( $request->header );
        // if (如果不满足我某些自定义的需求条件,那么返回end输出,返回false,握手失败) {
        //    $response->end();
        //     return false;
        // }
    
        // websocket握手连接算法验证
        $secWebSocketKey = $request->header['sec-websocket-key'];
        $patten          = '#^[+/0-9A-Za-z]{21}[AQgw]==$#';
        if (0 === preg_match($patten, $secWebSocketKey) || 16 !== strlen(base64_decode($secWebSocketKey))) {
            $response->end();
            return false;
        }
        echo $request->header['sec-websocket-key'];
        $key = base64_encode(sha1(
            $request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
            true
        ));
    
        $headers = [
            'Upgrade'               => 'websocket',
            'Connection'            => 'Upgrade',
            'Sec-WebSocket-Accept'  => $key,
            'Sec-WebSocket-Version' => '13',
        ];
    
        // WebSocket connection to 'ws://127.0.0.1:9502/'
        // failed: Error during WebSocket handshake:
        // Response must not include 'Sec-WebSocket-Protocol' header if not present in request: websocket
        if (isset($request->header['sec-websocket-protocol'])) {
            $headers['Sec-WebSocket-Protocol'] = $request->header['sec-websocket-protocol'];
        }
    
        foreach ($headers as $key => $val) {
            $response->header($key, $val);
        }
    
        $response->status(101);
        $response->end();
    });

onOpen

?> WebSocket客户端与服务器建立连接并完成握手后会回调此函数。

onOpen(Swoole\Websocket\Server $server, Swoole\Http\Request $request);
  • 提示

  • $request 是一个Http请求对象,包含了客户端发来的握手请求信息

  • onOpen事件函数中可以调用 push 向客户端发送数据或者调用 close 关闭连接

  • onOpen事件回调是可选的

onMessage

?> 当服务器收到来自客户端的数据帧时会回调此函数。

onMessage(Swoole\Websocket\Server $server, Swoole\Websocket\Frame $frame)
  • 提示

    • $frameswoole_websocket_frame对象,包含了客户端发来的数据帧信息
    • onMessage回调必须被设置,未设置服务器将无法启动
    • 客户端发送的ping帧不会触发onMessage,底层会自动回复pong
  • swoole_websocket_frame

属性说明
$frame->fd客户端的socket id,使用$server->push推送数据时需要用到
$frame->data数据内容,可以是文本内容也可以是二进制数据,可以通过opcode的值来判断
$frame->opcodeWebSocketOpCode类型,可以参考WebSocket协议标准文档
$frame->finish表示数据帧是否完整,一个WebSocket请求可能会分成多个数据帧进行发送(底层已经实现了自动合并数据帧,现在不用担心接收到的数据帧不完整)

!> $frame->data 如果是文本类型,编码格式必然是UTF-8,这是WebSocket协议规定的

  • OpCode与数据类型
OpCode数据类型
WEBSOCKET_OPCODE_TEXT = 0x1文本数据
WEBSOCKET_OPCODE_BINARY = 0x2二进制数据

方法

WebSocket\ServerServer 的子类,因此可以调用Server的全部方法。

需要注意WebSocket服务器向客户端发送数据应当使用WebSocket\Server::push方法,此方法会进行WebSocket协议打包。而 Server::send 方法是原始的TCP发送接口。

WebSocket\Server::disconnect()方法可以从服务端主动关闭一个WebSocket连接,可以指定状态码(根据WebSocket协议,可使用的状态码为十进制的一个整数,取值可以是10004000-4999)和关闭原因(采用utf-8编码、字节长度不超过125的字符串)。在未指定情况下状态码为1000,关闭原因为空。

push

?> websocket客户端连接推送数据,长度最大不得超过2M

Swoole\WebSocket\Server->push(int $fd, string $data, int $opcode = 1, bool $finish = true): bool;
  • 参数

    !> Swoole >= v4.2.0 传入的$data 如果是 Swoole\WebSocket\Frame 对象则其后续参数会被忽略

    • int $fd

      • 功能:客户端连接的ID 【如果指定的$fd对应的TCP连接并非websocket客户端,将会发送失败】
      • 默认值:无
      • 其它值:无
    • string $data

      • 功能:要发送的数据内容
      • 默认值:无
      • 其它值:无
    • int $opcode

      • 功能:指定发送数据内容的格式 【默认为文本。发送二进制内容$opcode参数需要设置为WEBSOCKET_OPCODE_BINARY
      • 默认值WEBSOCKET_OPCODE_TEXT
      • 其它值WEBSOCKET_OPCODE_BINARY
    • bool $finish

      • 功能:是否发送完成
      • 默认值true
      • 其它值false

      ?> 自v4.4.12版本起,finish参数(bool型)改为flagsint型)以支持Websocket压缩,finish对应SWOOLE_WEBSOCKET_FLAG_FIN值为1,原有bool型值会隐式转换为int型,此改动向下兼容无影响。 此外压缩flagSWOOLE_WEBSOCKET_FLAG_COMPRESS

exist

?> 判断WebSocket客户端是否存在,并且状态为Active状态。

!> v4.3.0以后, 此API仅用于判断连接是否存在, 请使用isEstablished判断是否为websocket连接

Swoole\WebSocket\Server->exist(int $fd): bool;
  • 返回值

    • 连接存在,并且已完成WebSocket握手,返回true
    • 连接不存在或尚未完成握手,返回false

pack

?> 打包WebSocket消息。

Swoole\WebSocket\Server::pack(string $data, int $opcode = 1, bool $finish = true, bool $mask = false): string;
  • 参数

    • string $data

      • 功能:消息内容
      • 默认值:无
      • 其它值:无
    • int $opcode

      • 功能:指定发送数据内容的格式 【默认为文本。发送二进制内容$opcode参数需要设置为WEBSOCKET_OPCODE_BINARY
      • 默认值WEBSOCKET_OPCODE_TEXT
      • 其它值WEBSOCKET_OPCODE_BINARY
    • bool $finish

      • 功能:帧是否完成
      • 默认值:无
      • 其它值:无
    • bool $mask

      • 功能:是否设置掩码
      • 默认值:无
      • 其它值:无
  • 返回值

    • 返回打包好的WebSocket数据包,可通过Swoole\Server基类的send()发送给对端

示例:

    $ws = new swoole_server('127.0.0.1', 9501 , SWOOLE_BASE);
    $ws->set(array(
        'log_file' => '/dev/null'
    ));
    $ws->on('WorkerStart', function (\swoole_server $serv) {
    });

    $ws->on('receive', function ($serv, $fd, $threadId, $data) {
        $sendData = "HTTP/1.1 101 Switching Protocols\r\n";
        $sendData .= "Upgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: IFpdKwYy9wdo4gTldFLHFh3xQE0=\r\n";
        $sendData .= "Sec-Websocket-Version: 13\r\nServer: swoole-http-server\r\n\r\n";
        $sendData .= swoole_websocket_server::pack("hello world\n");
        $serv->send($fd, $sendData);
    });

    $ws->start();

unpack

?> 解析WebSocket数据帧。

Swoole\WebSocket\Server::unpack(string $data): Swoole\WebSocket\Frame|false;
  • 参数

    • string $data
      • 功能:消息内容
      • 默认值:无
      • 其它值:无
  • 返回值

disconnect

?> 主动向websocket客户端发送关闭帧并关闭该连接。

!> Swoole >= v4.0.3

Swoole\WebSocket\Server->disconnect(int $fd, int $code = 1000, string $reason = ""): bool;
  • 参数

    • int $fd

      • 功能:客户端连接的ID【如果指定的$fd对应的TCP连接并非websocket客户端,将会发送失败】
      • 默认值:无
      • 其它值:无
    • int $code

      • 功能:关闭连接的状态码【根据RFC6455,对于应用程序关闭连接状态码,取值范围为10004000-4999之间】
      • 默认值:无
      • 其它值:无
    • string $reason

      • 功能:关闭连接的原因【utf-8格式字符串,字节长度不超过125
      • 默认值:无
      • 其它值:无
  • 返回值

    • 发送成功返回true,发送失败或状态码非法时返回false

isEstablished

?> 检查连接是否为有效的WebSocket客户端连接。

?> 此函数与exist方法不同,exist方法仅判断是否为TCP连接,无法判断是否为已完成握手的WebSocket客户端。

Swoole\WebSocket\Server->isEstablished(int $fd): bool;
  • 参数

    • int $fd
      • 功能:客户端连接的ID【如果指定的$fd对应的TCP连接并非websocket客户端,将会发送失败】
      • 默认值:无
      • 其它值:无

常量

数据帧类型

常量对应值说明
WEBSOCKET_OPCODE_TEXT0x1UTF-8文本字符数据
WEBSOCKET_OPCODE_BINARY0x2二进制数据
WEBSOCKET_OPCODE_PING0x9ping类型数据

连接状态

常量对应值说明
WEBSOCKET_STATUS_CONNECTION1连接进入等待握手
WEBSOCKET_STATUS_HANDSHAKE2正在握手
WEBSOCKET_STATUS_FRAME3已握手成功等待浏览器发送数据帧

选项

?> WebSocket\ServerServer的子类,可以使用Server::set()方法传入配置选项,设置某些参数。

websocket_subprotocol

?> 设置WebSocket子协议。

?> 设置后握手响应的Http头会增加Sec-WebSocket-Protocol: {$websocket_subprotocol}。具体使用方法请参考WebSocket协议相关RFC文档。

$server->set([
    'websocket_subprotocol' => 'chat',
]);

open_websocket_close_frame

?> 启用websocket协议中关闭帧(opcode0x08的帧)在onMessage回调中接收,默认为false

?> 开启后,可在Websocket\Server中的onMessage回调中接收到客户端或服务端发送的关闭帧,开发者可自行对其进行处理。

$server = new Swoole\Websocket\Server("0.0.0.0", 9501);
$server->set(array("open_websocket_close_frame" => true));
$server->on('open', function (Swoole\Websocket\Server $server, $request) {
});

$server->on('message', function (Swoole\Websocket\Server $server, $frame) {
    if ($frame->opcode == 0x08) {
        echo "Close frame received: Code {$frame->code} Reason {$frame->reason}\n";
    } else {
        echo "Message received: {$frame->data}\n";
    }
});

$server->on('close', function ($ser, $fd) {
});

$server->start();

websocket_compression

?> 启用数据压缩

?> 为true时允许对帧进行zlib压缩,具体是否能够压缩取决于客户端是否能够处理压缩(根据握手信息决定,参见RFC-7692) 需要配合flags参数SWOOLE_WEBSOCKET_FLAG_COMPRESS来真正地对具体的某个帧进行压缩,具体使用方法见此节

!> Swoole >= v4.4.12

其他

Swoole\WebSocket\Frame

?> 在v4.2.0版本中, 新增了服务端和客户端发送Swoole\WebSocket\Frame对象的支持
v4.4.12版本中,新增了flags属性以支持WebSocket压缩帧,同时增加了一个新的子类Swoole\WebSocket\CloseFrame

一个普通的frame对象具有以下属性

object(Swoole\WebSocket\Frame)#1 (4) {
  ["fd"]      =>  int(0)
  ["data"]    =>  NULL
  ["opcode"]  =>  int(1)
  ["finish"]  =>  bool(true)
}

一个普通的close frame对象具有以下属性, 多了codereason属性, 记录了关闭的错误代码和原因

如果服务端需要接收close frame, 需要通过$server->set开启open_websocket_close_frame参数

object(Swoole\WebSocket\CloseFrame)#1 (6) {
  ["fd"]      =>  int(0)
  ["data"]    =>  NULL
  ["finish"]  =>  bool(true)
  ["opcode"]  =>  int(8)
  ["code"]    =>  int(1000)
  ["reason"]  =>  string(0) ""
}

在用于发送时, fd属性会被忽略(因为服务器端fd是第一个参数, 客户端无需指定fd), 所以fd是一个只读属性

相关示例代码可以在 Websocket 单元测试 中找到

WebSocket帧压缩 (RFC-7692)

?> 首先你需要配置'websocket_compression' => true来启用压缩(websocket握手时将与对端交换压缩支持信息) 而后,你可以使用flag SWOOLE_WEBSOCKET_FLAG_COMPRESS 来对具体的某个帧进行压缩

  • 示例

    • 服务端
    use Swoole\WebSocket\Frame;
    use Swoole\WebSocket\Server;
    
    $server = new Server('127.0.0.1', 9501);
    $server->set(['websocket_compression' => true]);
    $server->on('message', function (Server $server, Frame $frame) {
        $server->push(
            $frame->fd,
            'Hello Swoole',
            SWOOLE_WEBSOCKET_OPCODE_TEXT,
            SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS
        );
        // $server->push($frame->fd, $frame); // 或者 服务端可以直接原封不动转发客户端的帧对象
    });
    $server->start();
    • 客户端
    use \Swoole\Coroutine\Http\Client;
    
    $cli = Client('127.0.0.1', 9501);
    $cli->set(['websocket_compression' => true]);
    $cli->upgrade('/');
    $cli->push(
        'Hello Swoole',
        SWOOLE_WEBSOCKET_OPCODE_TEXT,
        SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS
    );