php+websocket 实现的聊天室功能详解


Posted in PHP onMay 27, 2020

本文实例讲述了php+websocket 实现的聊天室功能。分享给大家供大家参考,具体如下:

一、配置 

开启socket组建,否则会报 Fatal error: Call to undefined function socket_create() 错误

1、打开php.ini配置文件,搜索 extension=php_sockets.dll,把前面的‘;'分号删掉。修改之后重启服务。
注意:如果php版本多,一定要注意使用的哪个版本就要取修改哪个版本的php.ini文件,wamp开启socket需要apache和php下面的php.ini一起修改,而phpstudy只需要修改一个php.ini.

2、检查socket组建是否开启
运行phpinfo.php查看,如果Sockets Support => enabled,就说明开启成功了。

php+websocket 实现的聊天室功能详解

 3、设置cmd可以运行php文件

在“我的计算机->属性->高级系统设置->高级->环境变量”,在用户变量的PATH添加一条,指向php的路径(注意版本要一致),在环境变量里的Path也需要添加一条,跟上面一样

php+websocket 实现的聊天室功能详解

 4、测试socket和php是否配置成功

在项目下新建一个名叫start.php的文件

if(extension_loaded('sockets')){
   echo "1";
  }else{
   echo "0";
  }

在cmd里输入 php d:\phpstudy\www\start.php,如果输出1,则说明配置正确,如果输出0,则配置错误,需要仔细重新配置

二、实现流程

前端实现比较简单,难点在后台,其逻辑如下:php主要就是接收加密key并返回其中完成套接字的创建和握手操作

php+websocket 实现的聊天室功能详解

  服务端的流程:
1、挂起一个socket套接字进程,等待连接
2、有socket连接之后,遍历套接字数组
3、没有握手的,进行握手操作,已经握手的,则把接收的数据解析并写入缓冲区进行输出。

三、前端代码

