Swoole实现h5版聊天室笔记

声明:该聊天室目前只有一对多,一对一的聊天功能,另外,由于没有使用到mysql,因此还存在比较多的缺陷地方,但知道原理就差很少了,这里主要分享下swoole简易的聊天室制做思路。javascript

开发环境:centos七、redis、swoolephp

首先看看效果图css

            

难点:这里一对多比较容易实现,就是简单的用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}\n";
		// });
		$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']}\n";
				//从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}".'聊天'."\n";
				$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}\n";
					$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}".'聊天'."\n";
				//从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}\n";
	}

	/**
	 *关闭时触发
	 */
	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

最后,由于这个是我的的练手学习笔记,因此但愿你们不喜勿喷,若是有问题的能够给我留言哦!html