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 相关文章推荐
第1次亲密接触PHP5(1)
Oct 09 PHP
如何过滤高亮显示非法字符
Oct 09 PHP
PHP fopen 读取带中文URL地址的一点见解
Sep 25 PHP
利用phpExcel实现Excel数据的导入导出(全步骤详细解析)
Nov 26 PHP
浅谈php冒泡排序
Dec 30 PHP
PHP基于cookie与session统计网站访问量并输出显示的方法
Jan 15 PHP
Zend Framework动作助手Url用法详解
Mar 05 PHP
PHP中的Trait 特性及作用
Apr 03 PHP
PHP面向对象程序设计之对象生成方法详解
Dec 02 PHP
CentOS 上搭建 PHP7 开发测试环境
Feb 26 PHP
PHP的mysqli_ssl_set()函数讲解
Jan 23 PHP
PHP7 整型处理机制修改
Mar 09 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定时更新程序设计思路分享
2014/06/10 PHP
微信小程序 消息推送php服务器验证实例详解
2017/03/30 PHP
jQuery初学:find()方法及children方法的区别分析
2011/01/31 Javascript
Jquery实现弹出层分享微博插件具备动画效果
2013/04/03 Javascript
JQuery给网页更换皮肤的方法
2015/05/30 Javascript
JavaScript使用RegExp进行正则匹配的方法
2015/07/11 Javascript
JavaScript 性能优化小结
2015/10/12 Javascript
Bootstrap Metronic完全响应式管理模板学习笔记
2016/07/08 Javascript
html中鼠标滚轮事件onmousewheel的处理方法
2016/11/11 Javascript
js cookie实现记住密码功能
2017/01/17 Javascript
iscroll-probe实现下拉刷新和下拉加载效果
2017/06/28 Javascript
vue 路由页面之间实现用手指进行滑动的方法
2018/02/23 Javascript
vue2.0 循环遍历加载不同图片的方法
2018/03/06 Javascript
解决Vue-cli npm run build生产环境打包,本地不能打开的问题
2018/09/20 Javascript
原生JS实现图片懒加载之页面性能优化
2019/04/26 Javascript
JavaScript中将值转换为字符串的五种方法总结
2019/06/06 Javascript
js实现自动播放匀速轮播图
2020/02/06 Javascript
JS中多层次排序算法的实现代码
2021/01/06 Javascript
python之PyMongo使用总结
2017/05/26 Python
关于Python 3中print函数的换行详解
2017/08/08 Python
Python实现解析Bit Torrent种子文件内容的方法
2017/08/29 Python
Python 中的lambda函数介绍
2018/10/10 Python
python实现植物大战僵尸游戏实例代码
2019/06/10 Python
Python八皇后问题解答过程详解
2019/07/29 Python
Python判断字符串是否xx开始或结尾的示例
2019/08/08 Python
html5 CSS过度-webkit-transition使用介绍
2013/07/02 HTML / CSS
法国床上用品商店:La Compagnie du lit
2019/12/26 全球购物
铭万公司.net面试题笔试题
2014/07/20 面试题
Net Remoting把服务器端激活两种模式
2014/01/22 面试题
北体毕业生求职信
2014/02/28 职场文书
认购协议书范本
2014/04/22 职场文书
银行行长竞聘演讲稿
2014/04/23 职场文书
同志主要表现材料
2014/08/21 职场文书
2014年教师德育工作总结
2014/11/10 职场文书
安全教育培训心得体会
2016/01/15 职场文书
Mysql中有关Datetime和Timestamp的使用总结
2021/12/06 MySQL