Thinkphp5使用workerman、socket、websocket、layui、layim建立即时通讯

在开始之前,有句话想说,曾经我以为socket会很难入门,所以为了节省时间,使用了ajax轮询的方式,最近项目不是很多,想起来优化一下曾经的项目,就准备引入socket代替ajax轮询,从开始到发出第一句话并接收处理用了大概一天时间,socket并不可怕,可怕的是我当初畏惧它的心。
好了,废话不多说,开始干活。

环境:阿里云ECS(windows) + Thinkphp5.0.24

在开始之前要先确定tp版本,tp5.0.24应该使用topthink/think-worker的 1.0 版本,刚搭好socket环境的时候,这个问题困扰我很久才解决,就是版本问题,tp5是不能使用高版本的think-worker的。
然后以下是我一步一步试验出来的流程:
一、要在服务器上开放端口,具体方法就是win+r -> 搜索防火墙 -> 高级设置 -> 入站规则 -> 新建规则 -> 端口,例如12138 -> 下 一步…… -> 保存
二、如果是阿里云ECS,要在ECS中建立安全组,ECS控制台 -> 具体的某个服务器 -> 安全组设置 -> 将刚刚开放的12138端口加入到安全组中
三、别忘了重启服务器,不知道这步适不适用于所有情况,反正我的服务器没重启端口就一直不生效,也是困扰了很久
端口配置完成之后,就要开始引包了,推荐使用composer引入
composer的具体使用方法请百度
项目引入成功后,可以测试一下端口是否可以成功ping通

端口通过之后就可以开始配置项目了:
在项目的访问根目录写一个开启服务的文件server.php:

#!/usr/bin/env php
<?php
define('APP_PATH', __DIR__ . '/../application/');
define('BIND_MODULE','admin/Socket');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';

server.php第四行代表具体workerman初始化文件在application/admin/controller下的socket.php,socket.php示例:

<?php
namespace application\admin\controller;
use think\worker\Server;
use Workerman\Lib\Timer;

error_reporting(E_ERROR | E_PARSE);
class Socket extends Server{
    protected $socket = 'websocket://0.0.0.0:12138'; //0.0.0.0表示内部、外部等均可访问
    protected $processes = 1; //因为业务需要向指定ID发送消息,此处进程数必须为1,若不需要可设置其他值
	protected $uidConnections = [];
	
	
    /**
     * 收到信息
     * @param $connection
     * @param $data
     */
    public function onMessage($connection, $data){
    	global $worker;
	    if(!isset($connection -> uid)){
	       	$connection -> uid = $data['u_id'];
	       	/* 
		    * 保存uid到connection的映射,这样可以方便的通过uid查找connection,
	        * 实现针对特定uid推送数据
	        */
	       	$worker -> uidConnections[$connection -> uid] = $connection;
	    }
		$connection -> lastMessageTime = time();
		
		//此处添加个人的业务代码,例如对数据的处理、对发送者的反馈或向对方发送消息等操作





    }

	//向指定ID发送消息
	private function sendMessageByUid($uid, $message){
	    global $worker;
	    if(isset($worker -> uidConnections[$uid])){
	        $worker -> uidConnections[$uid] -> send($message);
	    }
	}
	
    /**
     * 当连接建立时触发的回调函数
     * @param $connection
     */
    public function onConnect($connection){
		//此处可以判断连接数等	
    	
		
    }

    /**
     * 当连接断开时触发的回调函数
     * @param $connection
     */
    public function onClose($connection){
        global $worker;
	    if(isset($connection -> uid)){
	        // 连接断开时删除映射
	       unset($worker -> uidConnections[$connection -> uid]);
	    }
    }

    /**
     * 当客户端的连接上发生错误时触发
     * @param $connection
     * @param $code
     * @param $msg
     */
    public function onError($connection, $code, $msg){
        echo "error $code $msg\n";
    }

    /**
     * 每个进程启动
     * @param $worker
     */
    public function onWorkerStart($worker){
    	//进程启动时加入对每个连接的心跳判断,若55秒内没有对服务器发起过心跳或其他任何请求,则断开连接
    	Timer::add(1, function()use($worker){
	        $time_now = time();
	        foreach($worker -> connections as $connection) {
	            // 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
	            if (empty($connection -> lastMessageTime)) {
	                $connection -> lastMessageTime = $time_now;
					$u_ids[] = $connection -> uid;
	                continue;
	            }
	            // 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接
	            if ($time_now - $connection->lastMessageTime > 55) {
	                $connection -> close();
	            }
			}
	    });
    }
}

