关于easyswoole实现websocket聊天室的步骤解析
在去年,我们公司内部实现了一个聊天室系统,实现了一个即时在线聊天室功能,可以进行群组,私聊,发图片,文字,语音等功能,那么,这个聊天室是怎么实现的呢?后端又是怎么实现的呢?
后端框架
在后端框架上,我选用了php的easyswoole,easyswoole作为swoole中最简单易学的框架,上手简单,文档齐全,社区活跃

直接通过easyswoole官方文档的例子,即可实现一个websocket服务器,并且还实现了对控制器的转发等:
https://www.easyswoole.com/Cn/Socket/webSocket.html
前后端通信协议
由于考虑到聊天室的业务逻辑复杂,我们使用了http+websocket 2种协议,分别用在以下几个地方:
登录注册,个人信息修改,好友申请等,使用http 接口实现
私聊,群聊消息推送,系统消息申请等,使用websocket即时推送
websocket即时推送封包方式
在websocket中,为了区分客户端不同的操作(发送群消息,发送私聊消息等),我们定义了一个数据格式:
|
1
2
3
4
5
|
- op 命令- args 额外参数- msg 消息内容- msgType 消息类型(默认为1)- flagId 消息标识符(前端随机生成一个标识符,后台处理完该消息之后,会返回相同的标识符给与前端确认) |
使用json字符串方式传递
同样,为了区分服务端不同的推送,我们定义了服务端的响应格式:
|
1
2
3
4
5
|
- op 命令(响应类型)- args 额外参数- msg 消息内容(成功时为OK)- msgType 消息类型(默认为1)- flagId 将返回和前端一致的标识符,告知前端该次请求 成功/失败 |
例如:
|
1
2
3
4
5
6
7
|
## 发送消息私聊消息:`{"op":1001,"args":{"userId":12},"msg":"test","flagId":10086}`将回复:`{"op":1000,"args":[],"msg":"ok","flagId":10086}`目标用户将收到:`{"op":1101,"args":{"fromUserId":"12","msgId":16},"msg":"test"}` |
下文有许多op:xxx的数据,可以忽略xxx的数据,直接联系上下文获得op的命令类型
聊天记录存储
根据消息的类型,我们区分了 私聊消息,群消息,系统消息 3种消息,设计了3个表
为了使得客户端能够正常显示群消息,我们对群成员做了软删除处理,确保可以获取到群成员头像
用户可通过http接口,获得历史聊天记录
语音,图片,视频聊天
在上面我们可以看到,有一个msgType字段,它将决定了这条数据是文字消息,还是语音,视频
当msgType为语音类型时,msg将附带一个语音文件的地址(通过http接口上传文件,到oss或者服务器)
客户端进行判断,如果是语音,则下载文件,点击即可播放,视频,图片同理
心跳设置
由于tcp的特性,在长时间没有通信时,操作系统可能会自动对tcp连接进行销毁并且可能没有close事件提示,所以我们在websocket中提供了ping的命令,该命令发起后,服务器将响应pong,完成一次通信:
|
1
2
3
4
|
## ping发送:直接给客户端发送 "ping"即可返回:`{"op":1000,"args":null,"msg":"PONG"}` |
网络不稳定推送问题
当服务端推送消息时,为了确保用户已经收到,提供了isRecv字段,默认为0
当用户A向用户B发送消息,服务器向B推送时,该条消息记录初始isRecv为0,只有当B客户端接收到消息,并且向服务器发送已接收命令时,才会置为1:
|
1
2
3
4
5
|
### 消息接收状态`{"op":4002,"args":{"msgId":42},"msg":"","flagId":111}`服务器将响应:`{"op":1000,"args":[],"msg":"ok","msgType":1,"flagId":111}` |
每次重新连接websocket服务时,可通过发起好友未读消息推送的命令,向服务器获得之前的未读消息(网络不稳定断线重连)
|
1
2
3
4
5
6
|
当ws连接成功时,可通过该命令获取所有的未读好友消息:`{"op":4001,"args":{"userId":null,"size":5},"msg":"","flagId":111}`其中`userId` 为限制单独一个好友的未读消息,可不传其中`size`为每次响应条数,默认为5,可不传服务器将响应:`{"op":4101,"args":{"total":0,"list":[]},"msg":"ok","msgType":1,"flagId":111} |
每次推送完,都需要客户端遍历list,进行上面的已接收推送
聊天室流程讲解
整个聊天室流程为:
- 用户http接口登录获得授权
- 通过授权请求http接口获得好友列表,不同好友的最后一条未读消息以及未读消息数(用于首页显示)
- 通过授权请求获得群列表(群消息为了节省存储空间没有做已读未读)
- 建立ws链接
- 注册断线重连机制,当触发close事件时,重连ws
- 建立ping定时器,每隔30秒进行一次ping
- 通过ws接口,获得所有未读消息,客户端进行处理,推送到通知栏等
- 接收新消息推送,并显示到消息列表
- 当点击进某个群/好友消息界面时,自动获取最新n条消息,用户上拉时继续获取n条
不同设备数据同步
为了服务端性能问题,所有消息记录,好友消息,群成员消息将缓存到客户端,当用户登录成功时
直接显示之前登录时的所有状态(消息列表,最后一条消息显示等)
当新设备登录时,只获取未读消息列表,其他消息需要点击某个好友/群,才会进行显示
fd->userId对应
当用户登录成功时,我们使用了swoole的Table进行存储fd->userId以及userId->fd的对应
通过这2者对应的存储,我们可以通过userId找到fd进行推送数据,也可以通过fd找到userId获取用户消息
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
<?phpnamespace App\Utility;use EasySwoole\Component\Singleton;use Swoole\Table;class FdManager{ use Singleton; private $fdUserId;//fd=>userId private $userIdFd;//userId=>fd function __construct(int $size = 1024*256) { $this->fdUserId = new Table($size); $this->fdUserId->column('userId',Table::TYPE_STRING,25); $this->fdUserId->create(); $this->userIdFd = new Table($size); $this->userIdFd->column('fd',Table::TYPE_INT,10); $this->userIdFd->create(); } function bind(int $fd,int $userId) { $this->fdUserId->set($fd,['userId'=>$userId]); $this->userIdFd->set($userId,['fd'=>$fd]); } function delete(int $fd) { $userId = $this->fdUserId($fd); if($userId){ $this->userIdFd->del($userId); } $this->fdUserId->del($fd); } function fdUserId(int $fd):?string { $ret = $this->fdUserId->get($fd); if($ret){ return $ret['userId']; }else{ return null; } } function userIdFd(int $userId):?int { $ret = $this->userIdFd->get($userId); if($ret){ return $ret['fd']; }else{ return null; } }} |
同理,当需要群发消息时,只需要获得群成员的userId,即可获得当前所有在线成员的fd,进行遍历推送
服务端推送问题
当A客户端在群发送一条消息时,由于群成员可能有很多,如果直接同步推送给所有群成员,会造成A客户端等待响应时间过长的情况
所以需要使用task做异步推送:
当A客户端发送一条消息,先存入数据库,并调用task进行异步群发推送,同时给A客户端响应ok,代表接收到此消息
通过easyswoole的task组件,进行推送:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
namespace App\Task;use App\HttpController\Api\User\Message\GroupMessage;use App\HttpController\Api\User\Message\SystemMessage;use App\Model\Group\GroupUserModel;use App\Model\Message\GroupMessageModel;use App\Model\Message\SystemMessageModel;use App\Model\Message\UserMessageModel;use App\Utility\FdManager;use App\WebSocket\Command;use EasySwoole\EasySwoole\ServerManager;use EasySwoole\Task\AbstractInterface\TaskInterface;//消息异步推送class WebSocketPush implements TaskInterface{ protected $messageModel; function __construct($messageModel) { $this->messageModel = $messageModel; } function run(int $taskId, int $workerIndex) { $message = $this->messageModel; $result = false; //好友消息 if ($message instanceof UserMessageModel) { $result = $this->friendMsg($message); } //群组消息 if ($message instanceof GroupMessageModel) { $result = $this->groupMsg($message); } //系统消息 if ($message instanceof SystemMessageModel) { $result = $this->systemMsg($message); } return $result; }} |
websocket验权,提下线功能
用户在连接ws服务时,需要带上token进行验权,
服务端在onopen事件时,会进行token验权,如果验证失败则响应一条消息表示登录过期:
|
1
2
3
4
5
6
7
|
{ "op": -1003, "args": [], "msg": "登陆状态失效", "msgType": 1, "flagId": null} |
当A用户在客户端1登录成功后,又在客户端2登录时,将给客户端1发送一条已被踢下线消息::
|
1
2
3
4
5
6
7
|
{ "op": -1002, "args": [], "msg": "你的账号在其他设备登陆,你已被强制下线", "msgType": 1, "flagId": null} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
static function onOpen(Server $server, \Swoole\Http\Request $request) { $session = $request->get['userSession'] ?? null; $user = new UserModel(); if (!empty($session)) { $user->userSession = $session; $info = $user->getOneBySession(); if (empty($info)) { self::pushSessionError($request->fd); ServerManager::getInstance()->getSwooleServer()->close($request->fd); return true; } //如果已经有设备登陆,则强制退出 self::userClose($info->userId); FdManager::getInstance()->bind($request->fd, $info->userId); //推送消息// self::pushMessage($request->fd,$info->userId); } else { self::pushSessionError($request->fd); ServerManager::getInstance()->getSwooleServer()->close($request->fd); } } |
关于客户端网络不稳定时候的情况解析
当客户端发送一条消息之前,需要生成一个flagId,发送消息时附带flagId
服务端响应消息时,会附带flagId
因此,当客户端发送消息时,新增一个flagId的定时器,当定时器到期却没有接收到服务端响应消息时,判断该条消息发送失败,显示红色感叹号,提示用户重发
当服务端响应成功时,将取消这个定时器,并直接将消息置为发送成功状态
本文为仙士可原创文章,转载无需和我联系,但请注明来自仙士可博客www.php20.cn
关于easyswoole实现websocket聊天室的步骤解析的更多相关文章
- WebSocket聊天室demo
根据Socket异步聊天室修改成WebSocket聊天室 WebSocket特别的地方是 握手和消息内容的编码.解码(添加了ServerHelper协助处理) ServerHelper: using ...
- Netty入门(一)之webSocket聊天室
一:简介 Netty 是一个提供 asynchronous event-driven (异步事件驱动)的网络应用框架,是一个用以快速开发高性能.高可靠性协议的服务器和客户端. 换句话说,Netty 是 ...
- 使用.NET Core和Vue搭建WebSocket聊天室
博客地址是:https://qinyuanpei.github.io. WebSocket是HTML5标准中的一部分,从Socket这个字眼我们就可以知道,这是一种网络通信协议.WebSocket是 ...
- 用Java构建一个简单的WebSocket聊天室
前言 首先对于一个简单的聊天室,大家应该都有一定的概念了,这里我们省略用户模块的讲解,而是单纯的先说说聊天室的几个功能:自我对话.好友交流.群聊.离线消息等. 今天我们要做的demo就能帮我们做到这一 ...
- websocket聊天室
目录 websocket方法总结 群聊功能 基于websocket聊天室(版本一) websocket方法总结 # 后端 3个 class ChatConsumer(WebsocketConsumer ...
- 网络编程-基于Websocket聊天室(IM)系统
目录 一.HTML5 - Websocket协议 二.聊天室(IM)系统的设计 2.1.使用者眼中的聊天系统 2.2.开发者眼中的聊天系统 2.3.IM系统的特性 2.4.心跳机制:解决网络的不确定性 ...
- php +html5 websocket 聊天室
针对内容比较长出错,修改后的解码函数 和 加码函数 原文请看上一篇 http://yixun.yxsss.com/yw3104.html function uncode($str,$key){ $ma ...
- koa2+webSocket 聊天室
做了一个简单的的聊天室,用来看看 koa和 websocket的使用还是挺好的,已经放到gitHub. https://github.com/zhaowanhua/koa2WebSocket
- 实现一个简单的WebSocket聊天室
WebSocket 简介 WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议. WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主 ...
随机推荐
- s40 KVM虚拟化企业级实战
1-为何使用虚拟化 02-KVM虚拟化环境准备 yum install libvirt* virt-* qemu-kvm* -y [root@cs7-kvm ~]# systemctl start l ...
- Docker Registry 简化版
目录 Docker Registry 为什么要使用Registry 依赖 启动 Configuring a registry 配置认证 Docker Registry https://docs.doc ...
- Qt 设置窗体透明
一.前言 在音频开发中,窗体多半为半透明.圆角窗体,如下为Qt 5.5 VS2013实现半透明方法总结. 二.半透明方法设置 1.窗体及子控件都设置为半透明 1)setWindowOpacity(0. ...
- Echarts的柱状统计图出现x轴统计时间出现间隔显示的问题
今天在使用Echarts的柱状统计图出现x轴统计时间出现间隔显示的问题: 数据都拿到了,放到Json数组都是完整的, 展现是时候 如下图:
- 开源软硬一体OpenCV AI Kit(OAK)
开源软硬一体OpenCV AI Kit(OAK) OpenCV 涵盖图像处理和计算机视觉方面的很多通用算法,是非常有力的研究工具之一,且稳居开发者最喜爱的 AI 工具/框架榜首. 1.会不会被USA禁 ...
- 开放式神经网络交换-ONNX(下)
开放式神经网络交换-ONNX(下) 计算节点由名称.它调用的算子operator的名称.命名输入的列表.命名输出的列表和属性列表组成. 输入和输出在位置上与算子operator输入和输出相关联.属性通 ...
- FPGA与ASIC:它们之间的区别以及使用哪一种?
FPGA与ASIC:它们之间的区别以及使用哪一种? FPGA Vs ASIC: Differences Between Them And Which One To Use? VL82C486 Sing ...
- .NET平台系列18 .NET5的超强优势
系列目录 [已更新最新开发文章,点击查看详细] 支持所有 .NET 应用程序类型 .NET5 统一版本之后将支持所有 .NET 应用程序类型:Xamarin.ASP.NET.IoT 和桌面.此 ...
- AJAX第二天笔记
AJAX day1 jquery中的ajax 拦截请求: $.ajaxPrefilter() jquery方法请求参数的本质: 无论我们填写的何种形式的参数,都会被jQuery转换成查询字符串形式传 ...
- UF 公共类型
Open C uc4400uc4403uc4404uc4406uc4409uf3192uf4401uf4402UF_add_callback_functionUF_allocate_memoryUF_ ...