WebSocket 通信中会使用到一串key(GUID) 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
在 RFC 6455 中说明这是一组 GUID , 用于使用传输协议的安全验证
HTTP 请求头
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: null
Sec-WebSocket-Key: sB8aRrC/n9NdMaecy2XJFQ==
Sec-WebSocket-Version: 13
首先将 Sec-WebSocket-Key 浏览器随机生成的 Base64编码
加上 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 进行拼接,如下:
sB8aRrC/n9NdMaecy2XJFQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
将拼接的字符以sha1 加密后,经过 base64编码再返回给请求端。
以下为swoole项目中,websocket握手的相关处理代码:
<?php
/**
* Created by PhpStorm.
* User: Apple
* Date: 2018/11/1 0001
* Time: 14:47
*/
namespace App\WebSocket;
use EasySwoole\Component\Di;
use EasySwoole\EasySwoole\ServerManager;
use EasySwoole\EasySwoole\Logger;
use EasySwoole\Socket\Bean\Caller;
/**
* Class WebSocketEvent
*
* 此类是 WebSocet 中一些非强制的自定义事件处理
*
* @package App\WebSocket
*/
class WebSocketEvent
{
/**
* 握手事件
*
* @param \swoole_http_request $request
* @param \swoole_http_response $response
* @return bool
*/
public function onHandShake(\swoole_http_request $request, \swoole_http_response $response)
{
/** 此处自定义握手规则 返回 false 时中止握手 */
if (!$this->customHandShake($request, $response)) {
$response->end();
return false;
}
/** 此处是 RFC规范中的WebSocket握手验证过程 必须执行 否则无法正确握手 */
if ($this->secWebsocketAccept($request, $response)) {
$response->end();
return true;
}
$response->end();
return false;
}
/**
* 自定义握手事件
*
* @param \swoole_http_request $request
* @param \swoole_http_response $response
* @return bool
*/
protected function customHandShake(\swoole_http_request $request, \swoole_http_response $response): bool
{
/**
* 这里可以通过 http request 获取到相应的数据
* 进行自定义验证后即可
* (注) 浏览器中 JavaScript 并不支持自定义握手请求头 只能选择别的方式 如get参数
*/
$headers = $request->header;
$cookie = $request->cookie;
// if (如果不满足我某些自定义的需求条件,返回false,握手失败) {
// return false;
// }
return true;
}
/**
* 当客户端连接上服务端,执行该方法
* @author Jeffrey
* @param \swoole_server $server
* @param \swoole_http_request $request
* @date 2019年12月14日
*/
public function onOpen(\swoole_server $server, \swoole_http_request $request) {
Logger::getInstance()->console('有客户端连接, 客户端ID是' . $request->fd . ", 客户端IP是".$request->header['x-real_ip']);
$errordi = Di::getInstance()->get("error");
$code = $errordi->getvalue("E_CONNECTED", 'code');
$message = $errordi->getvalue("E_CONNECTED", 'error_name');
$msg = json_encode([
"code" => $code,
"message" => $message
]);
$server->push($request->fd, $msg);
// TODO...此处可以为用户绑定其他操作
}
/**
* 关闭事件
*
* @param \swoole_server $server
* @param int $fd
* @param int $reactorId
*/
public function onClose(\swoole_server $server, int $fd, int $reactorId)
{
/** @var array $info */
$info = $server->getClientInfo($fd);
/**
* 判断此fd 是否是一个有效的 websocket 连接
* 参见 https://wiki.swoole.com/wiki/page/490.html
*/
if ($info && $info['websocket_status'] === WEBSOCKET_STATUS_FRAME) {
/**
* 判断连接是否是 server 主动关闭
* 参见 https://wiki.swoole.com/wiki/page/p-event/onClose.html
*/
if ($reactorId < 0) {
Logger::getInstance()->console('服务端关闭连接, 客户端ID ' . $fd . ", 客户端IP:".$info['remote_ip']);
echo "server close \n";
} else {
Logger::getInstance()->console('客户端断开连接, 客户端ID ' . $fd . ", 客户端IP:".$info['remote_ip']);
echo "client close \n";
}
/*以下逻辑是下线操作,下线时读取每个控制器的offline,如果控制器里有offline方法则执行,如果没有则不执行该方法*/
$routedi = Di::getInstance()->get("route");
$routeall = $routedi->getAll();
foreach ($routeall as $key => $value) {
if ($key == 'ping') continue;
$class = '\\App\\WebSocket\\'. ucfirst($value['0']);
if (class_exists($class)) {
$example = new $class;
if (method_exists($example, "offline")) {
$example->offline($fd);
}
}
}
}
}
/**
* RFC规范中的WebSocket握手验证过程
* 以下内容必须强制使用
*
* @param \swoole_http_request $request
* @param \swoole_http_response $response
* @return bool
*/
protected function secWebsocketAccept(\swoole_http_request $request, \swoole_http_response $response): bool
{
// ws rfc 规范中约定的验证过程
if (!isset($request->header['sec-websocket-key'])) {
// 需要 Sec-WebSocket-Key 如果没有拒绝握手
var_dump('shake fai1 3');
return false;
}
if (0 === preg_match('#^[+/0-9A-Za-z]{21}[AQgw]==$#', $request->header['sec-websocket-key'])
|| 16 !== strlen(base64_decode($request->header['sec-websocket-key']))
) {
//不接受握手
var_dump('shake fai1 4');
return false;
}
$key = base64_encode(sha1($request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$headers = array(
'Upgrade' => 'websocket',
'Connection' => 'Upgrade',
'Sec-WebSocket-Accept' => $key,
'Sec-WebSocket-Version' => '13',
'KeepAlive' => 'off',
);
if (isset($request->header['sec-websocket-protocol'])) {
$headers['Sec-WebSocket-Protocol'] = $request->header['sec-websocket-protocol'];
}
// 发送验证后的header
foreach ($headers as $key => $val) {
$response->header($key, $val);
}
// 接受握手 还需要101状态码以切换状态
$response->status(101);
var_dump('shake success at fd :' . $request->fd);
return true;
}
}