php基于websocket實(shí)現(xiàn)的在線聊天室

一、socket協(xié)議的簡(jiǎn)介

WebSocket是什么,有什么優(yōu)點(diǎn)

WebSocket是一個(gè)持久化的協(xié)議,這是相對(duì)于http非持久化來(lái)說(shuō)的。應(yīng)用層協(xié)議。舉個(gè)簡(jiǎn)單的例子,http1.0的生命周期是以request作為界定的,也就是一個(gè)request,一個(gè)response,對(duì)于http來(lái)說(shuō),本次client與server的會(huì)話到此結(jié)束;而在http1.1中,稍微有所改進(jìn),即添加了keep-alive,也就是在一個(gè)http連接中可以進(jìn)行多個(gè)request請(qǐng)求和多個(gè)response接受操作。然而在實(shí)時(shí)通信中,并沒(méi)有多大的作用,http只能由client發(fā)起請(qǐng)求,server才能返回信息,即server不能主動(dòng)向client推送信息,無(wú)法滿足實(shí)時(shí)通信的要求。而WebSocket可以進(jìn)行持久化連接,即client只需進(jìn)行一次握手,成功后即可持續(xù)進(jìn)行數(shù)據(jù)通信,值得關(guān)注的是WebSocket實(shí)現(xiàn)client與server之間全雙工通信,即server端有數(shù)據(jù)更新時(shí)可以主動(dòng)推送給client端。

二、介紹client與server之間的socket連接原理

1、下面是一個(gè)演示client和server之間建立WebSocket連接時(shí)握手部分

image

2、client與server建立socket時(shí)握手的會(huì)話內(nèi)容,即request與response

a、client建立WebSocket時(shí)向服務(wù)器端請(qǐng)求的信息
  GET /chat HTTP/1.1
  Host: server.example.com
  Upgrade: websocket //告訴服務(wù)器現(xiàn)在發(fā)送的是WebSocket協(xié)議
  Connection: Upgrade
  Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== //是一個(gè)Base64 encode的值,這個(gè)是瀏覽器隨機(jī)生成的,用于驗(yàn)證服務(wù)器端返回?cái)?shù)據(jù)是否是WebSocket助理
  Sec-WebSocket-Protocol: chat, superchat
  Sec-WebSocket-Version: 13
  Origin: http://example.com

b、服務(wù)器獲取到client請(qǐng)求的信息后,根據(jù)WebSocket協(xié)議對(duì)數(shù)據(jù)進(jìn)行處理并返回,其中要對(duì)Sec-WebSocket-Key進(jìn)行加密等操作
  HTTP/1.1 101 Switching Protocols
  Upgrade: websocket //依然是固定的,告訴客戶端即將升級(jí)的是Websocket協(xié)議,而不是mozillasocket,lurnarsocket或者shitsocket
  Connection: Upgrade
  Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= //這個(gè)則是經(jīng)過(guò)服務(wù)器確認(rèn),并且加密過(guò)后的 Sec-WebSocket-Key,也就是client要求建立WebSocket驗(yàn)證的憑證
  Sec-WebSocket-Protocol: chat

3、socket建立連接原理圖:

image

三、PHP中建立websocket的過(guò)程講解
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:正在關(guān)閉
 3:連接已關(guān)閉或無(wú)法打開
*/

  //創(chuàng)建一個(gè)webSocket 實(shí)例
  var webSocket = new WebSocket("ws://192.168.31.152:8083");

  webSocket.onerror = function (event){
    onError(event);
  };

  // 打開websocket
  webSocket.onopen = function (event){
    onOpen(event);
  };

  //監(jiān)聽消息
  webSocket.onmessage = function (event){
    onMessage(event);
  };


  webSocket.onclose = function (event){  //服務(wù)端關(guān)閉后 觸發(fā)
    onClose(event);
  }

  //關(guān)閉監(jiān)聽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 = ['未連接','連接成功,可通訊','正在關(guān)閉','連接已關(guān)閉或無(wú)法打開'];
      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.后臺(tái)代碼實(shí)踐

服務(wù)端做的流程大致是:

掛起一個(gè)socket套接字進(jìn)程等待連接
有socket連接之后遍歷套接字?jǐn)?shù)組
沒(méi)有握手的進(jìn)行握手操作,如果已經(jīng)握手則接收數(shù)據(jù)解析并寫入緩沖區(qū)進(jìn)行輸出
下面是示例代碼(我寫的是一個(gè)類所以代碼是根據(jù)函數(shù)分段的),文底給出github地址以及自己遇到的一些坑。
1、首先是創(chuàng)建套接字

//建立套接字
    public function createSocket($address,$port)
    {
      //創(chuàng)建一個(gè)套接字
      $socket= socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
      //設(shè)置套接字選項(xiàng)
      socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
      //綁定IP地址和端口
      socket_bind($socket,$address,$port);
      //監(jiān)聽套接字
      socket_listen($socket);
      return $socket;
    }

2、將套接字放入數(shù)組

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

3、掛起進(jìn)程遍歷套接字?jǐn)?shù)組,主要操作都是在這里面完成的

