从The WebSocket Protocol协议到php、node代码,针对客户端——从协议到代码

websocket协议,php、node做一个服务器框架很多,很容易做一个websocket服务器,这里主要是从协议出发,来看看如何实现php、node一个客户端。其中websocket协议英文版完整版可见这里;中文版自行百度the websocket protocol也能找到。

协议的第一章介绍、第二章一致性要求,跳过可以自行查看。这里从第三章websocket Urls开始。

这章先介绍了两种方案,一直是ws,另一种是wss,也就是80端口、443端口,对应着http和https,协议认为443是安全的;然后这章又说了资源名也就是路径的组建规则,路径参数等。

先看php实现第三章的urls:

/**
     * 解析websocket连接地址
     * @param $wsUrl
     * @throws BadUriException
     */
    protected function parseUrl($wsUrl)
    {
        $UrlValue = parse_url($wsUrl);
        if (!$UrlValue) {
            throw new BadUrlException('不正确的ws Url格式', __LINE__);
        }
        if ($UrlValue['scheme'] != self::PROTOCOL_WS && $UrlValue['scheme'] != self::PROTOCOL_WSS) {
            throw new BadUrlException('ws的Url必须是以ws://或wss://开头', __LINE__);
        }
        $this->scheme = $UrlValue['scheme'];
        $this->host = $UrlValue['host'];
        $this->port = isset($url_parts['port']) ? $url_parts['port'] : ($scheme === 'wss' ? 443 : 80);
        $this->path = isset($url_parts['path']) ? $url_parts['path'] : '/';
        $this->query = isset($url_parts['query'])    ? $url_parts['query'] : '';
        $this->fragment = isset($url_parts['fragment']) ? $url_parts['fragment'] : '';
        $this->user  = isset($url_parts['user']) ? $url_parts['user'] : '';
        $this->pass  = isset($url_parts['pass']) ? $url_parts['pass'] : '';

        if ($UrlValue['query']) {
            $this->path .= '?' . $UrlValue['query'];
        }
        if ($UrlValue['fragment']) {
            $this->path .= '#' . $UrlValue['fragment'];
        }
}
      

这个方法主要是通过php中的函数parse_url来先对websocket的url地址进行解析,来判断是否符合websocket的协议要求,并把相关的参数存放到类的变量中,已被后面连接发送等阶段使用。接下来看node对这部分的实现方法:

  let parsedUrl;

  if (address instanceof URL) {
    parsedUrl = address;
    websocket._url = address.href;
  } else {
    parsedUrl = new URL(address);
    websocket._url = address;
  }

  const isUnixSocket = parsedUrl.protocol === 'ws+unix:';

  if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) {
    throw new Error(`Invalid URL: ${websocket.url}`);
  }

  const isSecure =
    parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:';
  const defaultPort = isSecure ? 443 : 80;

第四章连接握手。为了建立一个websocket连接,客户端需要连接并且发送一个定义的握手协议,客户端需要提供第三章的主机、端口、资源名、安全标记、协议、扩展列表或源字段。

先来看php代码:

    /**
     * Perform WebSocket 连接
     */
    protected function connect(): void
    {
             // Open the socket. stream_socket_client: Open Internet or Unix domain socket connection
        $this->socket = stream_socket_client(
            ($this->protocol == self::PROTOCOL_WSS ? 'ssl://' : 'tcp://') . $this->host . ':' . $this->port,
            $errno,
            $errstr,
            $this->options['timeout'],
            $this->flags,
            $this->context
        );

        restore_error_handler();

        if (!$this->isConnected()) {
            $error = "Could not open socket to \"{$this->host}:{$this->port}\": {$errstr} ({$errno}) {$error}.";
            throw new ConnectionException($error);
        }

        // Set timeout on the stream as well.
        stream_set_timeout($this->socket, $this->options['timeout']);
    }

