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中header和session_start前不能有输出原因分析
Jan 11 PHP
从PHP $_SERVER相关参数判断是否支持Rewrite模块
Sep 26 PHP
php模板原理讲解
Nov 13 PHP
一组PHP可逆加密解密算法实例代码
Jan 21 PHP
PHP jQuery表单,带验证具体实现方法
Feb 15 PHP
php通过ajax实现双击table修改内容
Apr 28 PHP
查找php配置文件php.ini所在路径的二种方法
May 26 PHP
php异步多线程swoole用法实例
Nov 14 PHP
PHP中的switch语句的用法实例详解
Oct 21 PHP
Yii CFileCache 获取不到值的原因分析
Feb 08 PHP
php及codeigniter使用session-cookie的方法(详解)
Apr 06 PHP
PHP实现删除多重数组对象属性并重新赋值的方法
Jun 07 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简单判断文本编码的方法
2015/07/30 PHP
解决安装WampServer时提示缺少msvcr110.dll文件的问题
2017/07/09 PHP
jquery与google map api结合使用 控件,监听器
2010/03/04 Javascript
jQuery源码分析-05异步队列 Deferred 使用介绍
2011/11/14 Javascript
JS验证身份证有效性示例
2013/10/11 Javascript
js 3种归并操作的实例代码
2013/10/30 Javascript
JavaScript中的undefined学习总结
2013/11/30 Javascript
JavaScript生成随机数的4种自定义函数分享
2015/02/28 Javascript
JavaScript中操作字符串之localeCompare()方法的使用
2015/06/06 Javascript
js实现无限级树形导航列表效果代码
2015/09/23 Javascript
jQuery插件formValidator自定义函数扩展功能实例详解
2015/11/25 Javascript
Vue 与 Vuex 的第一次接触遇到的坑
2018/08/16 Javascript
浅谈ElementUI中switch回调函数change的参数问题
2018/08/24 Javascript
[52:52]DOTA2上海特级锦标赛C组资格赛#1 OG VS LGD第三局
2016/02/27 DOTA
python 读入多行数据的实例
2018/04/19 Python
浅谈Python type的使用
2019/11/19 Python
python实现实时视频流播放代码实例
2020/01/11 Python
pytorch使用tensorboardX进行loss可视化实例
2020/02/24 Python
澳大利亚设计的婴儿和女孩的衣服:Oobi
2018/12/16 全球购物
介绍一下JMS编程步骤
2015/09/22 面试题
校三好学生主要事迹
2014/01/11 职场文书
大四学生思想汇报
2014/01/13 职场文书
法学专业自我鉴定
2014/02/05 职场文书
法人授权委托书范本
2014/04/04 职场文书
2014年班主任自我评价范文
2014/04/23 职场文书
大学生个人先进事迹材料范文
2014/05/03 职场文书
职员竞岗演讲稿
2014/05/14 职场文书
党的群众路线对照检查材料思想汇报
2014/09/25 职场文书
2014年青年教师工作总结
2014/12/17 职场文书
起诉书范文
2015/05/20 职场文书
萤火虫之墓观后感
2015/06/05 职场文书
就业证明函
2015/06/17 职场文书
名人传读书笔记
2015/06/26 职场文书
Django使用channels + websocket打造在线聊天室
2021/05/20 Python
Python数据可视化之绘制柱状图和条形图
2021/05/25 Python
教你win10系统中APPCRASH事件问题解决方法
2022/07/15 数码科技