<!DOCTYPE html>
  <html lang="en">
  <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>websocket聊天室</title>
  <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="external nofollow" rel="stylesheet">
  <link rel="stylesheet" href="/static/index/css/reset.min.css" rel="external nofollow" >
  <link rel="stylesheet" href="/static/index/css/chat.css" rel="external nofollow" >
  <link rel="stylesheet" href="/static/index/layui/layer/theme/default/layer.css" rel="external nofollow" >
  <style>
  .message img {
  float: left;
  width: 40px;
  height: 40px;
  margin-right: 12px;
  border-radius: 50%;
  }
  
  .you {
  margin-left: 60px;
  margin-top: -39px;
  }
  
  .me-header {
  float: right !important;
  margin-right: 0 !important;
  }
  
  .me {
  margin-right: 60px;
  margin-top: -39px;
  }
  
  .active-chat::-webkit-scrollbar, .left::-webkit-scrollbar {
  width: 2px;
  }
  </style>
  </head>
  <body>
  
  <div class="wrapper">
    <div class="container">
      <div class="left">
        <div class="top" style="padding: 20px 29px;height: auto;">
          <div class="" style="font: 400 13.3333px Arial;font-weight: 400;">在线人数:<span id="numbers">0</span> 人
          </div>
        </div>
        <ul class="people">
        </ul>
      </div>
      <div class="right">
        <div class="top"><span>Tips: <span class="name">PHP之websocket聊天室</span></span></div>
        <div class="chat active-chat" data-chat="person1"
         style="height: auto;border-width: 0px;padding: 10px;height: 483px; padding: 10px;overflow-y: auto;scrollTop: 100px">
        </div>
        <div class="write">
          <a href="javascript:;" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="write-link attach"></a>
          <input type="text" id="input-value" onkeydown="confirm(event)"/>
          <a href="javascript:;" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="write-link smiley"></a>
          <a href="javascript:;" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="write-link send" onclick="send()"></a>
        </div>
      </div>
    </div>
  </div>
  
  <script src="/static/index/js/jquery-1.11.3.min.js"></script>
  <script src="/static/index/js/chat.js"></script>
  <script src="/static/index/layui/layer/layer.js"></script>
  <script>
  var uname = "user" + uuid(8, 11);
  layer.open({
    title: '您的用户名如下',
    content: uname,
    closeBtn: 0,
    yes: function (index, layero) {
      layer.close(index);
    }
  });
  // 随机选出一个头像
  var avatar = ['a1.jpg', 'a2.jpg', 'a3.jpg', 'a4.jpg', 'a5.jpg', 'a6.jpg', 'a7.jpg', 'a8.jpg', 'a9.jpg', 'a10.jpg'];
  if (avatar[Math.round(Math.random() * 10)]) {
    var headerimg = "img/" + avatar[Math.round(Math.random() * 10)];
  } else {
    var headerimg = "img/" + avatar[0];
  }
  
  var ws = null;
  // 创建websocket连接
  connect();
  function connect() {
    // 创建一个 websocket 连接 ws://ip:端口号
    ws = new WebSocket("ws://127.0.0.1:1234");
    
    // 连接状态 1已建立连接
    console.log(ws.readyState)
    
    // 连接建立时触发
    ws.onopen = onopen;
    
    // 客户端接收服务端数据时触发
    ws.onmessage = onmessage;
    
    // 连接关闭时触发
    ws.onclose = onclose;
    
    // 通信发生错误时触发
    ws.onerror = onerror;
  }
  
  // 通信建立成功 
  function onopen()
  {
    var data = "系统消息:建立连接成功";
    console.log(data);
  }
  
  // 接收客户端的数据,发送数据
  function onmessage(e)
  {
    var data = JSON.parse(e.data);
    console.log(data)
    
    switch (data.type) {
      case 'handShake':
        //首次登录,发送登陆数据
        var user_info = {'type': 'login', 'msg': uname, 'headerimg': headerimg};
        sendMsg(user_info);
        break;
      case 'login':
        userList(data.user_list);
        systemMessage('系统消息: ' + data.msg + ' 已上线');
        break;
      case 'logout':
        userList(data.user_list);
        if (data.msg.length > 0) {
        systemMessage('系统消息: ' + data.msg + ' 已下线');
        }
        break;
      case 'user':
        messageList(data);
        break;
      case 'system':
        systemMessage();
        break;
    }
  }
  function onclose()
  {
    console.log("连接关闭,定时重连");
    connect();
  }
  
  // websocket 错误事件
  function onerror()
  {
    var data = "系统消息 : 出错了,请退出重试.";
    console.log(data);
  }
  
  function confirm(event) {
    var key_num = event.keyCode;
    if (13 == key_num) {
      send();
    } else {
      return false;
    }
  }
  
  // 发送数据
  function send() {
    var msg = document.querySelector("input#input-value").value;
    var reg = new RegExp("\r\n", "g");
    msg = msg.replace(reg, "");
    sendMsg({type: "user", msg: msg});
    document.querySelector("input#input-value").value = "";
  }
  
  // 发送数据
  function sendMsg(msg) {
    var data = JSON.stringify(msg);
    ws.send(data);
  }
  
  
  // 追加数据 上下线的系统消息
  function systemMessage(msg) {
    var html = `<div class="conversation-start">
        <span>` + msg + `</span>
        </div>`;
    var active_chat = document.querySelector('div.active-chat');
    var oldHtml = active_chat.innerHTML;
    active_chat.innerHTML = oldHtml + html;
    active_chat.scrollTop = active_chat.scrollHeight;
  }
  
  // 追加从服务端返回的数据 左侧在线人数列表
  function userList(user) {
    var html = '';
    for (var i = 0; i < user.length; i++) {
      html += `<li class="person" data-chat="person1">
        <img src="` + user[i].headerimg + `" alt=""/>
        <span class="name">` + user[i].username + `</span>
        <span class="time">` + user[i].login_time + `</span>
        <span class="preview" style="color: green;font-size: 7px;">在线</span>
        </li>`;
    }
    document.querySelector('ul.people').innerHTML = html;
    document.querySelector('span#numbers').innerHTML = user.length;
  }
  
  // 右侧聊天记录列表
  function messageList(data) {
  
    // 判读是不是自己发送的消息,对应的样式不同
    if (data.from == uname) {  
      // 如果当前用户名和feom的用户名相同,就说明时自己发送的消息
      var html = `<div class="message">
          <img class="me-header" src="` + data.headerimg + `" alt=""/>
          <div class="bubble me">` + data.msg + `</div>
          </div>`;
    } else {
      // 别人发送的信息列表
      var html = `<div class="message">
          <img src="` + data.headerimg + `" alt=""/>
          <div class="bubble you">` + data.msg + `</div>
          </div>`;
    }
    var active_chat = document.querySelector('div.active-chat');
    var oldHtml = active_chat.innerHTML;
    active_chat.innerHTML = oldHtml + html;
    active_chat.scrollTop = active_chat.scrollHeight;
  }
  
  /**
   * 生产一个全局唯一ID作为用户名的默认值;
   *
   * @param len
   * @param radix
   * @returns {string}
   */
  function uuid(len, radix) {
    var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
    var uuid = [], i;
    radix = radix || chars.length;
    
    if (len) {
      for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
    } else {
      var r;
      
      uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
      uuid[14] = '4';
      
      for (i = 0; i < 36; i++) {
        if (!uuid[i]) {
          r = 0 | Math.random() * 16;
          uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
        }
      }
    }
    return uuid.join('');
  }
  </script>
  </body>
  </html>

 四、后端代码