// Once a connection to the server has been established (including a   connection via a proxy or over a TLS-encrypted tunnel), the client   MUST send an opening handshake to the server.  The handshake consists   of an HTTP Upgrade request, along with a list of required and   optional header fields. 客户端发送一个握手的数据包给服务端,这个数据包由一个http升级请求构成,包含一系列必须和可选的header字段。握手的具体要求看文档即可。

    /**
     * Perform WebSocket 握手
     */
    protected function handshake()
    {
        $this->secWebSocketKey = self::generateWsKey();//随机数The request MUST include a header field with the name        |Sec-WebSocket-Key|.  The value of this header field MUST be a        nonce consisting of a randomly selected 16-byte value that has        been base64-encoded .  The nonce  MUST be selected randomly for each connection.
        $headers = [
            'GET ' . $this->path . ' HTTP/1.1',
            'Host: ' . $this->host . ':' . $this->port,
            'Upgrade: websocket',
            'Connection: Upgrade',
            'Sec-WebSocket-Key: ' . $this->secWebSocketKey,
            'Sec-WebSocket-Version: 13',
        ];
        $htmlHeader = implode(self::HTTP_HEADER_SEPARATION_MARK, $headers) . self::HTTP_HEADER_END_MARK;
        $this->writeToSock($htmlHeader);
        $response = '';
        $end = false;
        do {
            $str = fread($this->socket, 8192);
            if (strlen($str) == 0) {
                break;
            }
            $response .= $str;
            $end = strpos($response, self::HTTP_HEADER_END_MARK);
        } while ($end === false);
 
        if ($end === false) {
            throw new HandshakeException('握手失败:握手响应不是标准的http响应', __LINE__);
        }
 
        $resHeader = substr($response, 0, $end);
        $headers = explode(self::HTTP_HEADER_SEPARATION_MARK, $resHeader);
 
        if (strpos($headers[0], '101') === false) {
            throw new HandshakeException('握手失败:服务器返回http状态码不是101', __LINE__);
        }
        for($i = 1; $i < count($headers); $i++) {
            list($key, $val) = explode(':', $headers[$i]);
            if (strtolower(trim($key)) == 'sec-websocket-accept') {
                $accept = base64_encode(sha1($this->secWebSocketKey . self::UUID, true));
                if (trim($val) != $accept) {
                    throw new HandshakeException('握手失败: sec-websocket-accept值校验失败', __LINE__);
                }
                $this->handshakePass = true;
                break;
            }
        }
        if (!$this->handshakePass) {
            throw new HandshakeException('握手失败:缺少sec-websocket-accept http头', __LINE__);
        }
    }

