PHP用swoole+websocket和redis实现web一对一聊天


Posted in PHP onNovember 05, 2019

Redis 实现每个连接websocket的服务都唯一绑定一个用户。通过 用户账号 = websocket fd 存到redis中。

Mysql 实现离线消息池。如果一个用户不在线,则其他用户发送给他的消息暂时存储在mysql。待该用户上线时,再从离线消息池取出发送。

具体参考代码和相应注释:

<?php
$server = new swoole_websocket_server("0.0.0.0", 9052);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$db = new mysqli('127.0.0.1', 'test', 'test', 'thinkphp5');

$server->on('open', function (swoole_websocket_server $server, $request) {
 echo "server: handshake success with fd{$request->fd}\n";//$request->fd 是客户端id
});

$server->on('message', function (swoole_websocket_server $server, $frame) {
 $data = json_decode($frame->data,true); 
 if($data['flag'] == 'init'){
  //用户刚连接的时候初始化,每个用户登录时记录该用户对应的fd
  $GLOBALS['redis']->set($data['from'], $frame->fd);
  //处理发给该用户的离线消息
  $sql = "SELECT `from`,content FROM thinkphp5.app_offline WHERE `to`='{$data['from']}' AND `from`='{$data['to']}' AND `status`='0' ORDER BY addtime ASC;";
  if ($result = $GLOBALS['db']->query($sql)) {
   $re = array();
   while ($row = $result->fetch_assoc()) {
    array_push($re, $row);
   }
   $result->free();
   foreach($re as $content){
    $content = json_encode($content);
    $server->push($frame->fd , $content);
   }
   //设置消息池中的消息为已发送
   $sql = "UPDATE thinkphp5.app_offline SET `status`=1 WHERE `to`='{$data['from']}' AND `from`='{$data['to']}';";
   $GLOBALS['db']->query($sql);
  }
 }else if($data['flag'] == 'msg'){
  //非初始化的信息发送,一对一聊天,根据每个用户对应的fd发给特定用户
  $tofd = $GLOBALS['redis']->get($data['to']); //消息要发给谁
  $fds = []; //所有在线的用户(打开聊天窗口的用户)
  foreach($server->connections as $fd){
   array_push($fds, $fd);
  }
  if(in_array($tofd,$fds)){
   $tmp['from'] = $data['from']; //消息来自于谁
   $tmp['content'] = $data['content']; //消息内容
   $re = json_encode($tmp);
   $server->push($tofd , $re);
  }else{
   //该玩家不在线(不在聊天室内),将信息发送到离线消息池
   $time = time();
   $sql = "INSERT INTO thinkphp5.app_offline (`to`,`from`,`content`,`status`,`addtime`) VALUES ('{$data['to']}','{$data['from']}','{$data['content']}','0','{$time}');";
   $GLOBALS['db']->query($sql);
  }
 }else if($data['flag'] == 'group'){
  //todo 群聊
  
 }else if($data['flag'] == 'all'){
  //全站广播
  foreach($server->connections as $fd){
   $server->push($fd , $data);
  }
 } 
});

$server->on('close', function ($ser, $fd) {
 echo "client {$fd} closed\n";
});

$server->start();

客户端代码:

<!DOCTYPE html>
<html>
<head>
 <title>XST-app</title>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
 <meta name="viewport" content="width=device-width, initial-scale=0.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
 <meta name="keywords" content="test" />
 <meta name="description" content="test" />
 <meta name="author" content="XST-APP" />
 <meta content="yes" name="apple-mobile-web-app-capable" />
 <meta content="black" name="apple-mobile-web-app-status-bar-style" />
 <meta content="telephone=no" name="format-detection" />
