WebSocket服务端开发(四)-WebSocketServer类socket系列封装函数介绍

本文介绍WebSocketServer类中处理socket的封装函数实现。

getSocketId函数:

function getSocketId($socket) {
  //socketId由socket中的地址和端口号拼接,这样可以保证socket的唯一性,又可以通过id快速读取保存socket的信息以及其附加的其他相关信息
  if (socket_getpeername($socket, $address, $port) === FALSE) {
    return false;
  }
  return $address . '_' . $port;
}

addSocket函数:

function addSocket($socket) {
  //将socket添加到已接受的socket连接池中
  array_push($this->socketList, $socket);
  $socketId = $this->getSocketId($socket);

  //以socketId为索引建立socket映射表,便于后续快速处理socket相关信息
  $this->socketListMap[$socketId] = array(
    //读取缓冲区,由于可能存在分帧的情况,此处统一先保存到缓冲区中,带收到结束帧后统一处理缓冲区
    'buffer' => '',
    //握手成功标识,addSocket在接受连接后调用,故此时并未进行握手,初始化为false
    'handshake' => false,
    //最后通信时间,用于判断超时断开操作
    'lastCommuicate' => time(),
    //socket实例
    'socket' => $socket,
    //错误计数
    'errorCnt' => 0
  );
}

removeSocket函数:

function removeSocket($socketId) {
  $socket = $this->socketListMap[$socketId]['socket'];

  //找出socket在socketList中的索引
  $socketIndex = array_search($socket, $this->socketList);
  if ($this->debug) {
    echo "RemoveSocket at $socketIndex\n";
  }

  //移除socketList中的socket
  array_splice($this->socketList, $socketIndex, 1);

  //移除socketListMap中的相关信息
  unset($this->socketListMap[$socketId]);

  //回调事件
  $this->onAfterRemoveSocket($socketId);
}

socketAccept函数:

function socketAccept() {
  //接受socket
  $socket = socket_accept($this->serverSocket);
  if ($socket !== FALSE) {
    return $socket;
  } else if ($this->debug) {
    echo $this->getLastErrMsg();
  }
}

socketRecv函数:

//从指定socket中读取数据
function socketRecv($socketId) {
  $socket = $this->socketListMap[$socketId]['socket'];
  $bufferLen = socket_get_option($socket, SOL_SOCKET, SO_RCVBUF);
  $recv = socket_recv($socket, $buffer, $bufferLen, 0);
  if ($recv === FALSE) {
    $errCode = $this->getLastErrCode($socketId);
    $this->onerror($errCode, $socketId);
    if ($this->debug) {
      echo $this->getLastErrMsg(null, $errCode);
    }
    return NULL;
  } else if ($recv > 0) {
    if ($this->debug) {
      echo "Recv:\n";
      $this->showData($buffer);
    }
    $this->socketListMap[$socketId]['lastCommuicate'] = time();
  }
  return $buffer;
}

socketSend函数:

function socketSend($socketId, $data) {
  $socket = $this->socketListMap[$socketId]['socket'];
  if ($this->debug) {
    echo "Send:\n";
    $this->showData($data);
  }
  if (socket_write($socket, $data, strlen($data)) > 0) {
    $this->socketListMap[$socketId]['lastCommuicate'] = time();
  }
}

socketClose函数:

function socketClose($socketId) {
  $socket = $this->socketListMap[$socketId]['socket'];
  socket_close($socket);
}

connect函数:

function connect($socket) {
  $this->addSocket($socket);
  $this->onconnected($socket);
}

disconnectBySocket函数:

//此函数通过遍历查找socket信息,性能较低,不建议使用,通常使用disconnect直接根据socketId断开连接
//此函数通常用于一些异常socket的处理
function disconnectBySocket($socket) {
  $socketIndex = array_search($socket, $this->socketList);
  foreach ($this->socketListMap as $socketId => $session) {
    if ($session['socket'] == $socket) {
      $this->disconnect($socketId);
      return;
    }
  }
}

disconnect函数:

function disconnect($socketId, $silent = false) {
  $this->socketClose($socketId);
  if (!$silent) {
    $this->ondisconnected($socketId);
  }
  $this->removeSocket($socketId);
}

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));
}