声明:该聊天室目前只有一对多,一对一的聊天功能,另外,因为没有使用到mysql,所以还存在比较多的缺陷地方,但知道原理就差不多了,这里主要分享下swoole简易的聊天室制作思路。
开发环境:centos7、redis、swoole
首先看看效果图
难点:这里一对多比较容易实现,就是简单的用h5里websocket特新+swoole,但是一对一比较难实现,因为具体到信息发到哪个人,信息接收等问题
首先看前端js的代码:
<script> $('.welcome').text('欢迎你进入聊天!'); //最开始进入聊天室时提示欢迎 var wsurl = "ws://www.junheng.ink:9501"; var websocket = new WebSocket(wsurl); //申请一个WebSocket对象 websocket.onopen = function(evt){ //当websocket创建成功时,会触发onopen事件 console.log("hello swoole"); } websocket.onmessage = function(evt){ //当客户端收到服务端发来的消息时,会触发onmessage事件 msg(evt.data); console.log("swoole-server-return-msg:" + evt.data ); } websocket.onclose = function(evt){ //当客户端收到服务端发送的关闭连接的请求时,触发onclose事件 console.log("bye swoole"); } websocket.onerror = function(evt,e){ //若中途出现失败,会触发onerror事件 console.log("error" + evt.data); } function speak_all(){ //这是用于发送消息给所有人的函数 let content = $("#chat_bottom_input").val(); data=JSON.stringify({content:content,type:"chat"}); console.log(data); websocket.send(data); } function speak_one(){ //这是用于发送消息给所有人的函数 let listener = $("#listener").text(); let content = $("#chat_bottom_input_ptp").val(); console.log(content); console.log(listener); data=JSON.stringify({content:content,listener:listener,type:"ptp_speak"}); console.log(data); websocket.send(data); } function msg(data){ //这是在onmessage函数里面执行的(收到服务器返回信息时做对应操作) var data=JSON.parse(data); switch(data.type) { case 1: //1代表统计在线总人数 $(".total").html(data.usernum); break; case 2: //2代表在一对多情况下发信息、接信息两种情况 console.log(data); var html =""; if(data.speaker==1){ html +='<div class="speak"><div class="my_picture"><span style="display:none">'+data.user_id+'</span><img class="pic" src="'+data.headimg+'"><div class="dialogue">'+data.user+'</div></div><span class="right_triangle"></span><div class="my_talk"><p class="talk_content">'+data.content+'</p></div><div class="my_nowtime">'+data.nowtime+'</div></div>'; }else{ html +='<div class="speak"><div class="picture" onclick="ptp(this)"><span style="display:none">'+data.user_id+'</span><img class="pic" src="'+data.headimg+'"><div class="dialogue">'+data.user+'</div></div><span class="left_triangle"></span><div class="talk"><p class="talk_content">'+data.content+'</p></div><div class="nowtime">'+data.nowtime+'</div></div>'; } $("#comments").append(html); $("#chat_bottom_input").val(""); var scheight=$("#chat_center")[0].scrollHeight; var ofheight=$("#chat_center").outerHeight(); $("#chat_center").scrollTop(scheight-ofheight); break; case 3: //3代表进入一对一时头部填上对应是哪个人 $("#listener").html(data.listener); console.log(data); break; case 4: //4代表一对一时自己发送消息 console.log(data); var html =""; html +='<div class="speak"><div class="my_picture"><img class="pic" src="'+data.speaer_headimg+'"><div class="dialogue">我:'+data.speaker+'</div></div><span class="right_triangle"></span><div class="my_talk"><p class="talk_content">'+data.content+'</p></div><div class="my_nowtime">'+data.nowtime+'</div></div>'; $(".new_msg").append(html); $("#chat_bottom_input_ptp").val(""); var scheight=$("#chat_center_ptp")[0].scrollHeight; var ofheight=$("#chat_center_ptp").outerHeight(); $("#chat_center_ptp").scrollTop(scheight-ofheight); break; case 5: //5代表一对一时别人发送消息 console.log(data); var html =""; html +='<div class="speak"><div class="picture""><img class="pic" src="'+data.speaker_headimg+'"><div class="dialogue">用户:'+data.speaker+'</div></div><span class="left_triangle"></span><div class="talk"><p class="talk_content">'+data.content+'</p></div><div class="nowtime">'+data.nowtime+'</div></div>'; $(".new_msg").append(html); var scheight=$("#chat_center_ptp")[0].scrollHeight; //这下面是把滚动框拉到最后意思 var ofheight=$("#chat_center_ptp").outerHeight(); $("#chat_center_ptp").scrollTop(scheight-ofheight); break; } } function ptp(event){ //这是点击了某个人头像,需要进行一对一聊天时触发的函数 user_id=event.children[0].innerText; let data=JSON.stringify({user_id:user_id,type:"ptp"}); console.log(data); websocket.send(data); //这里把被点击人的id传给后台swoole,后面它根据这个id发一对一的信息 $("#container_room").css("display","none"); $("#container_ptp").css("display","flex"); } function back(){ //这是一对一时点击了返回大厅按钮触发的函数 $(".new_msg").empty(); $("#container_room").css("display","flex"); $("#container_ptp").css("display","none"); let data=JSON.stringify({type:"ptp",want:"close"}); console.log(data); websocket.send(data); //这里发送信息是为了关闭swoole的定时器(用来检测是否有收到信息) var scheight=$("#chat_center")[0].scrollHeight; var ofheight=$("#chat_center").outerHeight(); $("#chat_center").scrollTop(scheight-ofheight); } </script>
然后再看看完整的后端代码
date_default_timezone_set('PRC'); class Ws{ /** * 这是定义静态redis方法,方便后面使用 */ static public function myRedis(){ $redis = new Redis(); $redis->connect('127.0.0.1', 6379); return $redis; } public $ws = null; public function __construct(){ $this->ws = new swoole_websocket_server("0.0.0.0",9501); //创建websocket服务器 $this->ws->set([ 'worker_num' => 4, //设置进程数 'task_worker_num' => 4 //设置task任务数 ]); $this->ws->on("start",[$this,'onStart']); //这是最开始创建时触发start事件 $this->ws->on("open",[$this,'onOpen']); //这是前端的websocket连接时触发事件 $this->ws->on("message",[$this,'onMessage']); //这是接收到前端发来的信息时触发事件 $this->ws->on("task",[$this,'onTask']); //这是用来分发异步任务时触发事件 $this->ws->on("finish",[$this,'onFinish']); //这是task的回调函数 $this->ws->on("close",[$this,'onClose']); //这是关闭时的触发的事件 $this->ws->start(); //启动websocket服务器 } /** * 为进程设置别名 */ public function onStart(){ swoole_set_process_name("swoole_chat"); //为进程id添加别名 } /** *监听打开事件 */ public function onOpen($ws,$request){ //可以用定时器监听是否有其他人的信息 // swoole_timer_tick(2000,function($time_id){ // echo "有消息吗?2s-time_id:{$time_id} "; // }); $redis = self::myRedis(); //这里用redis随机存取出头像 if(!$redis->sMembers('headimg')){ $redis->sAdd('headimg',"http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eokKDRjQuyqFIkpziaNalabDNicPibV3RJk0CmxdWSiaOUgYrqvucyGvHQ6oGHHwxYgYsC6LOcrYaGJ9A/132"); $redis->sAdd('headimg',"http://thirdwx.qlogo.cn/mmopen/vi_32/maKfSQsic5IOYjicXszArwLZOWbyW2zWj7TYzsDhgAbYgXnvehpjqhWmcYE91JyzFaJS2Dj6wMHdJvewRnVofeYw/132"); $redis->sAdd('headimg',"http://thirdwx.qlogo.cn/mmopen/vi_32/zr58J0258Ghbc8vtNhxZIyIUQwgOQnTn6NAnr1QYqe1CIm0uuBPkQwDA7cpY0JFvI6vhlwsDOEJJmkOwTDibLgQ/132"); $redis->sAdd('headimg',"http://thirdwx.qlogo.cn/mmopen/vi_32/u6KWfAsSuTJcvMvZOB5JmxZ9nnHZ4x4oofJ2COLvWcicD5F1fzibbYwyR4INb5CgS7nlnH9nlr2fpMgbuHfOEWBw/132"); $redis->sAdd('headimg',"http://thirdwx.qlogo.cn/mmopen/vi_32/zKKK0ZAicRAmPcU4FaOwvwOOVxSrP9icDBLHHhlDG7bJ4N7pePJEedVjThSZWTTlibLtTQsVSnzeJcQzkPTOfQL9Q/132"); $redis->sAdd('headimg',"http://thirdwx.qlogo.cn/mmopen/vi_32/IibMjuSiblgx57qfTvkKPePC8vvyjm0bBM852u0vS6GPvNEBLvMdKnzugibYJzVkOSuAXFlCRicicW1Cfpa0S8gIsibA/132"); $redis->sAdd('headimg',"http://thirdwx.qlogo.cn/mmopen/vi_32/ViaqjkbzPo33fqunp7vsJzzFSJeAiaQzCeaIALRnAPwHibibpFYm0RQ2VlDXasT5iapELic8McaZbC0cLEHTibtNXwsZA/132"); } //保存头像和用户id,使用户和对应头像关联起来 $this->getheader($request->fd); //把连接的用户全部放进chat集合 $redis->sAdd('chat',$request->fd); print_r("swoole服务端连接成功:{$request->fd}".PHP_EOL); //用task任务统计在线人数 $data=[ 'type' => 1 //1代表用于统计在线人数 ]; $this->ws->task($data); //分发异步任务 } /** *监听前端消息事件 */ public function onMessage($ws,$frame){ $redis = self::myRedis(); $content=$frame->data; $content=json_decode($content,true); switch($content['type']){ case 'chat': //这是一对多时触发 echo "ser-push-message:{$content['content']} "; //从hash结构取出对应用户的头像 $headimg=$redis->hGet('links',$frame->fd); //用task任务进行聊天 $data=[ 'user_id' => $frame->fd, 'user' => "用户:".$frame->fd, 'content' => $content['content'], 'nowtime'=> date("Y-m-d H:i:s"), 'type' => 2, //2代表聊天发信息 'speaker' => $frame->fd, 'headimg' => $headimg ]; $this->ws->task($data); break; case 'ptp': if(isset($content['want'])){ //这是点击了退出一对一的聊天,返回大厅按钮 $user_fd='用户'.$frame->fd; $tid=$redis->get($user_fd); $tid=intval($tid); swoole_timer_clear($tid); //关闭检测对方是否有发信息的定时器 }else{ //这是与某个人一对一聊天 echo "这是测试的:".$frame->fd; var_dump($frame->fd); $host=$frame->fd; $listener_id=$content['user_id']; echo "ser-push-message:".'与'."{$listener_id}".'聊天'." "; $res=$redis->HMGET($frame->fd,['content']); var_dump($res); $data=[ 'listener' => $listener_id, 'nowtime'=> date("Y-m-d H:i:s"), 'type' => 3, //3代点击了与某个人聊天 'speaker' => $frame->fd, ]; //定时器用于一对一时查看对方是否有发送消息 $timer_id=swoole_timer_tick(2000,function($time_id) use($host,$redis,$listener_id){ echo "host:".$host." listener:".$listener_id." 1s-time_id:{$time_id} "; $res=$redis->hMget($host,['speaker']); if($res['speaker']&&$res['speaker']==$listener_id){ $message=$redis->hMget($host,['speaker_headimg','speaker','content','nowtime','listener']); //获取这个人发来的信息 $data=[ 'content' => $message['content'], 'nowtime' => $message['nowtime'], 'listener' => $message['listener'], 'speaker_headimg' => $message['speaker_headimg'], 'speaker' => $message['speaker'], 'time_id' => $time_id, 'type' => 5, //定时器接收信息 ]; $this->ws->task($data); } }); $user_fd='用户'.$frame->fd; $redis->set($user_fd,$timer_id); $this->ws->task($data); } break; case 'ptp_speak': //一对一发送内容时触发 $listener_id=$content['listener']; echo "ser-push-message:{$frame->fd}".'与'."{$listener_id}".'聊天'." "; //从hash结构取出对应用户的头像 $speaker_headimg=$redis->hGet('links',$frame->fd); //$listener_headimg=$redis->hGet('links',$listener_id); //用task任务进行聊天 $nowtime=date("Y-m-d H:i:s"); $data=[ 'listener' => $listener_id, 'nowtime'=> $nowtime, 'type' => 4, //4代与某个人聊天 'speaker' => $frame->fd, 'speaer_headimg' => $speaker_headimg, 'content' => $content['content'], // 'listener_headimg' => $listener_headimg ]; //设置信息发送给谁的内容 $redis->hMset($listener_id,['speaker'=>$frame->fd,'speaker_headimg'=>$speaker_headimg,'listener'=>$listener_id,'content'=>$content['content'],'nowtime'=>$nowtime]); $this->ws->task($data); break; } } /** *执行task任务 */ public function onTask($serv,$taskId,$workerId,$data){ $redis = self::myRedis(); switch($data['type']){ case 1: $users=$redis->sMembers('chat'); //获取到所有在线人的id $data['usernum']=$redis->scard('chat'); //统计在线人成员数 foreach($users as $user){ $this->ws->push($user,json_encode($data)); //把这个在线成员数发给每一个人 } return "task-people finish"; break; case 2: $speaker=$data['speaker']; $users=$redis->sMembers('chat'); //获取chat集合的所有在线人的id foreach($users as $user){ if($user==$speaker){ //如果这个id等于当前人的id,则定义speak字段为1,聊天信息框在右侧 $data['speaker']=1; $this->ws->push($user,json_encode($data)); }else{ $data['speaker']=0; //如果这个id等于当前人的id,则定义speak字段为1,聊天信息框在左侧 $this->ws->push($user,json_encode($data)); } } return "task-chat finish"; break; case 3: $this->ws->push($data['speaker'],json_encode($data)); //3代表点击了某个人一对一时,把这个人的id放在头部 return "task3"; break; case 4: $this->ws->push($data['speaker'],json_encode($data)); //4代表一对一时我方发送信息,然后我方需要显示出来 return "task4"; break; case 5: $this->ws->push($data['listener'],json_encode($data)); //5代表一对一时收到了对方的信息,然后我方显示出来 $redis->del($data['listener']); return "task2".$data['time_id']; break; } } /** *task结束后执行的,在cli可以看到 */ public function onFinish($serv,$taskId,$data){ echo "finish-success:{$data} "; } /** *关闭时触发 */ public function onClose($ws,$fd){ $redis = self::myRedis(); $redis->sRem('chat',$fd); //用户关闭时,把这个人的id从chat集合中移除 $data=[ 'type' => 1 //标记类型为统计在线人数 ]; $this->ws->task($data); //执行task任务,把在线人数重新计算发给全部在线用户 echo "close:{$fd}"; } //把头像和用户id放进redis的hash结构 public function getheader($link_id){ $redis = self::myRedis(); $headimg=$redis->sPop('headimg'); //随机在头像集合中取一个 $redis->hSet('links',$link_id,$headimg); //把用户的id和头像存进hash结构里面 } } $obj = new Ws();
因为基本主要的代码都放出来了,并且都有注释,所以就不过多的解释了。
然后就再说一下这聊天时的整个运作流程:
用户进入聊天室 -> 前端触发onopen -> 后端onopen -> 前端发信息send -> 后端接收信息onmessage -> 前端接收后端的返回信息onmessage -> 前后端监听关闭onclose
最后,因为这个是个人的练手学习笔记,所以希望大家不喜勿喷,如果有问题的可以给我留言哦!