每次使用时,需要用命令行手动运行server.php来开启服务:
1.在cmd命令行中使用cd来进入到server.php所在目录
2.运行 php server.php 来开启服务,服务开启成功后,如下图提示:
在这里插入图片描述
需要注意的是,每次socket.php的代码更改后,需要重新开启一次服务,否则新代码将不会执行

后端配置完成后,开始配置前端,此处给出的是websocket,基于layui的示例。layui是一款优秀的前端框架,layui的即时通讯layim极为方便的解决了聊天界面的复杂实现且扩展容易。
layim的使用及引入请参考官方网站:layui

//首先判断浏览器是否支持websocket
if(typeof(WebSocket) == 'undefined'){
	layer.msg('你的浏览器不支持 WebSocket,无法进行聊天功能,推荐使用Google Chrome、Mozilla Firefox、360浏览器及QQ浏览器等');
}
//layui使用
layui.use('layim', function(layim){
	//layim的初始化配置
	layim.config({
		brief: false,//是否简约模式(如果true则不显示主面板)
    	title: '客服会话', //主面板最小化后显示的名称
    	min: false, //用于设定主面板是否在页面打开时,始终最小化展现
    	isAudio: false, //是否开启聊天工具栏音频
    	isVideo: false, //是否开启开启聊天工具栏视频
    	notice: false, //是否开启桌面消息提醒,即在浏览器之外的提醒
    	voice: false, //不开启声音提示
    	isfriend: true, //是否开启好友
    	isgroup: false, //是否开启群组
    	maxLength: 3000, //可允许的消息最大字符长度
  		chatLog: layui.cache.dir + 'css/modules/layim/html/chatlog.html', //历史记录模板
    	init: { //获取主面板列表信息
    		url: '', //获取好友列表的接口地址
		  	type: 'get', //默认get,一般可不填
		  	data: { 
		  		//额外参数
		  		
		  	}
    	},
    	uploadImage: {
		  	url: '', //图片上传地址
		} 
 
  	});
	var so = new WebSocket('ws://域名或IP:12138');
	//websocket心跳
	var heartCheck = {
	    timeout: 55000, //55秒心跳一次,告诉服务器,这个连接还接着用
	    timeoutObj: null,
	    reset: function(){
	        clearTimeout(this.timeoutObj);
	    	this.start();
	    },
	    start: function(){
	        this.timeoutObj = setTimeout(function(){
				console.log('我心跳了');
	        }, this.timeout);
	    },
	    remove: function(){
	    	clearTimeout(this.timeoutObj);
	    }
	}
	
	//连接成功时触发
	so.onopen = function(){
		console.log('我上线了');
		//开启心跳
    	heartCheck.start();
	};
	//so.readyState属性值
//	0 :对应常量CONNECTING (numeric value 0),
//	 正在建立连接连接,还没有完成。The connection has not yet been established.
//	1 :对应常量OPEN (numeric value 1),
//	 连接成功建立,可以进行通信。The WebSocket connection is established and communication is possible.
//	2 :对应常量CLOSING (numeric value 2)
//	 连接正在进行关闭握手,即将关闭。The connection is going through the closing handshake.
//	3 : 对应常量CLOSED (numeric value 3)
//	 连接已经关闭或者根本没有建立。The connection has been closed or could not be opened.
  	so.onclose = function(){
    	console.log('会话已关闭');
    	heartCheck.remove();
    }
    //发送消息时执行
  	layim.on('sendMessage',function(res) {
		var mine = res.mine; //包含我发送的消息及我的信息
		var to = res.to; //对方的信息
		so.send(JSON.stringify({
			//可加其他参数
			type: 'chatMessage',//随便写,需要时对应上即可
			data: res
		}));
	});
	//监听收到的聊天消息
	so.onmessage = function(res) {
		//重置心跳
		heartCheck.reset();
		//官方示例是JSON.parse(),我在使用的时候不生效,试了不少方法,随后eval()可以解析
		var msg_res = eval('('+ res.data +')');
		//以下为我写的业务代码,收到消息时有code,并对参数进行了处理
		if(msg_res.code == 200){
			if(msg_res.addfriend == 1){
				//添加好友
				layim.addList(msg_res);
			}else if(msg_res.send == 1){
				//系统消息
				if(msg_res.system == 1){
					layim.getMessage({
					  	system: true
					  	,id: msg_res.to_id
					  	,type: "friend"
					  	,content: msg_res.msg
					});
				}else{
					layim.getMessage(msg_res);
				}
			}
		}else{
			//弹出框报错
			layer.msg(msg_res.msg);
		}
	};
	
});

以上就是socket的简单使用方法,更多逻辑操作请参照自身的业务。本文只是为了解决一些简单的入门问题,重在抛砖引玉,各位看官如有意见及建议请留言,在下必然回复您的厚爱。

<!--结束,谢谢,再见-->
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章