WebSocket服务端开发(六)-WebSocketServer类帧解析相关函数介绍

本文介绍WebSocketServer类解析帧相关的函数。

isMasked函数:

  //判断是否是经过掩码处理的帧
  function isMasked($byte) {
    return (ord($byte) & 0x80) > 0;
  }

getPayloadLen函数:

  //获取负载的长度
  //首字节如果小于126,则长度为首字节的值
  //首字节如果等于126,则长度为后面紧跟的两个字节表示的字节数
  //如果首字节是127,则长度为后面紧跟8字节表示的字节数
  function getPayloadLen($data) {
    $first = ord($data[0]) & 0x7F;
    $second = (ord($data[1]) << 8) + ord($data[2]);
    $third = (ord($data[3]) << 40) + (ord($data[4]) << 32) + (ord($data[5]) << 24) + (ord($data[6]) << 16) + (ord($data[7]) << 8) + ord($data[8]);
    if ($first < 126) {
      return $first;
    } else if ($first === 126) {
      return $second;
    } else {
      return ($second << 48) + $third;
    }
  }

getPayLoad函数:

  //获取负载的内容,根据帧的结构
  //第0字节为结束标记及帧类型
  //第1字节是否掩码标识位及负载长度的首字节
  //根据协议要求,浏览器发送的数据必须经过掩码处理,所以偏移量至少是1字节的帧首字节+4字节的掩码长度
  //根据帧长度的不同,表示长度的字节数可能为1、3或9
  //所以根据不同的情况截取不同长度的数据即为负载内容
  function getPayload($data, $len) {
    $offset = 5;
    if ($len < 126) {
      return substr($data, $offset + 1, $len);
    } else if ($len < 65536) {
      return substr($data, $offset + 3, $len);
    } else {
      return substr($data, $offset + 9, $len);
    }
  }

getMask函数:

  //获取掩码的值
  //仍然是根据不同的偏移位置截取
  function getMask($data, $len) {
    $offset = 1;
    if ($len < 126) {
      return substr($data, $offset + 1, 4);
    } else if ($len < 65536) {
      return substr($data, $offset + 3, 4);
    } else {
      return substr($data, $offset + 9, 4);
    }
  }

getFrameType函数:

  //获取帧类型
  function getFrameType($byte) {
    return ord($byte) & 0x0F;
  }

isFin函数:

  //判断是否为结束帧
  function isFin($byte) {
    return (ord($byte[0]) & 0x80) > 0;
  }

isControlFrame函数:

  //判断是否为控制帧,控制帧包含关闭帧、PING帧和PONG帧
  function isControlFrame($frameType) {
    return $frameType === self::FRAME_CLOSE || $frameType === self::FRAME_PING || $frameType === self::FRAME_PONG;
  }

parseBinaryFrame、parseTextFrame、parseRawFrame函数:

  //处理负载的掩码,将其还原
  function parseRawFrame($payload, $mask) {
    $payloadLen = strlen($payload);
    $dest = '';
    $maskArr = array();
    for ($i = 0; $i < 4; $i++) {
      $maskArr[$i] = ord($mask[$i]);
    }
    for ($i = 0; $i < $payloadLen; $i++) {
      $dest .= chr(ord($payload[$i]) ^ $maskArr[$i % 4]);
    }
    return $dest;
  }

  function parseTextFrame($payload, $mask) {
    return $this->parseRawFrame($payload, $mask);
  }

  function parseBinaryFrame($payload, $mask) {
    return $this->parseRawFrame($payload, $mask);
  }

closeFrame函数:

  //创建并发送关闭帧
  function closeFrame($socketId, $closeCode = 1000, $closeMsg = 'goodbye') {
    $closeCode = chr(intval($closeCode / 256)) . chr($closeCode % 256);
    $frame = $this->createFrame($closeCode . $closeMsg, self::FRAME_CLOSE);
    $this->socketSend($socketId, $frame);
    $this->disconnect($socketId);
  }

sendPing、sendPong函数:

  function sendPing($socketId, $data = 'ping') {
    $frame = $this->createFrame($data, self::FRAME_PING);
    $this->socketSend($socketId, $frame);
  }

  function sendPong($socketId, $data = 'pong') {
    $frame = $this->createFrame($data, self::FRAME_PONG);
    $this->socketSend($socketId, $frame);
  }

