TP5.0+Swoole搭建websocket服务,实现pc端与⼩程序端即时聊天功能
前⾔
最近接触学习了⼀点swoole知识,想动⼿练习⼀下,⼀来闲来⽆事,⼆来巩固知识,于是便萌⽣了⽤swoole搭建websocket,实现⼩程序与pc后台即时通信功能。第⼀此做类似这样通讯功能,也没有完整清晰的思路,做的有点缓慢,在做的过程中⼀步步思考思路才慢慢清晰出来。
正⽂
1. 在TP5项⽬中搭建websocket服务
在项⽬根⽬录下创建server⽂件夹,在其下新建ws.php⽂件,作为创建ws服务的脚本。
<?php
use app\common\Websocket;
class Ws {
const HOST = "0.0.0.0";
const PORT = 8080;
public $ws = null;
public function __construct()
{
$this->ws = new swoole_websocket_server(self::HOST, self::PORT, SWOOLE_BASE, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$this->ws->set([
'document_root' => __DIR__.'/../public',
'worker_num' => 2,
'ssl_key_file' => '/usr/xxx.key',
'ssl_cert_file' => '/'
]);
$this->ws->on('open', [$this, 'onOpen']);
$this->ws->on('message', [$this, 'onMessage']);
$this->ws->on('workerstart', [$this, 'onWorkerstart']);
$this->ws->on('close', [$this, 'onClose']);
$this->ws->start();
}
//客户端连接后
public function onOpen($server, $request)
{
Websocket::init()->open($this->ws, $request);
}
/
/客户端发送消息
public function onMessage($server, $fram)
{
Websocket::init()->sendMsgAndSave($this->ws, $fram->fd, json_decode($fram->data));
}
public function onWorkerstart($server, $worker_id)
php项目搭建{
// 定义应⽤⽬录
define('APP_PATH', __DIR__ . '/../application/');
// 1. 加载基础⽂件
require __DIR__ . '/../thinkphp/start.php';
Websocket::init()->flushCache();
}
//客户端断开连接
public function onClose($server, $fd, $reactorId)
{
Websocket::init()->removeRedisCache($fd);
}
}
new Ws();
上⽅可以看出,我是以8080端⼝作为websocket服务,因为⼩程序只⽀持wss连接,所以需要⾃⼰申请域名ssl证书。(原来⼩程序我记得好像后台设置可信域名只⽀持80或443端⼝,不知道哪个版本更新,现在⽀持其他端⼝的设置。)
上⽅四个回调函数⾥⾯具体做了哪些事情我们先不急看。先看下onWorkerstart回调函数中,其实就是TP5.0⼊⼝⽂件所做的事情,载⼊框架信息。(Tips:因为swoole是常驻内存的,所以当改变websocket服务所调⽤的⽅法,或者框架核⼼代码,需要重启websocket服务)
在linux服务器中,进⼊根⽬录下执⾏:php ./server/ws.php 此时websocket服务端便运⾏起来。
2. ⼩程序端连接
var socketTask = wx.connectSocket({
url: 'wss://:8080?token=' + token,
header: {
'content-type': 'application/json'
},
method: 'GET',
success: function (obj) {
},
fail: function (err) {
wx.showToast({
title: 'websocket连接失败',
icon: 'none'
})
}
})
const version = wx.getSystemInfoSync().SDKVersion
if (thispareVersion(version, '1.7.0') >= 0) {
console.log('close', obj)
})
console.log('open', obj)
})
console.log('error', obj)
})
var app = getApp()
adnum = adnum + 1
if (adnum>0) {
wx.setTabBarBadge({
index: 3,
text: ''+adnum
})
}
})
这段连接websocket代码的运⾏时机我是放在了app.js onLaunch函数中,使得⼩程序刚运⾏时就连接websocket服务,这样做事为了当有新客服消息时动态改变tabbar上的⼩红点值,从onMessage回调函数中就可以看出。然后将返回的socketTask 对象存⼊到全局变量globalData中,后期进⼊到聊天页⾯,⽤来重写onMessage回调函数。
这⾥的onMessage监听函数主要是接受到新消息后,动态设置未读消息⼩红点的值。
从上⽅连接url中可以看到,我是传⼊了⼀个token参数。为什么要传此参数呐?因为每个⽤户进⼊⼩程序都会调⽤的wx.login后台拿到此⽤户的唯⼀标识openid,但是openid不便作为参数来当做调⽤⼩程序各个接⼝的凭证,于是后台根据⼀定规则⽣成token令牌,后期⽤户只要拿着此令牌调⽤接⼝,后台会解析token以便对应上此⽤户标识openid。
⼩程序连接成功后,会调⽤websocket服务四个⽅法中的onOpen回调函数,内部执⾏了Websocket::init()->open($this->ws, $request);这个⽅法。我们进⼊这个⽅法看看具体做了那些事情:
public function open($ws, $request)
{
$fd = $request->fd;
$getArr = $request->get; //获得所有get参数
if (!empty($getArr) && isset($getArr['token']) && !empty($getArr['token'])) {
if ($getArr['token'] == config('queue.websocket_kefu')) { //客服连接
return Predis::getInstance()->set($getArr['token'].$fd, $fd); //缓存客服标识 => fd
} else { //⽤户连接
$cacheRst = cache($getArr['token']);
if ($cacheRst){
if (!is_array($cacheRst)){
$cacheRst = json_decode($cacheRst,true);
}
if (array_key_exists('openid', $cacheRst)){
return Predis::getInstance()->set($cacheRst['openid'], $fd); //缓存openid => fd
}
}
}
}
return $ws->close($fd);
}
我们可以看出⾥⾯具体做了两件事:
判断是否有传⼊token变量,没有或者为空则认为是⾮法连接,调⽤close主动关闭连接
fd(客户端连接的唯⼀表⽰,以后可⽤来
将ws->push($fd)推送消息)、和openid⽤户的唯⼀标识关联起来,存⼊redis缓存。形如:openid=>fd
这样便可以将客户端推送的消息与openid连接起来,便知道是哪⼀位客户推送的。
(因为token令牌是有过期时间的,所以不能⽤token令牌作为⽤户标识。因为我这个⼩程序没有绑定开放平台,所以openid可以作为唯⼀标识,若⼩程序绑定了开放平台,请⽤unionid作为唯⼀表⽰)
3. ⼩程序推送消息
我们进⼊⼩程序客服聊天页⾯,⽤户输⼊⽂字点击发送⾛的下⽅代码:
const socketTask = app.globalData.socketTask
var data = { msg: message, type: '2'}
var that = this
socketTask.send({
data: JSON.stringify(data),
success:function(){
that.setData({
message: '',
msg_list: that.data.at({
type: 2,
msg: message,
date: ''
})
})
},
fail:function(){
chat._showModal('错误','发送失败,请联系管理员',()=>{},false)
}
})
上⾯代码主要做了两件事:①拿到全局socketTask对象,调⽤send⽅法发送数据。②将此数据绑定渲染到页⾯上。
当客户端调⽤send⽅法发送数据时,服务端就会⾛onMessage回调函数,⾥⾯主要⾛了Websocket::init()->sendMsgAndSave($this->ws, $fram->fd, json_decode($fram->data));此⽅法:
/**
* 客户端发送数据,并保存
* @param int $sfd 哪个客户端发送的
* @param object $data {msg:发送的数据;type:哪⽅发送的;openid:客户标识} 1客服发送给⽤户 2⽤户发送给客服
*/
public function sendMsgAndSave($ws, $sfd, $data)
{
if ($data->type && $data->msg) {
if ($data->type == 1 && !empty($data->openid)) {
$fd = Predis::getInstance()->get($data->openid);
if ($fd) {
$ws->push($fd, json_encode(['msg' => $data->msg]));
}
$sql = "insert into `chat` (msg,openid,type,isread,creat_time) values ('{$data->msg}', '{$data->openid}', 1,0,'".date('Y-m-d H:i:s')."')";
}
if ($data->type == 2) {
$openid = $this->getKeyByVal($sfd);
$allKefuKeys = Predis::getInstance()->keys(config('queue.websocket_kefu').'*');
foreach ($allKefuKeys as $val) {//给所有客服发送
$fd = Predis::getInstance()->get($val);
$ws->push($fd, json_encode(['msg' => $data->msg, 'openid' => $openid]));
}
$sql = "insert into `chat` (msg,openid,type,isread,creat_time) values ('{$data->msg}', '{$openid}', 2,0,'".date('Y-m-d H:i:s')."')";
}
//异步将数据插⼊数据库
Mysqlasync::getInstance()->query($sql);
}
}
上⽅代码主要做了三件事:① 根据type判断是⼩程序端过来的数据还是pc端过来的数据。type是⾃⼰提前设定好的。②根据openid获取连接唯⼀表⽰fd,发送数据。③异步将数据写⼊数据库
异步写⼊数据库具体代码就不看了,就是官⽹上的例⼦:
⽤户发送数据后,就该pc客服端接受了。
4. pc客服端接受
var wsUrl = "wss://:8080?token=" + token
var ws = new WebSocket(wsUrl)
console.log('client-open-ws')
}
var rst = JSON.parse(evt.data) //{msg,openid}
receiveMessage(rst)
}
console.log('client-onclose-ws')
}
console.log('client-onerror-ws')
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论