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 相关文章推荐
php基础知识:类与对象(2) 自动加载对象
Dec 13 PHP
PHP 截取字符串 分别适合GB2312和UTF8编码情况
Feb 12 PHP
PHP 进程锁定问题分析研究
Nov 24 PHP
PHP+ACCESS 文章管理程序代码
Jun 21 PHP
PHP中的函数嵌套层数限制分析
Jun 13 PHP
php生成二维码的几种方式整理及使用实例
Jun 03 PHP
php检测iis环境是否支持htaccess的方法
Feb 18 PHP
PHP延迟静态绑定示例分享
Jun 22 PHP
完美解决thinkphp验证码出错无法显示的方法
Dec 09 PHP
PHP加密解密类实例代码
Jul 20 PHP
PHPCMS2008广告模板SQL注入漏洞修复
Oct 11 PHP
浅谈php常用的7大框架的优缺点
Jul 20 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重新实现PHP脚本引擎内置函数
2007/03/06 PHP
PHP URL路由类实例
2013/11/12 PHP
php.ini save_handler 修改不生效的解决办法
2014/07/22 PHP
PHP中使用Imagick读取pdf并生成png缩略图实例
2015/01/21 PHP
php curl请求信息和返回信息设置代码实例
2015/04/27 PHP
PHP单例模式是什么 php实现单例模式的方法
2016/05/14 PHP
详解Yii2 定制表单输入字段的标签和样式
2017/01/04 PHP
yii2 commands模式以及配置crontab定时任务的方法
2017/08/19 PHP
THINKPHP3.2使用soap连接webservice的解决方法
2017/12/13 PHP
php使用fputcsv实现大数据的导出操作详解
2020/02/27 PHP
js 获取计算后的样式写法及注意事项
2013/02/25 Javascript
整理的比较全的event对像在ie与firefox浏览器中的区别
2013/11/25 Javascript
常用的几段javascript代码分享
2014/03/25 Javascript
jQuery+css3动画属性制作猎豹浏览器宽屏banner焦点图
2015/03/16 Javascript
使用CSS+JavaScript或纯js实现半透明遮罩效果的实例分享
2016/05/09 Javascript
JavaScript实现区块链
2018/03/14 Javascript
Vue.js中对css的操作(修改)具体方式详解
2018/10/30 Javascript
基于Vue插入视频的2种方法小结
2019/04/02 Javascript
微信小程序云开发如何使用云函数生成二维码
2019/05/18 Javascript
Vue项目中使用WebUploader实现文件上传的方法
2019/07/21 Javascript
vue 解决form表单提交但不跳转页面的问题
2019/10/30 Javascript
基于vue+element实现全局loading过程详解
2020/07/10 Javascript
Python3读取zip文件信息的方法
2015/05/22 Python
50行Python代码实现视频中物体颜色识别和跟踪(必须以红色为例)
2019/11/20 Python
Python类继承和多态原理解析
2020/02/05 Python
突破canvas语法限制 让他支持链式语法
2012/12/24 HTML / CSS
全球速卖通法国在线交易平台:AliExpress法国
2017/07/07 全球购物
Club Monaco加拿大官网:设计师男女服装
2019/09/29 全球购物
计算机专业毕业生的自我评价
2013/11/18 职场文书
2014厂务公开实施方案
2014/02/17 职场文书
数字化校园建设方案
2014/05/03 职场文书
2015年世界环境日活动总结
2015/02/11 职场文书
货款欠条范本
2015/07/03 职场文书
优秀学生主要事迹怎么写
2015/11/04 职场文书
创业计划书介绍
2019/04/24 职场文书
vue实现书本翻页动画效果实例详解
2022/04/08 Vue.js