上一个Demo里,在启动时注册了支持websocket的组件、解析器,在到映射到的路由,实现了简单的通信,在开发中也是现学现卖,摸着石头过河,难度也是不小的,机会和挑战总是成对出现的,所以要加油加油再加油,边学习边总结,才能有更大的进步。
用以下几个主要功能点:
1 = HEART // 心跳
2 = LOGIN // 登录
3 = LOGOUT // 退出登录
4 = COMMENT // 评论
5 = PULL // 拉取
6 = NOTIFICATION // 通知
7 = READ // 已读
8 = RECEIVED // 收到确认
先从tcp协议说起,tcp的连接需要三次握手和四次挥手,在反复链接的过程中
先从tcp协议说起,tcp创建一个连接有三次握手,而断开一个连接有四次挥手。不管是服务器还是客户端发起连接的关闭,都会完整的走完四次挥手的过程,这样,一切很完美,系统回收这个fd,应用层也可以通过onClose回调处理相关的事情.
fd是需要重点理解的一个知识点 fd学名是文件描述符,在unix的哲学就是一切皆文件中,这个fd就是系统层暴露给业务层的用来表示一个五元组网络连接的标识。你可以简单的理解为一个索引,通过对这个fd的操作,系统层可以找到相应的连接而且进行的一系列操作,如发送数据到网瞳,进行连接关闭等等。
心跳检测存在的意义就是系统fd资源是有限的,当检测链接已断开时,在系统级别进行主动回收资源,判定时间的间隔,两次交互时间超过设置的时长,就主动断开链接。
配置文件dev.php
// 表示一个连接如果300秒内未向服务器发送任何数据,此连接将被强制关闭
'heartbeat_idle_time' => 180 ,
// 表示每60秒遍历一次
'heartbeat_check_interval' => 60 ,
断线重连最多三次,时间间隔为60s、120s、180s,是上次交互的时间 与检测时候的时间相差180s,就被被踢出。
在解析器里做了一层分发,把请求数据按客户端类型进行区分,Pc、H5、Ios、Android,出于代码严谨程度的考虑,在所有的Controller上规定了一个interface接口
,这个可以保证各端方法的统一,还可以增加在各类型上的个性化。
/**
* Interface iMessage websocket 控制器继承接口
* 1 = HEART // 心跳
* 2 = LOGIN // 登录
* 3 = LOGOUT // 退出登录
* 4 = COMMENT // 评论
* 5 = PULL // 拉取
* 6 = NOTIFICATION // 通知
* 7 = READ // 已读
**/
namespace App\Library\IMessage;
interface iMessageController {
function heart();
function login();
function logout();
function comment();
function pull();
function notification();
function read();
}
Contoller 代码展示:
namespace App\WebSocketController\V1;
class PcMessage extends Base implements iMessageController
{
//code 略...
}
每次token验证成功,建立长链接时,用redis的 有序集合(sorted set)做存储类型,主要考虑的点有两个,有序集合中可以保证value是唯一的,这样就能保证一对一的uid和fd的对应关系,还有一点是这次开发还有广播的通知需求,这样可以更好的为广播服务。
/**
* login 登录方法
*/
public function login()
{
//验证参数代码略....
$fd = $this->caller()->getClient()->getFd();
$zaddRet = $redis->zAdd('PUSH_MSG_ZSET', $fd, $uid);
$noce_ack = $this->getNoceAck();
$pushMsg = [
'noce_ack' => $noce_ack,
'uid' => $uid,
'to_uid' => 0,
'is_sent' => 1,
'msg_type' => 2,
'client_type' => 1,
'create_time' => time(),
'update_time' => time()
];
$listRet = $redis->lPush('PUSH_MSG_LISTS', json_encode($pushMsg));
$model = PushMsgModel::create($pushMsg);
$ret = $model->save();
echo 'sql执行结果:'.$ret;
//返回结果进行处理
$result = [
'uid' => $uid,
'msg_type' => 2,
'code' => 200,
'msg' => 'SUCCESS',
'body' => '',
'noce_ack' => $noce_ack,
];
return $this->response()->setMessage(json_encode($result));
}
重点:这里需要注意的是对应关系保证唯一,一个uid不允许对应两个fd,会造成推送不准的问题 解决方案应该在关闭链接的时候清除对应的记录。
开始的时候,我犯了一个错误,以为响应的就是推送出去的数据,响应和推送是两个兴致不同的事情
,WS的原理是一次握手,就不会断开了,可以进行实时的交互。响应和推送都是链接之后的反应。
实现步骤,Controller的具体代码
use EasySwoole\EasySwoole\ServerManager;
class PcMessage extends Base implements iMessageController
{
public function pushMsg(){
//实例化一个websocket server对象
$server = ServerManager::getInstance()->getSwooleServer();
//获取用户的fd
$fd = $this->caller()->getClient()->getFd();
$data = [
'uid' => 17000223,
'noce_ack' => '453DF8E5-296A-3DF3-EB47-DD6425889D75'
];
//推送消息
$server->push($fd, json_encode($data));
//发送响应消息
$this->response()->setMessage('this is heart');
}
}
在进行消息推送时,要记录通信的ack标识码,可以由服务端实现,也可以由客户端实现,这个项目由服务端实现,如:9857315A-CFFF-E845-F603-3068DEFFEAD5
:
/**
* @return string 生成通信唯一的ack标识
*/
protected function getNoceAck(){
mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
$charid = strtoupper(md5(uniqid(rand(), true)));
$hyphen = chr(45);// "-"
$noce_ack =// "{"
substr($charid, 0, 8).$hyphen
.substr($charid, 8, 4).$hyphen
.substr($charid,12, 4).$hyphen
.substr($charid,16, 4).$hyphen
.substr($charid,20,12);
return $noce_ack;
}
因为时间关系,就整理到这儿,onClose的时候需要回收集合里的对应关系的问题还没有找到,有小伙伴发现可以在底下留言,互相学习。