<?php
/**
 * Created by PhpStorm.
 * User: 25754
 * Date: 2019/4/23
 * Time: 14:13
 */

class socketServer
{

  const LISTEN_SOCKET_NUM = 9;
  const LOG_PATH = "./log/"; //日志
  private $_ip = "127.0.0.1"; //ip
  private $_port = 1234; //端口 要和前端创建WebSocket连接时的端口号一致
  private $_socketPool = array(); //socket池,即存放套接字的数组
  private $_master = null;  //创建的套接字对象

  public function __construct()
  {
    $this->initSocket();
  }

  // 创建WebSocket连接
  private function initSocket()
  {
    try {
      //创建socket套接字
      $this->_master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
      // 设置IP和端口重用,在重启服务器后能重新使用此端口;
      socket_set_option($this->_master, SOL_SOCKET, SO_REUSEADDR, 1);
      //绑定地址与端口
      socket_bind($this->_master, $this->_ip, $this->_port);
      //listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接,其中的能存储的请求不明的socket数目。
      socket_listen($this->_master, self::LISTEN_SOCKET_NUM);
    } catch (Exception $e) {
      $this->debug(array("code: " . $e->getCode() . ", message: " . $e->getMessage()));
    }
    //将socket保存到socket池中 (将套接字放入数组)默认把当前用户放在第一个
    $this->_socketPool[0] = array('resource' => $this->_master);
    $pid = getmypid();
    $this->debug(array("server: {$this->_master} started,pid: {$pid}"));
  }

