WebSocket服务端开发(三)-WebSocketServer类握手相关函数介绍

本文介绍WebSocketServer类握手相关函数的实现。

握手函数doHandShake:

function doHandShake($socketId){
  //一旦进入了doHandshake函数,说明已收到完整的请求头,故将此socketId从handshakingList中移除
  array_splice($this->handshakingList, array_search($socketId, $this->handshakingList), 1);

  //获取socket的相关信息
  $session = $this->socketListMap[$socketId];

  //获取http请求头
  $headers = $this->getHeaders($session['buffer']);

  //请求的数据内容会清空,因为已经读取过了,这里buffer是一个读取缓冲区
  $this->socketListMap[$socketId]['buffer'] = '';
  $this->socketListMap[$socketId]['headers'] = $headers;

  //checkBaseHeader用于检查基本头信息,如果有任何一个头信息不符合WebSocket协议,则检查失败
  //checkCustomHeader为用户自定义头部检查,需要继承类覆盖实现,一般检查cookie、origin等与业务相关的头部信息
  if (!$this->checkBaseHeader($headers) || !$this->checkCustomHeader($headers)) {
    //生成握手失败响应
    $this->badRequest($socketId);

    //关闭连接
    $this->disconnect($socketId);

    //握手失败回调
    $this->onHandShakeFailure($socketId);
    return false;
  } else {
    //获取握手返回头部数据
    $responseHeader = $this->getHandShakeHeader($headers);
  }
  //发送响应头
  $this->socketSend($socketId, $responseHeader);

  //已握手标记置为true,之后在收到该socket数据将进入数据处理逻辑
  $this->socketListMap[$socketId]['handshake'] = true;

  //握手成功回调
  $this->onHandShakeSuccess($socketId);
}

checkBaseHeader函数:

function checkBaseHeader($header) {
  //检查Upgrade字段是否为websocket
  return strcasecmp($header['Upgrade'], 'websocket') === 0 &&
  //检查Connection字段是否为Upgrade
  strcasecmp($header['Connection'], 'Upgrade') === 0 &&
  //检查Sec-WebSocket-Key字段Base64解码后长度是否为16字节
  strlen(base64_decode($header['Sec-WebSocket-Key'])) === 16 &&
  //检查WebSocket协议版本是否为13,该类仅处理版本为13的WebSocket协议
    $header['Sec-WebSocket-Version'] === '13';
}

badRequest函数:

function badRequest($socketId) {
  //该函数仅拼装握手错误的响应信息,并发送
  $message = 'This is a websocket server!';
  $out = "HTTP/1.1 400 Bad request\n";
  $out .= "Server: WebSocket Server/lyz810\n";
  $out .= "Content-Length: " . strlen($message) . "\n";
  $out .= "Connection: close\n\n";
  $out .= $message;
  $this->socketSend($socketId, $out);
}

getHandShakeHeader函数:

function getHandShakeHeader($headers) {
  //拼装响应头的相关字段
  $responseHeader = array(
    'HTTP/1.1 101 Switching Protocols',
    'Upgrade: WebSocket',
    'Connection: Upgrade',
    'Sec-WebSocket-Accept: ' . $this->getWebSocketAccept($headers['Sec-WebSocket-Key']),
  );
  if (isset($headers['Sec-WebSocket-Protocol'])) {
    //子协议选择,应由继承类覆盖实现,否则默认使用最先出现的子协议
    $protocol = $this->selectProtocol(explode(',', $headers['Sec-WebSocket-Protocol']));
    array_push($responseHeader, 'Sec-WebSocket-Protocol: ' . $protocol);
  }
  return implode("\r\n", $responseHeader) . "\r\n\r\n";
}

getWebSocketAccept函数:

function getWebSocketAccept($websocketKey) {
  //根据协议要求,计算WebSocket-accept-key
  return base64_encode(sha1($websocketKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}

WebSocket服务端开发(二)-WebSocketServer类主流程介绍

本文介绍WebSocketServer主函数run的实现,从整体上理解协议工作流程。

run方法代码如下:

function run() {
  //将服务器的socket添加到初始化socket列表中
  array_push($this->socketList, $this->serverSocket);
  //工作流程开始
  while (true) {
    //read为所有存在的socket列表
    $read = $this->socketList;
    //如果shutdown变量设置为true,服务器关闭,退出循环
    if ($this->shutdown) {
      $this->onshutdown();
      return;
    }

    if ($this->debug) {
      echo "Waiting for socket_select\n";
    }
    //该函数会从所有可读写的socket中选取一个socket进行处理,该方法会阻塞流程,只有在收到连接时该方法才会返回
    if (socket_select($read, $write, $except, NULL) === false) {
      if ($this->debug) {
        echo $this->getLastErrMsg();
      }
      continue;
    }

    foreach ($read as $socketItem) {
      //如果选取的socket是服务器监听的socket,则此时是新连接接入
      if ($socketItem === $this->serverSocket) {
        //接受socket连接
        $socket = $this->socketAccept();
        if ($socket) {
          //执行连接方法
          $this->connect($socket);
        }
      } else {
        //此时是连接过的socket,获取socketId
        $socketId = $this->getSocketId($socketItem);
        if ($socketId === FALSE) {
          //获取socketId失败,则将该socket断开连接
          $this->disconnectBySocket($socketItem);
          continue;
        }
        //接收传来的数据
        $data = $this->socketRecv($socketId);
        if (strlen($data) > 0) {
          //收到的数据长度不为空时,需要重置连接错误计数
          $this->socketListMap[$socketId]['errorCnt'] = 0;
          if (!isset($this->socketListMap[$socketId])) {
            $this->disconnect($socketId);
            continue;
          } else if (!$this->socketListMap[$socketId]['handshake']) {
            //尚未进行WebSocket协议握手,尝试读取连接缓冲区,如果缓冲区中没有数据,则将socketId记录到握手中列表
            //这是为了防止握手包被分成多个包进行传递(正常情况下不会出现此问题)
            //但根据HTTP协议,并未规定HTTP请求头不能被分割,故应该根据协议中的\r\n\r\n来判断请求头已发送完毕
            if (strlen($this->socketListMap[$socketId]['buffer']) === 0) {
              $this->handshakingList[$socketId] = time();
            }
            //将数据写入缓冲区
            $this->socketListMap[$socketId]['buffer'] .= $data;
            //比较后4个字节是否为\r\n\r\n
            if (substr_compare($this->socketListMap[$socketId]['buffer'], str_repeat(chr(0x0D) . chr(0x0A), 2), -4) === 0) {
              //进行握手处理
              $this->doHandShake($socketId);
            } else {
              //数据没有传送完毕,需要缓冲数据直到全部接收请求头(这个可以通过Telnet命令直接连接,每输入一个字节都会立即传给服务器,这时服务器应该缓存内容。但同时也应该设置超时时间,防止恶意占用服务器资源。)
              $this->onUpgradePartReceive($socketId);
            }
          } else if ($this->parseFrame($data, $socketId)) {
            //parseFrame会解析数据帧,如果该帧FIN标识为1则函数会返回true,交给businessHandler进行业务逻辑处理,数据在socketListMap的buffer中,所以只需要提供socketId即可找到该socket的所有信息。
            $this->businessHandler($socketId);
          }
        } else {
          $this->socketListMap[$socketId]['errorCnt'] += 1;
          if ($this->debug){
            echo "Receive empty data![$errorCnt]\n";
          }
          if ($errorCnt >= 3) {
            $this->disconnect($socketId);
          }
        }
      }
    }
    //每次处理完连接后,判断是否需要健康检查,检查之后会移除不健康的socket
    if (time() - $this->lastHealthCheck > $this->healthCheckInterval) {
      $this->healthCheck();
    }
    $this->removeUnhandshakeConnect();
  }
}