websocket初探

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了websocket初探相关的知识,希望对你有一定的参考价值。

背景:

  前段时间有个小需求:进入页面后需要根据数据库状态来自行跳转。去查了查,大部分都是用轮询做。自己觉得其实轮询也不是不可以,但是总觉比较尴尬。

因为本身的模式应该是以状态为主体,页面部分为客体,当状态发生变化的时候,主动把信号传给页面,然后页面跟着做跳转。如果用轮询,就变成了以页面为

主体,状态为客体,页面不断的给请求,如果状态变了,然后自己做跳转。然后看到了websocket,不过说实话不懂这东西。socket编程一直都有点模糊。于是

稍微看看这到底是神马。

主题:

  去github上查了一波,找到了一个小实例很适合初学。

  地址:https://github.com/ghedipunk/php-Websockets

  主要步骤:

  建立socket并监听

    服务器端 :

$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Failed: socket_create()");
socket_set_option(
$this->master, SOL_SOCKET, SO_REUSEADDR, 1) or die("Failed: socket_option()"); socket_bind($this->master, $addr, $port) or die("Failed: socket_bind()"); socket_listen($this->master,20) or die("Failed: socket_listen()");

 

    客户端:

 

var host = "ws://127.0.0.1:9000"; // SET THIS TO YOUR SERVER
   socket = new WebSocket(host);

  websocket协议的三次握手:

    服务端

      主要是对头部信息的拆分,拿到其中的sec-websocket-key,进行拼接后加密再编码返回给客户端。(PS对这块真不太熟,具体可参见下边的链接,很详细)

      提取代码如下

protected function doHandshake($user, $buffer) {
    $magicGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
    $headers = array();
    $lines = explode("\n",$buffer);
    foreach ($lines as $line) {
      if (strpos($line,":") !== false) {
        $header = explode(":",$line,2);
        $headers[strtolower(trim($header[0]))] = trim($header[1]);
      }
      elseif (stripos($line,"get ") !== false) {
        preg_match("/GET (.*) HTTP/i", $buffer, $reqResource);
        $headers[‘get‘] = trim($reqResource[1]);
      }
    }
    if (isset($headers[‘get‘])) {
      $user->requestedResource = $headers[‘get‘];
    } 
    else {
      // todo: fail the connection
      $handshakeResponse = "HTTP/1.1 405 Method Not Allowed\r\n\r\n";     
    }
    if (!isset($headers[‘host‘]) || !$this->checkHost($headers[‘host‘])) {
      $handshakeResponse = "HTTP/1.1 400 Bad Request";
    }
    if (!isset($headers[‘upgrade‘]) || strtolower($headers[‘upgrade‘]) != ‘websocket‘) {
      $handshakeResponse = "HTTP/1.1 400 Bad Request";
    } 
    if (!isset($headers[‘connection‘]) || strpos(strtolower($headers[‘connection‘]), ‘upgrade‘) === FALSE) {
      $handshakeResponse = "HTTP/1.1 400 Bad Request";
    }
    if (!isset($headers[‘sec-websocket-key‘])) {
      $handshakeResponse = "HTTP/1.1 400 Bad Request";
    } 
    else {

    }
    if (!isset($headers[‘sec-websocket-version‘]) || strtolower($headers[‘sec-websocket-version‘]) != 13) {
      $handshakeResponse = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocketVersion: 13";
    }
    if (($this->headerOriginRequired && !isset($headers[‘origin‘]) ) || ($this->headerOriginRequired && !$this->checkOrigin($headers[‘origin‘]))) {
      $handshakeResponse = "HTTP/1.1 403 Forbidden";
    }
    if (($this->headerSecWebSocketProtocolRequired && !isset($headers[‘sec-websocket-protocol‘])) || ($this->headerSecWebSocketProtocolRequired && !$this->checkWebsocProtocol($headers[‘sec-websocket-protocol‘]))) {
      $handshakeResponse = "HTTP/1.1 400 Bad Request";
    }
    if (($this->headerSecWebSocketExtensionsRequired && !isset($headers[‘sec-websocket-extensions‘])) || ($this->headerSecWebSocketExtensionsRequired && !$this->checkWebsocExtensions($headers[‘sec-websocket-extensions‘]))) {
      $handshakeResponse = "HTTP/1.1 400 Bad Request";
    }

    // Done verifying the _required_ headers and optionally required headers.

    if (isset($handshakeResponse)) {
      socket_write($user->socket,$handshakeResponse,strlen($handshakeResponse));
      $this->disconnect($user->socket);
      return;
    }

    $user->headers = $headers;
    $user->handshake = $buffer;

    $webSocketKeyHash = sha1($headers[‘sec-websocket-key‘] . $magicGUID);

    $rawToken = "";
    for ($i = 0; $i < 20; $i++) {
      $rawToken .= chr(hexdec(substr($webSocketKeyHash,$i*2, 2)));
    }
    $handshakeToken = base64_encode($rawToken) . "\r\n";

    $subProtocol = (isset($headers[‘sec-websocket-protocol‘])) ? $this->processProtocol($headers[‘sec-websocket-protocol‘]) : "";
    $extensions = (isset($headers[‘sec-websocket-extensions‘])) ? $this->processExtensions($headers[‘sec-websocket-extensions‘]) : "";

    $handshakeResponse = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $handshakeToken$subProtocol$extensions\r\n";
    socket_write($user->socket,$handshakeResponse,strlen($handshakeResponse));
    $this->connected($user);
  }

    客户端  