接下来是node的:


  const key = randomBytes(16).toString('base64');
  const get = isSecure ? https.get : http.get;
  let perMessageDeflate;

  opts.createConnection = isSecure ? tlsConnect : netConnect;
  opts.defaultPort = opts.defaultPort || defaultPort;
  opts.port = parsedUrl.port || defaultPort;
  opts.host = parsedUrl.hostname.startsWith('[')
    ? parsedUrl.hostname.slice(1, -1)
    : parsedUrl.hostname;
  opts.headers = {
    'Sec-WebSocket-Version': opts.protocolVersion,
    'Sec-WebSocket-Key': key,
    Connection: 'Upgrade',
    Upgrade: 'websocket',
    ...opts.headers
  };
  opts.path = parsedUrl.pathname + parsedUrl.search;
  opts.timeout = opts.handshakeTimeout;

  if (opts.perMessageDeflate) {
    perMessageDeflate = new PerMessageDeflate(
      opts.perMessageDeflate !== true ? opts.perMessageDeflate : {},
      false,
      opts.maxPayload
    );
    opts.headers['Sec-WebSocket-Extensions'] = format({
      [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
    });
  }
  if (protocols) {
    opts.headers['Sec-WebSocket-Protocol'] = protocols;
  }
  if (opts.origin) {
    if (opts.protocolVersion < 13) {
      opts.headers['Sec-WebSocket-Origin'] = opts.origin;
    } else {
      opts.headers.Origin = opts.origin;
    }
  }
  if (parsedUrl.username || parsedUrl.password) {
    opts.auth = `${parsedUrl.username}:${parsedUrl.password}`;
  }

  if (isUnixSocket) {
    const parts = opts.path.split(':');

    opts.socketPath = parts[0];
    opts.path = parts[1];
  }

  let req = (websocket._req = get(opts));  //http或https.get,携带参数发出get请求。

第五章,主要是讲解数据帧协议。在websocket中,数据是通过一系列的数据帧来进行传输的,为了安全,客户端发送给服务器的所有帧必须添加掩码(Mask)。服务器发送的数据帧不允许添加掩码。数据帧是在握手之后和终端发送关闭帧之间的时间传输的。具体的帧格式看协议内容。

php代码如下:

  /**
     * @param int $opCode 帧类型
     * @param string $playLoad 携带的数据
     * @param bool $isMask 是否使用掩码
     * @param int $status 关闭帧状态
     * @return false|string
     */
    protected function packFrame($opCode, $playLoad = '', $isMask = true, $status = 1000)
    {
        $firstByte = 0x80 | $opCode;
        if ($isMask) {
            $secondByte = 0x80;
        } else {
            $secondByte = 0x00;
        }
 
        $playLoadLen = strlen($playLoad);
        if ($opCode == self::OPCODE_CLOSE) {
            // 协议规定关闭帧必须使用掩码
            $isMask = true;
            $playLoad = pack('CC', (($status >> 8) & 0xff), $status & 0xff) . $playLoad;
            $playLoadLen += 2;
        }
        if ($playLoadLen <= self::FRAME_LENGTH_LEVEL_1_MAX) {
            $secondByte |= $playLoadLen;
            $frame = pack('CC', $firstByte, $secondByte);
        } elseif ($playLoadLen <= self::FRAME_LENGTH_LEVEL_2_MAX) {
            $secondByte |= 126;
            $frame = pack('CCn', $firstByte, $secondByte, $playLoadLen);
        } else {
            $secondByte |= 127;
            $frame = pack('CCJ', $firstByte, $secondByte, $playLoadLen);
        }
 
        if ($isMask) {
            $maskBytes = [mt_rand(1, 255), mt_rand(1, 255), mt_rand(1, 255), mt_rand(1, 255)];
            $frame .= pack('CCCC', $maskBytes[0], $maskBytes[1], $maskBytes[2], $maskBytes[3]);
            if ($playLoadLen > 0) {
                for ($i = 0; $i < $playLoadLen; $i++) {
                    $playLoad[$i] = chr(ord($playLoad[$i]) ^ $maskBytes[$i % 4]);
                }
            }
        }
 
        $frame .= $playLoad;
 
        return $frame;
    }

node的代码如下:

  /**
   * Frames a piece of data according to the HyBi WebSocket protocol.
   *
   * @param {Buffer} data The data to frame
   * @param {Object} options Options object
   * @param {Number} options.opcode The opcode
   * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
   *     modified
   * @param {Boolean} [options.fin=false] Specifies whether or not to set the
   *     FIN bit
   * @param {Boolean} [options.mask=false] Specifies whether or not to mask
   *     `data`
   * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
   *     RSV1 bit
   * @return {Buffer[]} The framed data as a list of `Buffer` instances
   * @public
   */
  static frame(data, options) {
    const merge = options.mask && options.readOnly;
    let offset = options.mask ? 6 : 2;
    let payloadLength = data.length;

    if (data.length >= 65536) {
      offset += 8;
      payloadLength = 127;
    } else if (data.length > 125) {
      offset += 2;
      payloadLength = 126;
    }

    const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);

    target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
    if (options.rsv1) target[0] |= 0x40;

    target[1] = payloadLength;

    if (payloadLength === 126) {
      target.writeUInt16BE(data.length, 2);
    } else if (payloadLength === 127) {
      target.writeUInt32BE(0, 2);
      target.writeUInt32BE(data.length, 6);
    }

    if (!options.mask) return [target, data];

    randomFillSync(mask, 0, 4);

    target[1] |= 0x80;
    target[offset - 4] = mask[0];
    target[offset - 3] = mask[1];
    target[offset - 2] = mask[2];
    target[offset - 1] = mask[3];

    if (merge) {
      applyMask(data, mask, target, offset, data.length);
      return [target];
    }

    applyMask(data, mask, data, 0, data.length);
    return [target, data];
  }

先看到这里,改天再看。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值