createFrame函数:

  //封装帧头的相关标识位、长度等信息
  function createFrame($data, $type, $fin = 0x01) {
    $dataLen = strlen($data);
    $frame = chr(($fin << 7) + $type);
    if ($dataLen < 126) {
      $frame .= chr($dataLen);
    } else if ($dataLen < 65536) {
      $frame .= chr(126);
      $frame .= chr(intval($dataLen / 256));
      $frame .= chr(intval($dataLen % 256));
    } else {
      $frame .= chr(127);
      $hexLen = str_pad(base_convert($dataLen, 10, 16), 16, '0', STR_PAD_LEFT);
      for ($i = 0; $i < 15; $i += 2) {
        $frame .= chr((intval($hexLen[$i], 16) << 8) + intval($hexLen[$i + 1], 16));
      }
    }
    $frame .= $data;
    return $frame;
  }

WebSocket服务端开发(五)-WebSocketServer类帧解析整体流程介绍

本文介绍WebSocketServer类的parseFrame函数流程。

parseFrame函数:

function parseFrame($data, $socketId) {
  //判断该帧是否是经过掩码处理
  $isMasked = $this->isMasked($data[1]);

  //如果未经掩码处理,则根据协议规定,需要断开连接
  if (!$isMasked) {
    //此处使用1002状态码,表示协议错误,发送关闭帧
    $this->closeFrame($socketId, 1002, 'There is no mask!');

    //断开连接
    $this->disconnect($socketId);
    return false;
  }
  //获取负载的长度字节数
  $payloadLen = $this->getPayloadLen(substr($data, 1, 9));

  //根据负载长度获取负载的全部数据
  $payload = $this->getPayload($data, $payloadLen);

  //获取掩码值
  $mask = $this->getMask($data, $payloadLen);

  //获取帧的类型
  $frameType = $this->getFrameType($data[0]);

  //处理帧
  switch ($frameType) {
  case self::FRAME_CONTINUE:
    //后续帧,需要拼接buffer
    $this->socketListMap[$socketId]['buffer'] .= $this->parseRawFrame($payload, $mask);
    break;
  case self::FRAME_TEXT:
    //文本帧,处理方式默认保持一致,均使用parseRawFrame处理,如果由特殊需求可以重写parseTextFrame函数
    $this->socketListMap[$socketId]['buffer'] = $this->parseTextFrame($payload, $mask);
    break;
  case self::FRAME_BIN:
    //二进制帧,处理方式默认保持一致,均使用parseRawFrame处理,如果由特殊需求可以重写parseBinaryFrame函数
    $this->socketListMap[$socketId]['buffer'] = $this->parseBinaryFrame($payload, $mask);
    break;
  case self::FRAME_CLOSE:
    //发送关闭帧(应答帧)
    $this->closeFrame($socketId);
    break;
  case self::FRAME_PING:
    //发送pong帧响应,浏览器目前不提供ping、pong帧的API,此处逻辑基本不会走到,只为实现协议内容
    $this->sendPong($socketId, $this->parseRawFrame($payload, $mask));
    break;
  case self::FRAME_PONG:
    //收到pong帧不进行任何处理(正常情况下不会收到,浏览器不会主动发送pong帧)
    break;
  default:
    //其他帧类型无法处理,直接断开连接,根据协议,此处使用1003状态码关闭连接更好
    $this->disconnect($socketId);
    break;
  }
  if ($this->debug) {
    //输出调试信息
    echo "isFin:" . ((ord($data[0]) & 0x80) >> 7) . "\n";
    echo "opCode:$frameType\n";
    echo "payLoad Length:$payloadLen\n";
    echo "Mask:$mask\n\n";
  }

  //如果是结束的数据帧,返回true,否则均为false
  //当返回true时,外层调用函数会继续将执行核心业务逻辑,读取缓冲区中的数据进行处理
  //如果是false,则不进行进一步的处理(控制帧及非结束帧都不会提交到业务层处理)
  return $this->isFin($data[0]) && !$this->isControlFrame($frameType);
}