<style type="text/css">
 body{background:url(/static/images/yuyin_bg.jpg);background-size:100%;}
 @media all and (min-width: 640px) {
  body,html,.wenwen-footer,.speak_window{width:640px!important;margin:0 auto}
  .speak_window,.wenwen-footer{left:50%!important;margin-left:-320px}
 }
 input,button{outline:none;}
 .wenwen-footer{width:100%;position:fixed;bottom:-5px;left:0;background:#fff;padding:3%;border-top:solid 1px #ddd;box-sizing:border-box;}
 .wenwen_btn,.wenwen_help{width:15%;text-align:center;}
 .wenwen_btn img,.wenwen_help img{height:40px;}
 .wenwen_text{height:40px;border-radius:5px;border:solid 1px #636162;box-sizing:border-box;width:66%;text-align:center;overflow:hidden;margin-left:2%;}
 .circle-button{padding:0 5px;}
 .wenwen_text .circle-button{font-size:14px;color:#666;line-height:38px;}
 .write_box{background:#fff;width:100%;height:40px;line-height:40px;}
 .write_box input{height:40px;padding:0 5px;line-height:40px;width:100%;box-sizing:border-box;border:0;}
 .wenwen_help button{width:95%;background:#42929d;color:#fff;border-radius:5px;border:0;height:40px;}
 #wenwen{height:100%;}
 .speak_window{overflow-y:scroll;height:100%;width:100%;position:fixed;top:50px;left:0;}
 .speak_box{margin-bottom:70px;padding:10px;}
 .question,.answer{margin-bottom:1rem;}
 .question{text-align:right;}
 .question>div{display:inline-block;}
 .left{float:left;}
 .right{float:right;}
 .clear{clear:both;}
 .heard_img{height:60px;width:60px;border-radius:5px;overflow:hidden;background:#ddd;}
 .heard_img img{width:100%;height:100%}
 .question_text,.answer_text{box-sizing:border-box;position:relative;display:table-cell;min-height:60px;}
 .question_text{padding-right:20px;}
 .answer_text{padding-left:20px;}
 .question_text p,.answer_text p{border-radius:10px;padding:.5rem;margin:0;font-size:14px;line-height:28px;box-sizing:border-box;vertical-align:middle;display:table-cell;height:30px;word-wrap:break-word;}
 .answer_text p{background:#fff;}
 .question_text p{background:#42929d;color:#fff;text-align:left;}
 .question_text i,.answer_text i{width:0;height:0;border-top:5px solid transparent;border-bottom:5px solid transparent;position:absolute;top:25px;}
 .answer_text i{border-right:10px solid #fff;left:10px;}
 .question_text i{border-left:10px solid #42929d;right:10px;}
 .answer_text p a{color:#42929d;display:inline-block;}
 .write_list{position:absolute;left:0;width:100%;background:#fff;border-top:solid 1px #ddd;padding:5px;line-height:30px;}
</style>
</head>

<body>
<div id="header" class="head">
  <div class="wrap">
    <i class="menu_back"><a href="javascript:history.go(-1);" rel="external nofollow" ></a></i>
    <div class="title">
      <span class="title_d"><p>与 {$tonickname} 的聊天</p></span>
      <div class="clear"></div>
    </div>
    <!--i class="menu_share"></i-->
  </div>
</div>
<input type="hidden" name="myemail" id="myemail" value="{$myemail}" />
<input type="hidden" name="mynickname" id="mynickname" value="{$mynickname}" />
<input type="hidden" name="myavatar" id="myavatar" value="{$myavatar}" />
<input type="hidden" name="toemail" id="toemail" value="{$toemail}" />
<input type="hidden" name="tonickname" id="tonickname" value="{$tonickname}" />
<input type="hidden" name="toavatar" id="toavatar" value="{$toavatar}" />

<!-- 对话内容 -->
<div class="speak_window">
 <div class="speak_box">

 </div>
</div>
<!-- 内容输入-->
<div class="wenwen-footer">
 <div class="wenwen_btn left"><img src="/static/images/jp_btn.png"></div>
 <div class="wenwen_text left">
  <div class="write_box"><input type="text" class="left" onKeyUp="keyup()" maxlength="100" placeholder="请输入信息(100字以内)..." /></div> 
 </div>
 <div class="wenwen_help right">
   <button onClick="send()" class="right">发送</button>
 </div>
 <div style="opacity:0;" class="clear"></div>
</div>

<script type="text/javascript">
 if ("WebSocket" in window){
  var ws = new WebSocket("ws://192.168.0.1:9052");
  ws.onopen = function(){
   console.log("握手成功");
   var myemail = $("#myemail").val();
   var toemail = $("#toemail").val();
   var arr = {"flag":"init","from":myemail,"to":toemail};
   var str = JSON.stringify(arr);
   ws.send(str);
  };
  ws.onmessage = function(e){
   var toemail = $("#toemail").val();
   var toavatar = $("#toavatar").val();
   var obj = JSON.parse(e.data);
   console.log(e.data);
   //但同时与两个人聊天时,可能两个人的消息都会出现在当前窗口,所以此处加个判断,此窗口只接收当前聊天对象的消息,其他则忽略
   if(obj.from === toemail){
    var ans = '<div class="answer"><div class="heard_img left"><img src="'+toavatar+'"></div>';
     ans += '<div class="answer_text"><p>'+obj.content+'</p><i></i>';
     ans += '</div></div>';
     $('.speak_box').append(ans);
     for_bottom();
   }
  };
  ws.onerror = function(){
   console.log("error");
   var str = '<div class="question">';
   str += '<div class="heard_img right"><img src="/static/images/xitong.jpg"></div>';
   str += '<div class="question_text clear"><p>聊天服务器出现异常,暂时无法提供服务。</p><i></i>';
   str += '</div></div>';
   $('.speak_box').append(str);
   $('.write_box input').val('');
   $('.write_box input').focus();
   autoWidth();
   for_bottom();
  };

  function send() {
   var content = $('.write_box input').val();
  if(content === ''){
   alert('请输入消息!');
   $('.write_box input').focus();
  }else{
    var toemail = $("#toemail").val();
    var myemail = $("#myemail").val();
    var myavatar = $("#myavatar").val();
    var arr = {"flag":"msg","to":toemail,"from":myemail,"content":content};
    var msg = JSON.stringify(arr);
    console.log(msg);
    ws.send(msg); 
    var str = '<div class="question">';
    str += '<div class="heard_img right"><img src="'+myavatar+'"></div>';
    str += '<div class="question_text clear"><p>'+content+'</p><i></i>';
    str += '</div></div>';
   $('.speak_box').append(str);
   $('.write_box input').val('');
   $('.write_box input').focus();
   autoWidth();
   for_bottom();
   }
  
  }
 }else{
  alert("您的浏览器不支持 WebSocket!");
 }
   
 function for_bottom(){
 var speak_height = $('.speak_box').height();
 $('.speak_box,.speak_window').animate({scrollTop:speak_height},500);
 }
 
 function autoWidth(){
 $('.question_text').css('max-width',$('.question').width()-60);
 }
 
 autoWidth();
 
</script>

</body>
</html>

数据表结构:

CREATE TABLE `app_offline` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `from` varchar(50) DEFAULT NULL COMMENT '离线发送方',
 `to` varchar(50) DEFAULT NULL COMMENT '离线接收方',
 `content` varchar(1000) DEFAULT NULL COMMENT '发送的离线内容',
 `status` tinyint(4) DEFAULT '0' COMMENT '发送状态:0-未发送,1-已发送',
 `addtime` int(11) DEFAULT NULL COMMENT '发送方发送时间',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

具体效果:

PHP用swoole+websocket和redis实现web一对一聊天

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
加强版phplib的DB类
Mar 31 PHP
深入php中var_dump方法的使用详解
Jun 24 PHP
PHP父类调用子类方法的代码例子
Apr 09 PHP
用php守护另一个php进程的例子
Feb 13 PHP
PHP使用mysqldump命令导出数据库
Apr 14 PHP
php实现对两个数组进行减法操作的方法
Apr 17 PHP
php转换颜色为其反色的方法
Apr 27 PHP
整理php防注入和XSS攻击通用过滤
Sep 13 PHP
yii2超好用的日期组件和时间组件
May 05 PHP
Laravel 修改默认日志文件名称和位置的例子
Oct 17 PHP
Laravel 集成微信用户登录和绑定的实现
Dec 27 PHP
如何用PHP实现分布算法之一致性哈希算法
May 26 PHP
基于thinkphp6.0的success、error实现方法
Nov 05 #PHP
php实现JWT(json web token)鉴权实例详解
Nov 05 #PHP
详解Laravel服务容器的绑定与解析
Nov 05 #PHP
php+laravel依赖注入知识点总结
Nov 04 #PHP
PHP保存Base64图片base64_decode的问题整理
Nov 04 #PHP
详解laravel passport OAuth2.0的4种模式
Nov 04 #PHP
laravel返回统一格式错误码问题
Nov 04 #PHP
You might like
PHP实现抓取迅雷VIP账号的方法
2015/07/30 PHP
JS 强制设为首页的代码
2009/01/31 Javascript
编写自己的jQuery插件简单实现代码
2011/04/19 Javascript
JavaScript实现的in_array函数
2014/08/27 Javascript
简单介绍JavaScript中字符串创建的基本方法
2015/07/07 Javascript
jquery小火箭返回顶部代码分享
2015/08/19 Javascript
jquery实现叠层3D文字特效代码分享
2015/08/21 Javascript
JS实现网页上随滚动条滚动的层效果代码
2015/11/04 Javascript
Nodejs中的this详解
2016/03/26 NodeJs
深入理解Ajax的get和post请求
2016/06/02 Javascript
AngularJS中$watch和$timeout的使用示例
2016/09/20 Javascript
vue中如何实现变量和字符串拼接
2017/06/19 Javascript
JavaScript创建对象的七种方式(推荐)
2017/06/26 Javascript
微信小程序实现下拉刷新和轮播图效果
2017/11/21 Javascript
基于Vue2.X的路由和钩子函数详解
2018/02/09 Javascript
微信小程序使用swiper组件实现类3D轮播图
2018/08/29 Javascript
ES2020 已定稿,真实场景案例分析
2020/05/25 Javascript
CentOS中升级Python版本的方法详解
2017/07/10 Python
Python决策树和随机森林算法实例详解
2018/01/30 Python
python微信公众号之关注公众号自动回复
2018/10/25 Python
在Python 中同一个类两个函数间变量的调用方法
2019/01/31 Python
Python 用matplotlib画以时间日期为x轴的图像
2019/08/06 Python
python实现ssh及sftp功能(实例代码)
2020/03/16 Python
django迁移文件migrations的实现
2020/03/31 Python
Python 如何反方向迭代一个序列
2020/07/28 Python
python中random模块详解
2021/03/01 Python
HTML5 Canvas图像模糊完美解决办法
2018/02/06 HTML / CSS
墨西哥网上购物:Linio墨西哥
2016/10/20 全球购物
Public Desire美国/加拿大:全球性的在线鞋类品牌
2018/12/17 全球购物
一些PHP的面试题
2015/05/06 面试题
央视元宵晚会主持串词
2014/03/25 职场文书
2015年度质量工作总结报告
2015/04/27 职场文书
开学典礼校长致辞
2015/07/29 职场文书
关于国庆节的广播稿
2015/08/19 职场文书
HTML5来实现本地文件读取和写入的实现方法
2021/05/25 HTML / CSS
MYSQL中文乱码问题的解决方案
2022/06/14 MySQL