  // 挂起进程遍历套接字数组,对数据进行接收、处理、发送
  public function run()
  {
    // 死循环 直到socket断开
    while (true) {
      try {
        
        $write = $except = NULL;
        // 从数组中取出resource列
        $sockets = array_column($this->_socketPool, 'resource');

        /* 
        $sockets 是一个存放文件描述符的数组。
        $write 是监听是否客户端写数据,传入NULL是不关心是否有写变化
        $except 是$sockets里面要派粗话的元素,传入null是监听全部
        最后一个参数是超时时间,0立即结束 n>1则最多n秒后结束,如遇某一个连接有新动态,则提前返回 null如遇某一个连接有新动态,则返回
        */
        // 接收套接字数字,监听他们的状态就是有新消息到或有客户端连接/断开时,socket_select函数才会返回,继续往下执行
        $read_num = socket_select($sockets, $write, $except, NULL);
        if (false === $read_num) {
          $this->debug(array('socket_select_error', $err_code = socket_last_error(), socket_strerror($err_code)));
          return;
        }

        // 遍历套接字数组
        foreach ($sockets as $socket) {

          // 如果有新的连接进来
          if ($socket == $this->_master) {

            // 接收一个socket连接
            $client = socket_accept($this->_master);
            if ($client === false) {
              $this->debug(['socket_accept_error', $err_code = socket_last_error(), socket_strerror($err_code)]);
              continue;
            }
            //连接 并放到socket池中
            $this->connection($client);
          } else {

            //接收已连接的socket数据,返回的是从socket中接收的字节数。
            // 第一个参数:socket资源,第二个参数:存储接收的数据的变量,第三个参数:接收数据的长度
            $bytes = @socket_recv($socket, $buffer, 2048, 0);

            // 如果接收的字节数为0
            if ($bytes == 0) {

              // 断开连接
              $recv_msg = $this->disconnection($socket);
            } else {

              // 判断有没有握手,没有握手进行握手,已经握手则进行处理
              if ($this->_socketPool[(int)$socket]['handShake'] == false) {
                // 握手
                $this->handShake($socket, $buffer);
                continue;
              } else {
                // 解析客户端传来的数据
                $recv_msg = $this->parse($buffer);
              }
            }

            // echo "<pre>";
            // 业务处理,组装返回客户端的数据格式
            $msg = $this->doEvents($socket, $recv_msg);
            // print_r($msg);

            socket_getpeername ( $socket , $address ,$port );
            $this->debug(array(
              'send_success',
              json_encode($recv_msg),
              $address,
              $port
            ));
            // 把服务端返回的数据写入套接字
            $this->broadcast($msg);
          }
        }
      } catch (Exception $e) {
        $this->debug(array("code: " . $e->getCode() . ", message: " . $e->getMessage()));
      }

    }

  }

  /**
   * 数据广播
   * @param $data
   */
  private function broadcast($data)
  {
    foreach ($this->_socketPool as $socket) {
      if ($socket['resource'] == $this->_master) {
        continue;
      }
      // 写入套接字
      socket_write($socket['resource'], $data, strlen($data));
    }
  }

  /**
   * 业务处理,在这可以对数据库进行操作,并返回客户端数据;根据不同类型,组装不同格式的数据
   * @param $socket
   * @param $recv_msg 客户端传来的数据
   * @return string
   */
  private function doEvents($socket, $recv_msg)
  {
    $msg_type = $recv_msg['type'];
    $msg_content = $recv_msg['msg'];
    $response = [];
    //echo "<pre>";
    switch ($msg_type) {
      case 'login':
      // 登陆上线信息
        $this->_socketPool[(int)$socket]['userInfo'] = array("username" => $msg_content, 'headerimg' => $recv_msg['headerimg'], "login_time" => date("h:i"));
        // 取得最新的名字记录
        $user_list = array_column($this->_socketPool, 'userInfo');
        $response['type'] = 'login';
        $response['msg'] = $msg_content;
        $response['user_list'] = $user_list;
        //print_r($response);

        break;
      case 'logout':
      // 退出信息
        $user_list = array_column($this->_socketPool, 'userInfo');
        $response['type'] = 'logout';
        $response['user_list'] = $user_list;
        $response['msg'] = $msg_content;
        //print_r($response);
        break;
      case 'user':
      // 发送的消息
        $userInfo = $this->_socketPool[(int)$socket]['userInfo'];
        $response['type'] = 'user';
        $response['from'] = $userInfo['username'];
        $response['msg'] = $msg_content;
        $response['headerimg'] = $userInfo['headerimg'];
        //print_r($response);
        break;
    }

    return $this->frame(json_encode($response));
  }

