TP5.1 + think-swoole 打造聊天室

搜遍全网都没找到正经的demo,琢磨了两三天,走了不少弯路才搞出来这个,弄完发现真是无敌简单,如果你也跟我一样在踩坑,可以参考一下。

坑1: TP5.1最高支持的是think-swoole 2.0.*
官方文档竟然都没写,导致直接按文档安装的,都是3.0,也就是TP6.0才适用的版本,吐血……

composer require topthink/think-swoole=2.0.*

坑2: 不要安装swoole4!
宝塔面板是可以直接装swoole的,但它有俩swoole,选第一个!
TP5.1 + think-swoole 打造聊天室_第1张图片

坑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)
    };

到这儿基本上就跑通了,作为一个小白,我纯属抛砖引玉,为还在踩坑的朋友提供帮助,也感谢为我提供帮助的大佬。

你可能感兴趣的:(php,小程序,swoole)