最近在用 Workerman 学习 WebSocket 的时候,遇到了一个问题:
var ws = new WebSocket('ws://' + document.domain + ':8081', 'encrypt');
结果浏览器报了这个错:
Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was receive
查阅了 RFC文档 得知问题在于新建WebSocket时的第二个参数,表示客户端接受的子协议。
子协议是什么?比如浏览器向服务端发起两个WebSocket连接,一条连接使用明文传输数据,另一条使用加密传输数据。
服务端可以开启两个端口分别处理明文和加密连接。也可以只开一个端口,通过客户端提供的子协议来判断该连接传输数据的方式。
那么新建WebSocket时的子协议参数,最终是以什么形式发送到服务端的?就藏在请求Header的 Sec-Websocket-Protocol
中:
服务端接收到 Sec-Websocket-Protocol
后,必须在响应 Header 中也包含 Sec-Websocket-Protocol
,取值和客户端传来的一样。否则浏览器就会报错:Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was receive
再提一点,子协议参数可以是个数组,比如:
var ws = new WebSocket('ws://' + document.domain + ':8081', ['text', 'json']);
表示该WebSocket连接既接收字符格式的数据,也可以接收JSON格式的数据,这时服务端必须在两者中选择其一返回给浏览器。
有点扯远了,今天的主角是 Workerman。Workerman 要获得子协议参数,需要在 onConnect 回调中设置 onWebSocketConnect 回调:
$ws = new Worker('websocket://0.0.0.0:8081');
$ws->onConnect = function (TcpConnection $connection) {
$connection->onWebSocketConnect = function (TcpConnection $connection, $header) {
if (isset($_SERVER['HTTP_SEC_WEBSOCKET_PROTOCOL'])) {
$protocols = explode(',', $_SERVER['HTTP_SEC_WEBSOCKET_PROTOCOL']);
$connection->headers = [
'Sec-WebSocket-Protocol: ' . $protocols[0],
];
}
};
};
然后通过 $_SERVER['HTTP_SEC_WEBSOCKET_PROTOCOL']
获得浏览器发来的子协议参数,是个字符串。如果浏览器接受多个子协议,那么协议之间会以 逗号 分隔。方便起见,例子中总是返回第一种子协议。
服务端响应时,将 Sec-Websocket-Protocol
字段放入 $connection->headers 数组中,这样就完成了子协议的协商。