WebSocket\Server
?> 通过内置的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章节。
面向过程代码
$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
客户端 - 异步IO 的
PHP
程序中可以使用 Swoole\Coroutine\Http 作为WebSocket
客户端 Apache/PHP-FPM
或其他同步阻塞的PHP
程序中可以使用swoole/framework
提供的同步WebSocket客户端- 非
WebSocket
客户端不能与WebSocket
服务器通信
如何判断连接是否为WebSocket客户端
?> 通过使用 $server->connection_info($fd) 获取连接信息,返回的数组中有一项为 websocket_status,根据此状态可以判断是否为WebSocket
客户端。
事件
?> WebSocket
服务器除了接收 Swoole\Server 和Swoole\Http\Server基类的回调函数外,额外增加了3
个回调函数设置。其中:
onMessage
回调函数为必选onOpen
和onHandShake
回调函数为可选
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);
onMessage
?> 当服务器收到来自客户端的数据帧时会回调此函数。
onMessage(Swoole\Websocket\Server $server, Swoole\Websocket\Frame $frame)
提示
$frame
是swoole_websocket_frame对象,包含了客户端发来的数据帧信息onMessage
回调必须被设置,未设置服务器将无法启动- 客户端发送的
ping
帧不会触发onMessage
,底层会自动回复pong
包
swoole_websocket_frame
属性 | 说明 |
---|---|
$frame->fd | 客户端的socket id ,使用$server->push 推送数据时需要用到 |
$frame->data | 数据内容,可以是文本内容也可以是二进制数据,可以通过opcode 的值来判断 |
$frame->opcode | WebSocket 的OpCode 类型,可以参考WebSocket 协议标准文档 |
$frame->finish | 表示数据帧是否完整,一个WebSocket 请求可能会分成多个数据帧进行发送(底层已经实现了自动合并数据帧,现在不用担心接收到的数据帧不完整) |
!> $frame->data
如果是文本类型,编码格式必然是UTF-8
,这是WebSocket
协议规定的
- OpCode与数据类型
OpCode | 数据类型 |
---|---|
WEBSOCKET_OPCODE_TEXT = 0x1 | 文本数据 |
WEBSOCKET_OPCODE_BINARY = 0x2 | 二进制数据 |
方法
WebSocket\Server
是 Server 的子类,因此可以调用Server
的全部方法。
需要注意WebSocket
服务器向客户端发送数据应当使用WebSocket\Server::push
方法,此方法会进行WebSocket
协议打包。而 Server::send 方法是原始的TCP
发送接口。
WebSocket\Server::disconnect()
方法可以从服务端主动关闭一个WebSocket
连接,可以指定状态码(根据WebSocket
协议,可使用的状态码为十进制的一个整数,取值可以是1000
或4000-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
型)改为flags
(int
型)以支持Websocket
压缩,finish
对应SWOOLE_WEBSOCKET_FLAG_FIN
值为1
,原有bool
型值会隐式转换为int
型,此改动向下兼容无影响。 此外压缩flag
为SWOOLE_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
- 功能:消息内容
- 默认值:无
- 其它值:无
返回值
- 解析失败返回
false
,解析成功返回Swoole\WebSocket\Frame对象
- 解析失败返回
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
,对于应用程序关闭连接状态码,取值范围为1000
或4000-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_TEXT | 0x1 | UTF-8文本字符数据 |
WEBSOCKET_OPCODE_BINARY | 0x2 | 二进制数据 |
WEBSOCKET_OPCODE_PING | 0x9 | ping类型数据 |
连接状态
常量 | 对应值 | 说明 |
---|---|---|
WEBSOCKET_STATUS_CONNECTION | 1 | 连接进入等待握手 |
WEBSOCKET_STATUS_HANDSHAKE | 2 | 正在握手 |
WEBSOCKET_STATUS_FRAME | 3 | 已握手成功等待浏览器发送数据帧 |
选项
?> WebSocket\Server
是Server
的子类,可以使用Server::set()方法传入配置选项,设置某些参数。
websocket_subprotocol
?> 设置WebSocket
子协议。
?> 设置后握手响应的Http
头会增加Sec-WebSocket-Protocol: {$websocket_subprotocol}
。具体使用方法请参考WebSocket
协议相关RFC
文档。
$server->set([
'websocket_subprotocol' => 'chat',
]);
open_websocket_close_frame
?> 启用websocket
协议中关闭帧(opcode
为0x08
的帧)在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
对象具有以下属性, 多了code
和reason
属性, 记录了关闭的错误代码和原因
如果服务端需要接收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 );