PHP小程序后端简单实现聊天功能
之前对这个聊天的功能很好奇,今天终于有机会接触了。一番交战之后,才觉得没有想象中的那么神秘。怎么实现的呢?
首先最主要的是需要swoole的服务,其次就是代码功能的实现。主要有以下步骤:
- 安装swoole拓展服务
- 修改php配置,开启拓展
- nginx/apache 代理wss服务
- php代码实例webscoket监听某个端口
- 聊天代码实现
由于服务器用了linux宝塔第一步和第二步就不描述了(宝塔真的很香~~)
服务端怎么配置?
由于在小程序里面域名,必须要符合安全域名的要求,所以决定用用同一个域名,wss的时候监听代理其他端口。
比较不喜欢的是,服务器使用的是apache(个人喜欢nginx)
网上找了一下apache怎么配置的方法:
//主要是wss代理,ssl和webscoket监听窗口就不示例了
ProxyRequests Off
ProxyPass /wss ws://127.0.0.1:9501
ProxyPassReverse /wss ws://127.0.0.1:9501
另外nginx配置:
location /ws {
proxy_pass http://127.0.0.1:9501;
proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'Upgrade';
}
数据库怎么设计?我的需求比较简单,就是一对一聊天就行了,所以数据库设计也非常简单。不同需求也可以设计更好的数据表。首先以一个表示聊天房间表,然后就是房间用户表,最后存放消息的表。
//聊天房间表
CREATE TABLE `pigcms_chat_room` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`store_id` int(11) NOT NULL DEFAULT '0' COMMENT '店铺id',
`agent_id` varchar(12) NOT NULL DEFAULT '' COMMENT '代理商id',
`is_store` tinyint(11) NOT NULL DEFAULT '0' COMMENT '0 平台 1 独立版小程序',
`add_time` int(11) NOT NULL DEFAULT '0',
`update_time` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
//房间用户表
CREATE TABLE `pigcms_chat_room_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`room_id` int(11) NOT NULL DEFAULT '0' COMMENT '房间id',
`user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户·id',
`to_user_id` int(11) NOT NULL DEFAULT '0' COMMENT '对方user_id',
`is_master` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1 客服人员',
`add_time` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
//消息表
CREATE TABLE `pigcms_chat_message` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`room_id` int(11) NOT NULL,
`send_user_id` int(11) NOT NULL DEFAULT '0' COMMENT '自己user_id',
`receive_user_id` int(11) NOT NULL DEFAULT '0' COMMENT '对方user_id',
`type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1 发送 2 接收',
`message_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '消息类型 1 文本 2 图片 3 语音',
`message_content` text COMMENT '消息内容',
`is_read` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0 未读 1 已读',
`add_time` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
php怎么实例webscoket呢?开发文档是这样的,我基本上也是这样写的:
//创建websocket服务器对象,监听0.0.0.0:9501端口
$ws = new Swoole\WebSocket\Server("0.0.0.0", 9501);
//监听WebSocket连接打开事件
$ws->on('open', function ($ws, $request) {
var_dump($request->fd, $request->get, $request->server);
$ws->push($request->fd, "hello, welcome\n");
});
//监听WebSocket消息事件
$ws->on('message', function ($ws, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
});
//监听WebSocket连接关闭事件
$ws->on('close', function ($ws, $fd) {
echo "client-{$fd} is closed\n";
});
$ws->start();
//怎么运行这段代码呢
php ws_server.php
//方便操作,最好可以编辑好启动,重启,关闭的linux命令
怎么实现聊天逻辑呢?代码就不放出来了,就简单的说下伪代码吧:
//实例化服务
$this->server = new \swoole_websocket_server("127.0.0.1", 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP);
$this->server->set(array(
'daemonize' => true, //常驻进程,开发调试时可以注释掉
));
//监听第一次连接
$this->server->on('open', function ($server, $request) {
//第一次连接时候根绝前端的传参确定聊天的对象,以及自己的用户身份
//webscoket在握手之后会用一个参数fd来标记前端的用户
//所以第一次连接的时候把fd和uid做个双向绑定操作,连接中的交互可以通过fd找到uid
//怎么通过uid找到fd,由于是一对一的聊天,每次退出聊天窗口都会断开连接
//所以不需要做一对多的绑定,只需要一对一的双向绑定即可
//业务逻辑就是根据fd找到自己的身份,然后确定聊天对象,如果没有聊天记录,就创建一个聊天,然后就是执行获取聊天记录,返回
})
//怎么存储fd和uid呢,我是用了缓存,可以用mysql,只是存到缓存读取比较快
<?php
use think\facade\Cache;
class connect
{
//设置
public static function save($fd, $uid)
{
Cache::rm('_fd_'.$fd);
Cache::rm('_uid_'.$uid);
$data = ['fd'=>$fd,'uid'=>$uid];
Cache::set('_fd_'.$fd, $data);
Cache::set('_uid_'.$uid, $data);
}
//获取uid
public static function get_uid($fd)
{
$fd = Cache::get('_fd_'.$fd);
return isset($fd['uid'])&&!empty($fd['uid']) ? $fd['uid'] : 0;
}
//获取fd
public static function get_fd($uid)
{
$uid = Cache::get('_uid_'.$uid);
return isset($uid['fd'])&&!empty($uid['fd']) ? $uid['fd'] : 0;
}
//断开连接清除数据
public static function delete($fd, $uid)
{
Cache::rm('_fd_'.$fd);
Cache::rm('_uid_'.$uid);
}
}
//存储的时候调用
connect::save($request->fd, $uid);
//监听message消息交互,主要有(获取更多的聊天的记录和接收消息和发送消息)
$this->server->on('message', function ($server, $frame) {
//连接中的交互用通过fd获取用户id
$uid = connect::get_uid($frame->fd);
//然后判断用户发送过来的数据是什么类型(获取聊天记录或者发送消息)
//发送消息时找到聊天对象,通过uid查找fd,判断对方是否在线
//如果在线,存储到数据库的消息,就标记为已读,并且把消息推送过去
$to_fd = connect::get_fd($touser['uid']);
if (!empty($to_fd)) {
$receive_message['is_read'] = 1;
}
if (!empty($to_fd)) {
$server->push($to_fd, json_encode($message_data));
}
//告诉自己发送成功了
return $server->push($frame->fd, json_encode(['type'=>'send_success','message'=>'发送成功'.$to_fd]));
})
//监听关闭链接,清掉存在的fd和uid
$this->server->on('close', function ($server, $fd) {
$uid = connect::get_uid($fd);
connect::delete($fd, $uid);
});