public function run(){
      //掛起進(jìn)程
      while(true){
        $arr=$this->socs;
        $write=$except=NULL;
        //接收套接字?jǐn)?shù)字 監(jiān)聽他們的狀態(tài)
       //當(dāng)select處于等待時(shí),兩個(gè)客戶端中甲先發(fā)數(shù)據(jù)來(lái),則socket_select會(huì)在$changes中保留甲的socket并往下運(yùn)行,另一個(gè)客戶端的socket就被丟棄了,所以再次循環(huán)時(shí),變成只監(jiān)聽甲了,這個(gè)可以在新循環(huán)中把所有鏈接的客戶端socket再次加進(jìn)$changes中,則可以避免本程序的這個(gè)邏輯錯(cuò)誤
      /** socket_select是阻塞,有數(shù)據(jù)請(qǐng)求才處理,否則一直阻塞
       * 此處$changes會(huì)讀取到當(dāng)前活動(dòng)的連接
       * 比如執(zhí)行socket_select前的數(shù)據(jù)如下(描述socket的資源ID):
       * $socket = Resource id #4
       * $changes = Array
       *       (
       *           [0] => Resource id #5 //客戶端1
       *           [1] => Resource id #4 //server綁定的端口的socket資源
       *       )
       * 調(diào)用socket_select之后,此時(shí)有兩種情況:
       * 情況一:如果是新客戶端2連接,那么 $changes = array([1] => Resource id #4),此時(shí)用于接收新客戶端2連接
       * 情況二:如果是客戶端1(Resource id #5)發(fā)送消息,那么$changes = array([1] => Resource id #5),用戶接收客戶端1的數(shù)據(jù)
       *
       * 通過(guò)以上的描述可以看出,socket_select有兩個(gè)作用,這也是實(shí)現(xiàn)了IO復(fù)用
       * 1、新客戶端來(lái)了,通過(guò) Resource id #4 介紹新連接,如情況一
       * 2、已有連接發(fā)送數(shù)據(jù),那么實(shí)時(shí)切換到當(dāng)前連接,接收數(shù)據(jù),如情況二*/
        socket_select($arr,$write,$except, NULL);
        //遍歷套接字?jǐn)?shù)組
        foreach($arr as $k=>$v){
          //如果是新建立的套接字返回一個(gè)有效的 套接字資源
          if($this->soc == $v){
            $client=socket_accept($this->soc);
            if($client <0){
              echo "socket_accept() failed";
            }else{
              // array_push($this->socs,$client);
              // unset($this[]);
              //將有效的套接字資源放到套接字?jǐn)?shù)組
              $this->socs[]=$client;
            }
          }else{
            //從已連接的socket接收數(shù)據(jù) 返回的是從socket中接收的字節(jié)數(shù)
            $byte=socket_recv($v, $buff,20480, 0);
            //如果接收的字節(jié)是0
            if($byte<7)
              continue;
            //判斷有沒(méi)有握手沒(méi)有握手則進(jìn)行握手,如果握手了 則進(jìn)行處理
            if(!$this->hand[(int)$v]){
              //進(jìn)行握手操作
              $this->hands($buff,$v);
            }else{
              //處理數(shù)據(jù)操作
              $mess=$this->decodeData($buff);
                //發(fā)送數(shù)據(jù)
              $this->send($mess,$v);
            }
          }
        }
      }
    }

4、進(jìn)行握手 流程是接收websocket內(nèi)容從Sec-WebSocket-Key:中獲取key并通過(guò)加密算法寫入緩沖區(qū)客戶端會(huì)進(jìn)行驗(yàn)證(自動(dòng)驗(yàn)證不需要我們處理)

public function hands($buff,$v)
    {
      //提取websocket傳的key并進(jìn)行加密 (這是固定的握手機(jī)制獲取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";
      //將套接字寫入緩沖區(qū)
      socket_write($v,$new_message,strlen($new_message));
      // socket_write(socket,$upgrade.chr(0), strlen($upgrade.chr(0)));
      //標(biāo)記此套接字握手成功
      $this->hand[$v]=true;
    }

5、解析客戶端的數(shù)據(jù)(我這里沒(méi)有進(jìn)行加密,如果有需要也可以自己加密 )

//解析數(shù)據(jù)
    public function decodeData($buff)
    {
      //$buff 解析數(shù)據(jù)幀
      $mask = array(); 
      $data = ''; 
      $msg = unpack('H*',$buff); //用unpack函數(shù)從二進(jìn)制將數(shù)據(jù)解碼
      $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)); 
          //遇到的問(wèn)題 剛連接的時(shí)候就發(fā)送數(shù)據(jù) 顯示 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++; 
        }
        //發(fā)送數(shù)據(jù)到客戶端
          //如果長(zhǎng)度大于125 將數(shù)據(jù)分塊
          $block=str_split($data,125);
          $mess=array(
            'mess'=>$block[0],
            );
        return $mess;          
      }

6、將套接字寫入緩沖區(qū)

//發(fā)送數(shù)據(jù)
    public function send($mess,$v)
    {
      //遍歷套接字?jǐn)?shù)組 成功握手的 進(jìn)行數(shù)據(jù)群發(fā)
      foreach ($this->socs as $keys => $values) {
        //用系統(tǒng)分配的套接字資源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、在與服務(wù)器初始套接字的時(shí)候發(fā)送數(shù)據(jù) (在第一次與服務(wù)器驗(yàn)證握手的時(shí)候不能發(fā)送內(nèi)容)

2、如果已經(jīng)驗(yàn)證過(guò)了但是客戶端沒(méi)有發(fā)送或者發(fā)送的消息為空也會(huì)出現(xiàn)這樣的情況
所以要檢驗(yàn)已連接的套接字的數(shù)據(jù)

image.png

3、可能瀏覽器不支持或者服務(wù)端沒(méi)有開啟socket開始之前最好驗(yàn)證下

if (window.WebSocket){
  console.log("This browser supports WebSocket!");
} else {
  console.log("This browser does not support WebSocket.");
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容