分类
JavaScript PHP 编程语言

websocket握手的GUID

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&#91;'x-real_ip']);

	    $errordi = Di::getInstance()->get("error");

	    $code = $errordi->getvalue("E_CONNECTED", 'code');
	    $message = $errordi->getvalue("E_CONNECTED", 'error_name');

	    $msg = json_encode(&#91;
		    "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&#91;'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&#91;'remote_ip']);
                echo "server close \n";
            } else {
	            Logger::getInstance()->console('客户端断开连接, 客户端ID ' . $fd . ", 客户端IP:".$info&#91;'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&#91;'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&#91;'sec-websocket-key'])) {
            // 需要 Sec-WebSocket-Key 如果没有拒绝握手
            var_dump('shake fai1 3');
            return false;
        }
        if (0 === preg_match('#^&#91;+/0-9A-Za-z]{21}&#91;AQgw]==$#', $request->header&#91;'sec-websocket-key'])
            || 16 !== strlen(base64_decode($request->header&#91;'sec-websocket-key']))
        ) {
            //不接受握手
            var_dump('shake fai1 4');
            return false;
        }

        $key = base64_encode(sha1($request->header&#91;'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&#91;'sec-websocket-protocol'])) {
            $headers&#91;'Sec-WebSocket-Protocol'] = $request->header&#91;'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;
    }
}

xiangzhanyou

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注