php implementation of websocket real-time message push

Keywords: Programming socket PHP Session SHA1

1, Brief introduction of socket protocol

What is WebSocket and what are its advantages

WebSocket is a persistent protocol, which is relative to http non persistent. Application layer protocol

For example, the life cycle of http1.0 is defined by request, that is, a request and a response. For HTTP, this session between client and server ends here. In http1.1, there is a slight improvement, that is, adding keep alive, that is, multiple request and multiple response acceptance operations can be performed in one HTTP connection. However, in real-time communication, there is not much effect. HTTP can only be requested by the client, and the server can return information, that is, the server can not actively push information to the client, and can not meet the requirements of real-time communication. WebSocket can make persistent connection, that is, the client only needs to shake hands once, and then data communication will be sustainable after success. It is worth noting that WebSocket can realize full duplex communication between client and server, that is, when the server has data update, it can be actively pushed to the client.

2, Introduce the socket connection principle between client and server

1. The following is a demonstration of handshake when establishing WebSocket connection between client and server

2. The session content of handshake when client and server establish socket, i.e. request and response

a. Information requested by the client to the server when establishing WebSocket

GET /chat HTTP/1.1
  Host: server.example.com
Upgrade: websocket / / tells the server to send the WebSocket protocol
  Connection: Upgrade
SEC WebSocket key: x3jjhmbdl1ezlkh9gbhxdw = = / / is a value of Base64 encode generated randomly by the browser to verify whether the data returned by the server is WebSocket assistant
  Sec-WebSocket-Protocol: chat, superchat
  Sec-WebSocket-Version: 13
  Origin: http://example.com

b. After the server obtains the information requested by the client, it processes and returns the data according to the WebSocket protocol, in which the SEC WebSocket key needs to be encrypted

HTTP/1.1 101 Switching Protocols
Upgrade: websocket / / is still fixed. Tell the client to upgrade the Websocket protocol instead of Mozilla socket, lurnarsocket or shitsocket
  Connection: Upgrade
SEC WebSocket accept: hsmrc0smlyukagmm5oppg2hagwk = / / this is the SEC WebSocket key that has been confirmed by the server and encrypted. That is, the client requires to establish the WebSocket authentication certificate
  Sec-WebSocket-Protocol: chat

3. Schematic diagram of socket connection:

 
 

3, How to build websocket in PHP
SocketService.php:

<?php
class SocketService
{
  private $address = '0.0.0.0';
  private $port = 8083;
  private $_sockets;
  public function __construct($address = '', $port='')
  {
      if(!empty($address)){
        $this->address = $address;
      }
      if(!empty($port)) {
        $this->port = $port;
      }
  }