  /**
   * socket握手
   * @param $socket
   * @param $buffer 客户端接收的数据
   * @return bool
   */
  public function handShake($socket, $buffer)
  {
    $acceptKey = $this->encry($buffer);
    $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
      "Upgrade: websocket\r\n" .
      "Connection: Upgrade\r\n" .
      "Sec-WebSocket-Accept: " . $acceptKey . "\r\n\r\n";

    // 将socket写入缓冲区
    socket_write($socket, $upgrade, strlen($upgrade));
    // 标记握手已经成功,下次接受数据采用数据帧格式
    $this->_socketPool[(int)$socket]['handShake'] = true;
    socket_getpeername ( $socket , $address ,$port );
    $this->debug(array(
      'hand_shake_success',
      $socket,
      $address,
      $port
    ));
    //发送消息通知客户端握手成功
    $msg = array('type' => 'handShake', 'msg' => '握手成功');
    $msg = $this->frame(json_encode($msg));
    socket_write($socket, $msg, strlen($msg));
    return true;
  }

  /**
   * 帧数据封装
   * @param $msg
   * @return string
   */
  private function frame($msg)
  {
    $frame = [];
    $frame[0] = '81';
    $len = strlen($msg);
    if ($len < 126) {
      $frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len);
    } else if ($len < 65025) {
      $s = dechex($len);
      $frame[1] = '7e' . str_repeat('0', 4 - strlen($s)) . $s;
    } else {
      $s = dechex($len);
      $frame[1] = '7f' . str_repeat('0', 16 - strlen($s)) . $s;
    }
    $data = '';
    $l = strlen($msg);
    for ($i = 0; $i < $l; $i++) {
      $data .= dechex(ord($msg{$i}));
    }
    $frame[2] = $data;
    $data = implode('', $frame);
    return pack("H*", $data);
  }

  /**
   * 解析客户端的数据
   * @param $buffer
   * @return mixed
   */
  private function parse($buffer)
  {
    $decoded = '';
    $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 json_decode($decoded, true);
  }

  //提取 Sec-WebSocket-Key 信息并加密
  private function encry($req)
  {
    $key = null;
    if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $req, $match)) {
      $key = $match[1];
    }
    // 加密
    return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
  }

  /**
   * 连接socket
   * @param $client
   */
  public function connection($client)
  {
    socket_getpeername ( $client , $address ,$port );
    $info = array(
      'resource' => $client,
      'userInfo' => '',
      'handShake' => false,
      'ip' => $address,
      'port' => $port,
    );
    $this->_socketPool[(int)$client] = $info;
    $this->debug(array_merge(['socket_connect'], $info));
  }

  /**
   * 断开连接
   * @param $socket
   * @return array
   */
  public function disconnection($socket)
  {
    $recv_msg = array(
      'type' => 'logout',
      'msg' => @$this->_socketPool[(int)$socket]['userInfo']['username'],
    );
    unset($this->_socketPool[(int)$socket]);
    return $recv_msg;
  }

  /**
   * 日志
   * @param array $info
   */
  private function debug(array $info)
  {
    $time = date('Y-m-d H:i:s');
    array_unshift($info, $time);
    $info = array_map('json_encode', $info);
    file_put_contents(self::LOG_PATH . 'websocket_debug.log', implode(' | ', $info) . "\r\n", FILE_APPEND);
  }
}

// 类外实例化
$sk = new socketServer();
// 运行
$sk -> run();

五、运行php

建立start.bat文件,运行php,也可以在cmd里输入命令运行php

php ./socketServer.php
pause

运行结果如下:

php+websocket 实现的聊天室功能详解

php+websocket 实现的聊天室功能详解

注意:start.bat要一直运行,如果关了,就表示socket也关了,就不能通信了,所有需要start.bat一直运行

 项目地址:https://github.com/zhxiangfei/php-websocket-

希望本文所述对大家PHP程序设计有所帮助。