socket.onopen    = function(msg) { }

   接受和发送信息:

    服务器端

      在进行通信之前要对数据帧解码/编码才能获取/发送客户端信息。

      握手过程代码都集中在 function doHandshake ()中。

      数据侦编码代码如下:

/*数据帧编码

@param string 原始信息
@param obj 用户对象
@param string 消息类型
@param boolean 是否为附加数据
return string 经过编码的数据
*/
protected function frame($message, $user, $messageType=‘text‘, $messageContinues=false) {
    switch ($messageType) {
      case ‘continuous‘:              //附加数据帧
        $b1 = 0;           
        break;
      case ‘text‘:                         //文本数据帧
        $b1 = ($user->sendingContinuous) ? 0 : 1;
        break;
      case ‘binary‘:                     //二进制数据帧
        $b1 = ($user->sendingContinuous) ? 0 : 2;
        break;
      case ‘close‘:                      //连接关闭
        $b1 = 8;
        break;
      case ‘ping‘:
        $b1 = 9;
        break;
      case ‘pong‘:
        $b1 = 10;
        break;
    }
    if ($messageContinues) {
      $user->sendingContinuous = true;
    } 
    else {
      $b1 += 128;
      $user->sendingContinuous = false;
    }

    $length = strlen($message);
    $lengthField = "";
    if ($length < 126) {
      $b2 = $length;
    } 
    elseif ($length < 65536) {
      $b2 = 126;
      $hexLength = dechex($length);
      //$this->stdout("Hex Length: $hexLength");
      if (strlen($hexLength)%2 == 1) {
        $hexLength = ‘0‘ . $hexLength;
      } 
      $n = strlen($hexLength) - 2;

      for ($i = $n; $i >= 0; $i=$i-2) {
        $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
      }
      while (strlen($lengthField) < 2) {
        $lengthField = chr(0) . $lengthField;
      }
    } 
    else {
      $b2 = 127;
      $hexLength = dechex($length);
      if (strlen($hexLength)%2 == 1) {
        $hexLength = ‘0‘ . $hexLength;
      } 
      $n = strlen($hexLength) - 2;

      for ($i = $n; $i >= 0; $i=$i-2) {
        $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
      }
      while (strlen($lengthField) < 8) {
        $lengthField = chr(0) . $lengthField;
      }
    }

    return chr($b1) . chr($b2) . $lengthField . $message;
  }                

 

      数据帧解码如下:

/*数据帧解码
@param string 原始数据帧
@param obj 用户实体

return string 用户端返回的数据
*/ 
 protected function deframe($message, &$user) {
    //echo $this->strtohex($message);
    $headers = $this->extractHeaders($message);
    $pongReply = false;
    $willClose = false;
    switch($headers[‘opcode‘]) {
      case 0:
      case 1:
      case 2:
        break;
      case 8:
        // todo: close the connection
        $user->hasSentClose = true;
        return "";
      case 9:
        $pongReply = true;
      case 10:
        break;
      default:
        //$this->disconnect($user); // todo: fail connection
        $willClose = true;
        break;
    }

    /* Deal by split_packet() as now deframe() do only one frame at a time.
    if ($user->handlingPartialPacket) {
      $message = $user->partialBuffer . $message;
      $user->handlingPartialPacket = false;
      return $this->deframe($message, $user);
    }
    */
    
    if ($this->checkRSVBits($headers,$user)) {
      return false;
    }

    if ($willClose) {
      // todo: fail the connection
      return false;
    }

    $payload = $user->partialMessage . $this->extractPayload($message,$headers);

    if ($pongReply) {
      $reply = $this->frame($payload,$user,‘pong‘);
      socket_write($user->socket,$reply,strlen($reply));
      return false;
    }
    if ($headers[‘length‘] > strlen($this->applyMask($headers,$payload))) {
        $user->handlingPartialPacket = true;
        $user->partialBuffer = $message;
        return false;
    }

    $payload = $this->applyMask($headers,$payload);

    if ($headers[‘fin‘]) {
      $user->partialMessage = "";
      return $payload;
    }
    $user->partialMessage = $payload;
    return false;
  }

  针对上述过程可以参考参考http://www.qixing318.com/article/643129914.html这篇文章,很详细。

  客户端:

    客户端相对来说简单一些,接受信息方法为 socket.onmessage,发送消息为 socket.send 方法。

 

总结:以原来那个需求来说,websocket是有点大材小用了,还是老老实实轮询来的方便和实际。。。。。。。

  

 

以上是关于websocket初探的主要内容,如果未能解决你的问题,请参考以下文章

初探websocket

WebSocket初探

websocket初探

初探和实现WebSocket心跳重连

websocket初探

Springboot-WebSocket初探-获取HttpSession问题