搜遍全网都没找到正经的demo,琢磨了两三天,走了不少弯路才搞出来这个,弄完发现真是无敌简单,如果你也跟我一样在踩坑,可以参考一下。
坑1: TP5.1最高支持的是think-swoole 2.0.*
官方文档竟然都没写,导致直接按文档安装的,都是3.0,也就是TP6.0才适用的版本,吐血……
composer require topthink/think-swoole=2.0.*
坑2: 不要安装swoole4!
宝塔面板是可以直接装swoole的,但它有俩swoole,选第一个!
坑3: 也不算是坑,是自己没弄明白……
如果你自定义了入口文件,那一定要在根目录的think里同样定义一下,否则是无法用下边这个命令运行swoole的。
php think swoole
php think swoole:server
具体点就是,一般都会在index.php定义类似这样的入口:
define('APP_PATH', __DIR__ . '/application/');
define('ADDON_PATH', __DIR__ . '/addons/');
那注意了,根目录的think文件也同样要定义。
坑4: 也不知道是谁最早说要这样运行swoole
cd public
php index.php /控制器/方法名/start
害的我折腾两天,死活就是报各种奇葩的错误,而官方文档说的很清楚,就是通过坑2的命令运行,所以如果你发现坑2没能运行,就去看看是否是自定义的入口文件没有同步到think里。
提示:不是说这样运行是错的,我看了几个教程视频也都是这么运行的,但我确实跑不起来,谁给说说为啥……
坑5: swoole的配置文件有俩:
config/swoole.php
config/swoole_server.php
人家写的很清楚了,一个对应的是坑2第一行的命令(Swoole HTTP),一个是第二行的命令(Swoole Server);
因为我要做聊天室,所以需要socket,也就是要打开第二个服务模式才行,对应的配置都在第二行那个配置文件里。
坑6: 我到现在都不知道,人家是如何做到把自定义服务类里,回调函数中打印的内容直接显示到ssh控制台的。
我用了个笨办法,通过log_file这个配置项,把涉及echo\dump\print_f等内容都输出到log文件里去。
log_file => 'log文件目录'
坑7: 这个确实不算坑,但跟我一样小白的要知道,你每次修改自定义类文件的时候,都要重启一下服务,否则是不生效的!
常用命令如下:
php think swoole:server //启动服务
lsof -i:端口号 //查看端口占用情况
kill -9 pid //杀掉端口的进程
好了,上述这些坑,我踩完了,如果能帮到大家,我非常荣幸,然后我给大家一个自定义类的demo,可以参考一下;
namespace miniapp\bwinHouse\controller;
use miniapp\bwinHouse\model\Chat as chatModel;
use think\swoole\Server;
use think\db;
class Swoole extends Server
{
protected $host = 'xxx.xxx.xxx.xxx';//你的ip地址,或者域名
protected $port = 39133;//端口号,记得要在安全组开放!
protected $serverType = 'socket';
protected $sockType = SWOOLE_SOCK_TCP | SWOOLE_SSL; //SWOOLE_SSL标识开启ssl,小程序wss协议要用,开这个必须把下边的两个证书配置好
protected $option = [
'worker_num' => 4, //设置启动的Worker进程数
'daemonize' => true,//守护进程化
'max_request' => 10000,
'dispatch_mode' => 2, //固定模式,保证同一个连接发来的数据只会被同一个worker处理
'debug_mode' => 1,
'log_file' => '/www/wwwroot/xxxx/public/swoole/error.log',//我为了记录出错记录的log
//心跳检测:每60秒遍历所有连接,强制关闭10分钟内没有向服务器发送任何数据的连接
'heartbeat_check_interval' => 60,
'heartbeat_idle_time' => 600,
//下边这俩证书,宝塔可以直接申请,位置就统一在这里了
'ssl_cert_file' => '/etc/letsencrypt/live/xxx/fullchain.pem', //ssl证书
'ssl_key_file' => '/etc/letsencrypt/live/xxx/privkey.pem', //ssl证书key
];
/**
* 当WebSocket客户端与服务器建立连接并完成握手后会回调此函数。
* @param $server
* @param $request
*/
public function onOpen($server, $request){
$fd = $request->fd;//发送方客户端标识id,每次创建聊天时随机,可以理解为房间号
$fid = $request->get['fid'];//客户端传递的发送方用户id
$tid = $request->get['tid'];//客户端传递的接收方用户id
$this->saveFdCache($fid,$tid,$fd);//发送人id、接收方id和发送方fd存储到缓存中
//为何要这么存,这个方法里有写
echo '发送方'.$fid.'与接收方'.$tid.'建立连接';//这个会打印在log_file里,哪位大神能告诉我如何直接打印在控制台里?
}
/**
* 当服务器收到来自客户端的数据帧时会回调此函数
* data格式 = {"fid":"fid","tid":"tid","content":"xxxxxxxxxxxxxxx"}
* @param $server
* @param $frame
* @return bool|void
*/
public function onMessage($server, $frame){
//接收数据处理
$fd = $frame->fd;//发送方房间号
$message = json_decode($frame->data,true);//接收的消息内容,转换为数组
$fid = $message['fid'];//从消息中拿到发送人id
$tid = $message['tid'];//从消息中拿到接收人id
$tfd = $this->getFdcache($tid,$fid);
//此时tid和fid相反,对应接收方的房间号,同样看存储方法里的注释
//组装发送数据
$data['fid'] = $fid;
$data['tid'] = $tid;
$data['message'] = $message['content'];
$data['post_time'] = date("m/d H:i",time());//这个是demo用到的
$arr = array('status'=>1,'message'=>'success','data'=>$data);//组装好的发送数据
$this->addChatRecord($fid,$tid,$message['content']); //保存聊天记录到数据库
//定向推送消息给fid所在的房间号fd
$server->push($fd, json_encode($arr));
$fds = []; //所有在线的用户(打开聊天窗口的用户)
foreach($server->connections as $fd){
array_push($fds, $fd);
}
//推送给接收者tid的房间
if(in_array($tfd,$fds)){
$server->push($tfd, json_encode($arr));
}else{
//这里可以写如果不在线的话如何,比如我打算发订阅消息
}
}
/**
* 绑定客户id,存储到cache保证信息准确推送
* @param $fid
* @param $tid
* @param $fd
* @return bool
*/
public function saveFdCache($fid,$tid,$fd)
{
$value = ['fid'=>$fid,'tid'=>$tid,'fd'=>$fd];
cache('fid'.$fid.'tid'.$tid,$value,1800);
//example:fid2tid3(id为2的人发给id为3消息时,2的房间号是fd)
}
//每次用户建立连接,都会发送双方id,此时与fd一起存入cache
//而当通过双方id查找对应fd时,肯定是最近一次有效的fd数据
/**
* 与save对应,一存一取
* @param $fid
* @param $tid
* @return mixed
*/
public function getFdcache($fid,$tid){
$data =cache('fid'.$fid.'tid'.$tid);
return $data['fd'];
}
//这里要注意,之所以要把双方id按顺序当做name存到cache里
//就是为了保证可以1v1的正确推送消息,我看到有一些demo的写法是只把fid和fd绑定存储
//后果就是,如果A和B在聊天,当有第三方C发消息给B时,B这儿就成了群聊了,无法正确回复消息
/**
* 新增聊天记录
* @param $fid
* @param $tid
* @param $content
* @return bool
*/
public function addChatRecord($fid,$tid,$content)
{
$chatModel = new chatModel();
$data = [
'fid'=>$fid,//发送人id
'tid'=>$tid,//接收人id
'content'=>$content
];
$result = $chatModel -> addRecord($data);
if($result == '1'){
return true;
}else{
return $result;
}
}
}
再来一个前端的demo,用的jquery\vue\layer,只看script部分:
var client = null;
var fid = 2; //当前用户id
var tid = 3; //接受消息用户id
var app = new Vue({
el: '#app',
data: {
message: '初始消息',
messages: []
},
methods: {
sentMessage: function() {
if (this.message == null || this.message == "") {
layer.msg('内容为空', {
shade: 0.1,
icon: 2,
time: 600
});
return false;
} else {
var Data = {
content: this.message,
fid: fid,
tid: tid,
};
client.send(JSON.stringify(Data));
this.message = '';
}
}
}
});
client = new WebSocket("wss:ip地址:39133?fid=" + fid + '&tid=' + tid);
client.onopen = function() {
layer.msg('服务器连接成功', {
shade: 0.1,
icon: 1,
time: 600
});
};
client.onerror = function() {
layer.msg('服务器连接失败', {
shade: 0.1,
icon: 2,
time: 600
});
};
client.onmessage = function(evt) {
var data = JSON.parse(evt.data);
console.log(data);
//错误提示
if (data.status != 1) {
layer.alert(data.message, {
icon: 2
});
return;
}
//消息返回
if (data.status == 1 && data.data.message != '') {
app.messages.push(data.data);
}
};
client.onclose = function(res) {
console.log(res)
};
到这儿基本上就跑通了,作为一个小白,我纯属抛砖引玉,为还在踩坑的朋友提供帮助,也感谢为我提供帮助的大佬。