PHP 相关文章推荐
PHP下用rmdir实现删除目录的三种方法小结
Apr 20 PHP
PHP 作用域解析运算符(::)
Jul 27 PHP
在PHP中实现Javascript的escape()函数代码
Aug 08 PHP
PHP判断文件是否存在、是否可读、目录是否存在的代码
Oct 03 PHP
解决file_get_contents无法请求https连接的方法
Dec 17 PHP
PHP编程中的常见漏洞和代码实例
Aug 06 PHP
分享一段PHP制作的中文拼音首字母工具类
Dec 11 PHP
PHP随机数 C扩展随机数
May 04 PHP
PHP实现文件上传功能实例代码
May 18 PHP
PHP CURL中传递cookie的方法步骤
May 09 PHP
php文件上传原理与实现方法详解
Dec 20 PHP
php连接mysql之mysql_connect()与mysqli_connect()的区别
Jul 19 PHP
php+js实现的拖动滑块验证码验证表单操作示例【附源码下载】
May 27 #PHP
PHP code 验证码生成类定义和简单使用示例
May 27 #PHP
PHP 计算至少是其他数字两倍的最大数的实现代码
May 26 #PHP
tp5.1 框架数据库-数据集操作实例分析
May 26 #PHP
tp5.1 框架路由操作-URL生成实例分析
May 26 #PHP
tp5.1 框架join方法用法实例分析
May 26 #PHP
tp5.1框架数据库子查询操作实例分析
May 26 #PHP
You might like
一些关于PHP的知识
2006/11/17 PHP
Yii模型操作之criteria查找数据库的方法
2016/07/15 PHP
PHP实现时间比较和时间差计算的方法示例
2017/07/24 PHP
PHP观察者模式示例【Laravel框架中有用到】
2018/06/15 PHP
Django中通过定时任务触发页面静态化的处理方式
2018/08/29 PHP
PHP的简单跳转提示的实现详解
2019/03/14 PHP
PHP pthreads v3下worker和pool的使用方法示例
2020/02/21 PHP
JavaScript 创建对象
2009/07/17 Javascript
JS清空多文本框、文本域示例代码
2014/02/24 Javascript
浅析tr的隐藏和显示问题
2014/03/05 Javascript
node.js中的fs.realpathSync方法使用说明
2014/12/16 Javascript
JavaScript中的包装对象介绍
2015/01/27 Javascript
jQuery Dialog对话框事件用法实例分析
2016/05/10 Javascript
创建一般js对象的几种方式
2017/01/19 Javascript
koa大型web项目中使用路由装饰器的方法示例
2019/04/02 Javascript
layui table去掉右侧滑动条的实现方法
2019/09/05 Javascript
vue 判断页面是首次进入还是再次刷新的实例
2020/11/05 Javascript
[15:39]教你分分钟做大人:龙骑士
2014/10/30 DOTA
python中类和实例如何绑定属性与方法示例详解
2017/08/18 Python
Python KMeans聚类问题分析
2018/02/23 Python
python实时监控cpu小工具
2018/06/21 Python
用python建立两个Y轴的XY曲线图方法
2019/07/08 Python
python-tornado的接口用swagger进行包装的实例
2019/08/29 Python
PyInstaller的安装和使用的详细步骤
2020/06/02 Python
is_file和file_exists效率比较
2021/03/14 PHP
我的珠宝盒:Ma boîte à bijoux
2019/08/27 全球购物
英国办公家具网站:Furniture At Work
2019/10/07 全球购物
幼儿园元旦活动感言
2014/03/02 职场文书
法律进学校实施方案
2014/03/15 职场文书
法人代表任命书范本
2014/06/05 职场文书
英语四级考试作弊检讨书
2014/09/29 职场文书
买房协议书范本
2014/10/23 职场文书
2015年机械设备管理工作总结
2015/05/04 职场文书
教师个人工作总结范文2015
2015/10/14 职场文书
SpringBoot生成License的实现示例
2021/06/16 Java/Android
CSS实现五种常用的2D转换
2021/12/06 HTML / CSS