  public function service(){
    //Get the tcp protocol number.
    $tcp = getprotobyname("tcp");
    $sock = socket_create(AF_INET, SOCK_STREAM, $tcp);
    socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);
    if($sock < 0)
    {
      throw new Exception("failed to create socket: ".socket_strerror($sock)."\n");
    }
    socket_bind($sock, $this->address, $this->port);
    socket_listen($sock, $this->port);
    echo "listen on $this->address $this->port ... \n";
    $this->_sockets = $sock;
  }

  public function run(){
    $this->service();
    $clients[] = $this->_sockets;
    while (true){
      $changes = $clients;
      $write = NULL;
      $except = NULL;
//When select is waiting, a of the two clients sends the data first, then socket [Select] will keep the socket of a in $changes and run down, and the socket of the other client will be discarded, so when looping again, it will only listen to A. This can add all the linked client sockets into $changes again in the new loop, so this logic error of this program can be avoided
      /** socket_select It's blocking. Only when there is a data request can it be processed. Otherwise, it will be blocked all the time
       * Here $changes will read to the currently active connection
       * For example, the data before socket select is as follows (describe the resource ID of socket):
       * $socket = Resource id #4
       * $changes = Array
       *       (
       *           [0] => Resource id #5 //Client 1
       *           [1] => Resource id #4 //server socket resource of bound port
       *       )
       * After calling socket select, there are two situations:
       * Case 1: if it is a new client 2 connection, $changes = array ([1] = > resource ID × 4), which is now used to receive the new client 2 connection
       * Case 2: if client 1 (resource ID ා) sends a message, then $changes = array ([1] = > resource ID ා), and the user receives the data of client 1
       *
       * As can be seen from the above description, socket [Select] has two functions, which also realizes IO multiplexing
       * 1,Here comes the new client. Introduce the new connection through resource ID 4, as shown in case 1
       * 2,If there is a connection to send data, switch to the current connection in real time and receive data, as in case 2*/
      socket_select($changes, $write, $except, NULL);
      foreach ($changes as $key => $_sock){
        if($this->_sockets == $_sock){ //Judge whether it is a new socket
          if(($newClient = socket_accept($_sock)) === false){
            die('failed to accept socket: '.socket_strerror($_sock)."\n");
          }
          $line = trim(socket_read($newClient, 1024));
          if($line === false){
             socket_shutdown($newClient);
             socket_close($newClient);
             continue;
          }  
          $this->handshaking($newClient, $line);
          //Get client ip
          socket_getpeername ($newClient, $ip);
          $clients[$ip] = $newClient;
          echo "Client ip:{$ip}  \n";
          echo "Client msg:{$line} \n";
        } else {
          $byte = socket_recv($_sock, $buffer, 2048, 0);
          if($byte < 7) continue;
          $msg = $this->message($buffer);
          //Business code here
          echo "{$key} clinet msg:",$msg,"\n";
          fwrite(STDOUT, 'Please input a argument:');
          $response = trim(fgets(STDIN));
          $this->send($_sock, $response);
          echo "{$key} response to Client:".$response,"\n";
        }
      }
    }
  }

  /**
   * handshake processing
   * @param $newClient socket
   * @return int Information received
   */
  public function handshaking($newClient, $line){

    $headers = array();
    $lines = preg_split("/\r\n/", $line);
    foreach($lines as $line)
    {
      $line = rtrim($line);
      if(preg_match('/^(\S+): (.*)$/', $line, $matches))
      {
        $headers[$matches[1]] = $matches[2];
      }
    }
    $secKey = $headers['Sec-WebSocket-Key'];
    $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
    $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
      "Upgrade: websocket\r\n" .
      "Connection: Upgrade\r\n" .
      "WebSocket-Origin: $this->address\r\n" .
      "WebSocket-Location: ws://$this->address:$this->port/websocket/websocket\r\n".
      "Sec-WebSocket-Accept:$secAccept\r\n\r\n";
    return socket_write($newClient, $upgrade, strlen($upgrade));
  }

  /**
   * Parsing received data
   * @param $buffer
   * @return null|string
   */
  public function message($buffer){
    $len = $masks = $data = $decoded = null;
    $len = ord($buffer[1]) & 127;
    if ($len === 126) {
      $masks = substr($buffer, 4, 4);
      $data = substr($buffer, 8);
    } else if ($len === 127) {
      $masks = substr($buffer, 10, 4);
      $data = substr($buffer, 14);
    } else {
      $masks = substr($buffer, 2, 4);
      $data = substr($buffer, 6);
    }
    for ($index = 0; $index < strlen($data); $index++) {
      $decoded .= $data[$index] ^ $masks[$index % 4];
    }
    return $decoded;
  }

  /**
   * send data
   * @param $newClinet New socket
   * @param $msg  Data to send
   * @return int|string
   */
  public function send($newClinet, $msg){
    $msg = $this->frame($msg);
    socket_write($newClinet, $msg, strlen($msg));
  }

  public function frame($s) {
    $a = str_split($s, 125);
    if (count($a) == 1) {
      return "\x81" . chr(strlen($a[0])) . $a[0];
    }
    $ns = "";
    foreach ($a as $o) {
      $ns .= "\x81" . chr(strlen($o)) . $o;
    }
    return $ns;
  }

  /**
   * Close socket
   */
  public function close(){
    return socket_close($this->_sockets);
  }
}

$sock = new SocketService();
$sock->run();

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: Unconnected
 1: Connection successful, communicable
 2: Shutting down
 3: Connection is closed or cannot be opened
*/

  //Create a webSocket instance
  var webSocket = new WebSocket("ws://192.168.31.152:8083");


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

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

  //Monitoring messages
  webSocket.onmessage = function (event){
    onMessage(event);
  };


  webSocket.onclose = function (event){  //Triggered after the server is shut down
    onClose(event);
  }

  //Turn off listening to 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 = ['Unconnected','Connection successful, communicable','Shutting down','Connection is closed or cannot be opened'];
      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>
 
 

Posted by edontee on Tue, 10 Mar 2020 03:01:08 -0700