关于easyswoole实现websocket聊天室的步骤解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 关于easyswoole实现websocket聊天室的步骤解析

在去年,我们公司内部实现了一个聊天室系统,实现了一个即时在线聊天室功能,可以进行群组,私聊,发图片,文字,语音等功能,那么,这个聊天室是怎么实现的呢?后端又是怎么实现的呢?

后端框架

在后端框架上,我选用了php的easyswoole,easyswoole作为swoole中最简单易学的框架,上手简单,文档齐全,社区活跃

直接通过easyswoole官方文档的例子,即可实现一个websocket服务器,并且还实现了对控制器的转发等:

https://www.easyswoole.com/Cn/Socket/webSocket.html

前后端通信协议

由于考虑到聊天室的业务逻辑复杂,我们使用了http+websocket 2种协议,分别用在以下几个地方:

登录注册,个人信息修改,好友申请等,使用http 接口实现

私聊,群聊消息推送,系统消息申请等,使用websocket即时推送

websocket即时推送封包方式

在websocket中,为了区分客户端不同的操作(发送群消息,发送私聊消息等),我们定义了一个数据格式:

- op 命令
- args 额外参数
- msg 消息内容
- msgType 消息类型(默认为1)
- flagId 消息标识符(前端随机生成一个标识符,后台处理完该消息之后,会返回相同的标识符给与前端确认)

使用json字符串方式传递

同样,为了区分服务端不同的推送,我们定义了服务端的响应格式:

- op 命令(响应类型)
- args 额外参数
- msg 消息内容(成功时为OK)
- msgType 消息类型(默认为1)
- flagId 将返回和前端一致的标识符,告知前端该次请求 成功/失败

例如:

## 发送消息
私聊消息:
`{"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,完成一次通信:

## ping
发送:直接给客户端发送 "ping"即可
返回:
`{"op":1000,"args":null,"msg":"PONG"}`

网络不稳定推送问题

当服务端推送消息时,为了确保用户已经收到,提供了isRecv字段,默认为0

当用户A向用户B发送消息,服务器向B推送时,该条消息记录初始isRecv为0,只有当B客户端接收到消息,并且向服务器发送已接收命令时,才会置为1:

### 消息接收状态
`{"op":4002,"args":{"msgId":42},"msg":"","flagId":111}`
服务器将响应:
`{"op":1000,"args":\[\],"msg":"ok","msgType":1,"flagId":111}`

每次重新连接websocket服务时,可通过发起好友未读消息推送的命令,向服务器获得之前的未读消息(网络不稳定断线重连)

当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获取用户消息

<?php
namespace 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组件,进行推送:

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验权,如果验证失败则响应一条消息表示登录过期:

{
  "op": -1003,
  "args": \[\],
  "msg": "登陆状态失效",
  "msgType": 1,
  "flagId": null
}

当A用户在客户端1登录成功后,又在客户端2登录时,将给客户端1发送一条已被踢下线消息::

{
  "op": -1002,
  "args": \[\],
  "msg": "你的账号在其他设备登陆,你已被强制下线",
  "msgType": 1,
  "flagId": null
}
``````bash
   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的定时器,当定时器到期却没有接收到服务端响应消息时,判断该条消息发送失败,显示红色感叹号,提示用户重发

当服务端响应成功时,将取消这个定时器,并直接将消息置为发送成功状态

目录
相关文章
|
15天前
|
机器学习/深度学习 存储 人工智能
让模型评估模型:构建双代理RAG评估系统的步骤解析
在当前大语言模型(LLM)应用开发中,评估模型输出的准确性成为关键问题。本文介绍了一个基于双代理的RAG(检索增强生成)评估系统,使用生成代理和反馈代理对输出进行评估。文中详细描述了系统的构建过程,并展示了基于四种提示工程技术(ReAct、思维链、自一致性和角色提示)的不同结果。实验结果显示,ReAct和思维链技术表现相似,自一致性技术则呈现相反结果,角色提示技术最为不稳定。研究强调了多角度评估的重要性,并提供了系统实现的详细代码。
41 10
让模型评估模型:构建双代理RAG评估系统的步骤解析
|
9天前
|
监控 数据挖掘 BI
项目管理流程全解析及关键步骤介绍
项目管理流程是项目成功的基石,涵盖启动、规划、执行、监控和收尾等阶段。Zoho Projects 等软件可提高效率,支持结构化启动与规划、高效执行与协作及实时监控。这些流程和工具对项目的全局视角、团队协作和风险控制至关重要。项目管理软件适用于不同规模企业,实施时间因软件复杂度和企业准备而异。
25 2
|
29天前
|
监控 网络协议 API
.NET WebSocket 技术深入解析,你学会了吗?
【9月更文挑战第4天】WebSocket 作为一种全双工协议,凭借低延迟和高性能特点,成为实时应用的首选技术。.NET 框架提供了强大的 WebSocket 支持,使实时通信变得简单。本文介绍 WebSocket 的基本概念、.NET 中的使用方法及编程模型,并探讨其在实时聊天、监控、在线游戏和协同编辑等场景的应用,同时分享最佳实践,帮助开发者构建高效实时应用。
80 12
|
2月前
|
图形学 数据可视化 开发者
超实用Unity Shader Graph教程:从零开始打造令人惊叹的游戏视觉特效,让你的作品瞬间高大上,附带示例代码与详细步骤解析!
【8月更文挑战第31天】Unity Shader Graph 是 Unity 引擎中的强大工具,通过可视化编程帮助开发者轻松创建复杂且炫酷的视觉效果。本文将指导你使用 Shader Graph 实现三种效果:彩虹色渐变着色器、动态光效和水波纹效果。首先确保安装最新版 Unity 并启用 Shader Graph。创建新材质和着色器图谱后,利用节点库中的预定义节点,在编辑区连接节点定义着色器行为。
101 0
|
2月前
|
监控 安全 iOS开发
|
2月前
|
数据库 Windows
超详细步骤解析:从零开始,手把手教你使用 Visual Studio 打造你的第一个 Windows Forms 应用程序,菜鸟也能轻松上手的编程入门指南来了!
【8月更文挑战第31天】创建你的第一个Windows Forms (WinForms) 应用程序是一个激动人心的过程,尤其适合编程新手。本指南将带你逐步完成一个简单WinForms 应用的开发。首先,在Visual Studio 中创建一个“Windows Forms App (.NET)”项目,命名为“我的第一个WinForms 应用”。接着,在空白窗体中添加一个按钮和一个标签控件,并设置按钮文本为“点击我”。然后,为按钮添加点击事件处理程序`button1_Click`,实现点击按钮后更新标签文本为“你好,你刚刚点击了按钮!”。
105 0
|
2月前
|
监控 安全 网络架构
深入解析子网划分的目的和关键步骤
【8月更文挑战第25天】
49 0
|
3月前
|
存储 前端开发 Go
golang怎么搭建Websocket聊天室服务端
连接的添加和移除 添加连接: 当一个新的WebSocket连接建立时,服务器需要将这个连接添加到全局的连接列表中。多个连接可能同时建立,从而导致多个并发操作试图修改连接列表。 移除连接: 当一个WebSocket连接断开时,服务器需要将这个连接从全局的连接列表中移除。如果多个连接同时断开,可能会导致并发修改连接列表。
|
3月前
|
数据安全/隐私保护 iOS开发
详细步骤解析:Undetectable指纹浏览器使用IPXProxy代理IP
对于品牌来说,社交媒体已经成为寻找目标受众的丰富资源。在社交媒体平台通过评论和留言进行推广具有很高的转化率,并且推广成本较低。为了获得可观的利润,大家可能需要管理至少几个社交媒体账号,然而在一台电脑上管理多个账号会比较困难。因此使用可靠的工具成为大家的必要选择,其中Undetectable指纹浏览器和IPXProxy代理IP就是两个不错的工具。下面给大家带来Undetectable指纹浏览器配置IPXProxy代理IP的详细教程。
157 0
|
3月前
|
运维 负载均衡 前端开发
深度解析:Python Web前后端分离架构中WebSocket的选型与实现策略
【7月更文挑战第16天】Python Web开发中,前后端分离常见于实时通信场景,WebSocket作为全双工协议,常用于此类应用。选型时考虑性能、功能、易用性、社区支持和成本。Flask-SocketIO是实现WebSocket的一个选项,它简化了与Flask的集成。案例展示了如何用Flask-SocketIO创建一个实时聊天室:后端处理消息广播,前端通过Socket.IO库连接并显示消息。此实现策略演示了在Python中实现实时通信的基本步骤。
75 0

热门文章

最新文章

推荐镜像

更多
下一篇
无影云桌面