美文网首页
php基于websocket实现的在线聊天室

php基于websocket实现的在线聊天室

作者: 金星show | 来源:发表于2019-01-22 20:58 被阅读0次

    一、socket协议的简介

    WebSocket是什么,有什么优点

    WebSocket是一个持久化的协议,这是相对于http非持久化来说的。应用层协议。举个简单的例子,http1.0的生命周期是以request作为界定的,也就是一个request,一个response,对于http来说,本次client与server的会话到此结束;而在http1.1中,稍微有所改进,即添加了keep-alive,也就是在一个http连接中可以进行多个request请求和多个response接受操作。然而在实时通信中,并没有多大的作用,http只能由client发起请求,server才能返回信息,即server不能主动向client推送信息,无法满足实时通信的要求。而WebSocket可以进行持久化连接,即client只需进行一次握手,成功后即可持续进行数据通信,值得关注的是WebSocket实现client与server之间全双工通信,即server端有数据更新时可以主动推送给client端。

    二、介绍client与server之间的socket连接原理

    1、下面是一个演示client和server之间建立WebSocket连接时握手部分

    image

    2、client与server建立socket时握手的会话内容,即request与response

    a、client建立WebSocket时向服务器端请求的信息
      GET /chat HTTP/1.1
      Host: server.example.com
      Upgrade: websocket //告诉服务器现在发送的是WebSocket协议
      Connection: Upgrade
      Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== //是一个Base64 encode的值,这个是浏览器随机生成的,用于验证服务器端返回数据是否是WebSocket助理
      Sec-WebSocket-Protocol: chat, superchat
      Sec-WebSocket-Version: 13
      Origin: http://example.com

    b、服务器获取到client请求的信息后,根据WebSocket协议对数据进行处理并返回,其中要对Sec-WebSocket-Key进行加密等操作
      HTTP/1.1 101 Switching Protocols
      Upgrade: websocket //依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocket,lurnarsocket或者shitsocket
      Connection: Upgrade
      Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= //这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key,也就是client要求建立WebSocket验证的凭证
      Sec-WebSocket-Protocol: chat

    3、socket建立连接原理图:

    image

    三、PHP中建立websocket的过程讲解
    1.前端代码:web.html

    <!doctype html>
    <html lang="en">
     <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, user-scalable=no">
     <title>websocket</title>
     </head>
     <body>
     <input id="text" value="">
     <input type="submit" value="send" onclick="start()">
     <input type="submit" value="close" onclick="close()">
    <div id="msg"></div>
     <script>
     /**
    webSocket.readyState
     0:未连接
     1:连接成功,可通讯
     2:正在关闭
     3:连接已关闭或无法打开
    */
    
      //创建一个webSocket 实例
      var webSocket = new WebSocket("ws://192.168.31.152:8083");
    
      webSocket.onerror = function (event){
        onError(event);
      };
    
      // 打开websocket
      webSocket.onopen = function (event){
        onOpen(event);
      };
    
      //监听消息
      webSocket.onmessage = function (event){
        onMessage(event);
      };
    
    
      webSocket.onclose = function (event){  //服务端关闭后 触发
        onClose(event);
      }
    
      //关闭监听websocket
      function onError(event){
        document.getElementById("msg").innerHTML = "<p>close</p>";
        console.log("error"+event.data);
      };
    
      function onOpen(event){
        console.log("open:"+sockState());
        document.getElementById("msg").innerHTML = "<p>Connect to Service</p>";
      };
    
      function onMessage(event){
        console.log("onMessage");
        document.getElementById("msg").innerHTML += "<p>response:"+event.data+"</p>"
      };
    
      function onClose(event){
        document.getElementById("msg").innerHTML = "<p>close</p>";
        console.log("close:"+sockState());
        webSocket.close();
      }
    
      function sockState(){
        var status = ['未连接','连接成功,可通讯','正在关闭','连接已关闭或无法打开'];
          return status[webSocket.readyState];
      }
    
     function start(event){
        console.log(webSocket);
        var msg = document.getElementById('text').value;
        document.getElementById('text').value = '';
        console.log("send:"+sockState());
        console.log("msg="+msg);
        webSocket.send("msg="+msg);
        document.getElementById("msg").innerHTML += "<p>request"+msg+"</p>"
      };
    
      function close(event){
        webSocket.close();
      }
     </script>
     </body>
    </html>
    

    2.后台代码实践

    服务端做的流程大致是:

    挂起一个socket套接字进程等待连接
    有socket连接之后遍历套接字数组
    没有握手的进行握手操作,如果已经握手则接收数据解析并写入缓冲区进行输出
    下面是示例代码(我写的是一个类所以代码是根据函数分段的),文底给出github地址以及自己遇到的一些坑。
    1、首先是创建套接字

    //建立套接字
        public function createSocket($address,$port)
        {
          //创建一个套接字
          $socket= socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
          //设置套接字选项
          socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
          //绑定IP地址和端口
          socket_bind($socket,$address,$port);
          //监听套接字
          socket_listen($socket);
          return $socket;
        }
    

    2、将套接字放入数组

    public function __construct($address,$port)
        {
          //建立套接字
          $this->soc=$this->createSocket($address,$port);
          $this->socs=array($this->soc);
     
        }
    

    3、挂起进程遍历套接字数组,主要操作都是在这里面完成的

    public function run(){
          //挂起进程
          while(true){
            $arr=$this->socs;
            $write=$except=NULL;
            //接收套接字数字 监听他们的状态
           //当select处于等待时,两个客户端中甲先发数据来,则socket_select会在$changes中保留甲的socket并往下运行,另一个客户端的socket就被丢弃了,所以再次循环时,变成只监听甲了,这个可以在新循环中把所有链接的客户端socket再次加进$changes中,则可以避免本程序的这个逻辑错误
          /** socket_select是阻塞,有数据请求才处理,否则一直阻塞
           * 此处$changes会读取到当前活动的连接
           * 比如执行socket_select前的数据如下(描述socket的资源ID):
           * $socket = Resource id #4
           * $changes = Array
           *       (
           *           [0] => Resource id #5 //客户端1
           *           [1] => Resource id #4 //server绑定的端口的socket资源
           *       )
           * 调用socket_select之后,此时有两种情况:
           * 情况一:如果是新客户端2连接,那么 $changes = array([1] => Resource id #4),此时用于接收新客户端2连接
           * 情况二:如果是客户端1(Resource id #5)发送消息,那么$changes = array([1] => Resource id #5),用户接收客户端1的数据
           *
           * 通过以上的描述可以看出,socket_select有两个作用,这也是实现了IO复用
           * 1、新客户端来了,通过 Resource id #4 介绍新连接,如情况一
           * 2、已有连接发送数据,那么实时切换到当前连接,接收数据,如情况二*/
            socket_select($arr,$write,$except, NULL);
            //遍历套接字数组
            foreach($arr as $k=>$v){
              //如果是新建立的套接字返回一个有效的 套接字资源
              if($this->soc == $v){
                $client=socket_accept($this->soc);
                if($client <0){
                  echo "socket_accept() failed";
                }else{
                  // array_push($this->socs,$client);
                  // unset($this[]);
                  //将有效的套接字资源放到套接字数组
                  $this->socs[]=$client;
                }
              }else{
                //从已连接的socket接收数据 返回的是从socket中接收的字节数
                $byte=socket_recv($v, $buff,20480, 0);
                //如果接收的字节是0
                if($byte<7)
                  continue;
                //判断有没有握手没有握手则进行握手,如果握手了 则进行处理
                if(!$this->hand[(int)$v]){
                  //进行握手操作
                  $this->hands($buff,$v);
                }else{
                  //处理数据操作
                  $mess=$this->decodeData($buff);
                    //发送数据
                  $this->send($mess,$v);
                }
              }
            }
          }
        }
    

    4、进行握手 流程是接收websocket内容从Sec-WebSocket-Key:中获取key并通过加密算法写入缓冲区客户端会进行验证(自动验证不需要我们处理)

    public function hands($buff,$v)
        {
          //提取websocket传的key并进行加密 (这是固定的握手机制获取Sec-WebSocket-Key:里面的key)
          $buf = substr($buff,strpos($buff,'Sec-WebSocket-Key:')+18);
          //去除换行空格字符
          $key = trim(substr($buf,0,strpos($buf,"\r\n")));
           //固定的加密算法
          $new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
          $new_message = "HTTP/1.1 101 Switching Protocols\r\n";
          $new_message .= "Upgrade: websocket\r\n";
          $new_message .= "Sec-WebSocket-Version: 13\r\n";
          $new_message .= "Connection: Upgrade\r\n";
          $new_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";
          //将套接字写入缓冲区
          socket_write($v,$new_message,strlen($new_message));
          // socket_write(socket,$upgrade.chr(0), strlen($upgrade.chr(0)));
          //标记此套接字握手成功
          $this->hand[$v]=true;
        }
    

    5、解析客户端的数据(我这里没有进行加密,如果有需要也可以自己加密 )

    //解析数据
        public function decodeData($buff)
        {
          //$buff 解析数据帧
          $mask = array(); 
          $data = ''; 
          $msg = unpack('H*',$buff); //用unpack函数从二进制将数据解码
          $head = substr($msg[1],0,2); 
          if (hexdec($head{1}) === 8) { 
            $data = false; 
          }else if (hexdec($head{1}) === 1){ 
            $mask[] = hexdec(substr($msg[1],4,2)); 
            $mask[] = hexdec(substr($msg[1],6,2)); 
            $mask[] = hexdec(substr($msg[1],8,2)); 
            $mask[] = hexdec(substr($msg[1],10,2)); 
              //遇到的问题 刚连接的时候就发送数据 显示 state connecting
            $s = 12; 
            $e = strlen($msg[1])-2; 
            $n = 0; 
            for ($i=$s; $i<= $e; $i+= 2) { 
              $data .= chr($mask[$n%4]^hexdec(substr($msg[1],$i,2))); 
              $n++; 
            }
            //发送数据到客户端
              //如果长度大于125 将数据分块
              $block=str_split($data,125);
              $mess=array(
                'mess'=>$block[0],
                );
            return $mess;          
          }
    

    6、将套接字写入缓冲区

    //发送数据
        public function send($mess,$v)
        {
          //遍历套接字数组 成功握手的 进行数据群发
          foreach ($this->socs as $keys => $values) {
            //用系统分配的套接字资源id作为用户昵称
              $mess['name']="Tourist's socket:{$v}";
              $str=json_encode($mess);
              $writes ="\x81".chr(strlen($str)).$str;
              if($this->hand[(int)$values])
                socket_write($values,$writes,strlen($writes));
            }
        }
    

    1、在与服务器初始套接字的时候发送数据 (在第一次与服务器验证握手的时候不能发送内容)

    2、如果已经验证过了但是客户端没有发送或者发送的消息为空也会出现这样的情况
    所以要检验已连接的套接字的数据

    image.png

    3、可能浏览器不支持或者服务端没有开启socket开始之前最好验证下

    if (window.WebSocket){
      console.log("This browser supports WebSocket!");
    } else {
      console.log("This browser does not support WebSocket.");
    }
    

    相关文章

      网友评论

          本文标题:php基于websocket实现的在线聊天室

          本文链接:https://www.haomeiwen.com/subject/pdgrjqtx.html