使用 Workerman-chat 配合 Laravel 构建自己的 IM 服务

Workerman 是一款纯 PHP 开发的开源高性能的 PHP socket 服务器框架。被广泛的用于手机app、移动通讯,微信小程序,手游服务端、网络游戏、PHP聊天室、硬件通讯、智能家居、车联网、物联网等领域的开发。 支持TCP长连接,支持 Websocket、HTTP 等协议,支持自定义协议。拥有异步 Mysql、异步 Redis、异步 Http、异步消息队列等众多高性能组件。

workerman-chat 是一个以 workerman 作为服务器容器,使用 PHP 开发的基于 Websocket 协议的一个可分布式部署的聊天室框架。

workerman-chat 采用 gateway workers 进程模型。gateway只负责网络IO,全异步非阻塞,每个 gateway 进程都可以同时接受上万客户端连接。 workers 采用的是PHP开发者所熟悉的同步模型,并提供了开发者基本的接口 onConnect、onMessage、onClose、sendToClient、sendToAll 等方法。 开发者只要在 onConnect、onMessage、onClose三个方法中添加上自己的业务逻辑即可,开发维护非常简单。

由于采用的是 gateway workers 进程模型,gateway 和 workers 之间是无状态的,gateway 和 workers 可以分别部署在不同的物理机上,所以扩容和升级都非常方便。 workerman-chat 也非常适合游戏后台开发。

因业务需求,要开发一款浏览器端的 IM 应用。 Workerman 系列本身轻便简洁,十分适合用作我们的 WebSocket 服务端。研究了部分资料之后,同时根据 Workerman 官方文档推荐的 workerman 与其他框架的结合方案,并不是内嵌到框架中,而是作为一个独立的应用,与 Api 和 Client 一起处理问题。整体结构如下:

分析具体需求

大体需求如下:

  • 多客户端同时在线,数据同步
  • 发送端实时接收已读状态更新
  • 接收端在一个客户端查看消息后,其他在线客户端去掉未读提醒

其他背景:

  • Api 端使用 user-id + token 的校验字段判断用户登录状态。
  • 所有数据库操作都在 Api 端也就是 Laravel 中完成, Workerman 只处理 Websocket 请求,不直接操作数据库。

首先,为满足第一个需求,则需要在 Socket Server 端记录在线信息,将 user_idclient_id 做一个一对多的关系绑定。此类动态需求,不涉及 MySQL,使用 Redis 处理。
第二和第三个需求,则是通过 Api 和 Socket Server 的协作来完成。一个 “发送 + 已读” 的操作流程如下:

User A: 发送者,在线客户端 Client A1 & Client A2
User B: 接收者,在线客户端 Client B1 & Client B2

WS-Socket: Workerman 提供的 Websocket 服务
WS-API: Workerman 提供的 HTTP 接口服务
API: Laravel 提供的 Api 服务

注:
(1)登录、注册、下限: WS 根据 Client 传来的数据记录 user_idclient_id 的一对多关联关系。
(2)所有Socket请求,Client都会携带 user-id & token 参数,然后 WS 将参数传给 API 端校验登录是否有效。

1
2
3
4
5
6
7
8
9
(1)Client A1 : 调用 Api 实现新消息的发送, Api 记录消息,并将消息标记为未读状态。
(2)Api 调用 WS-API,有消息发出,并通知 WS-API 消息的 msg_id 、send_user_id 、recieve_user_id
(3)WS-API 内部调用 WS-SOCKET 接口
(4)WS-SOCKET 判断 User A 和 User B 的 Socket 在线状态,并告诉 User A的所有客户端你刚发了一个消息 msg_id,再告诉 User B 的所有客户端你刚收到一条消息 msg_id
(5)Client A1 和 Client A2 收到 Socket 提醒,从 Api 中使用 msg_id 取回消息并同步显示为未读消息。
(6)Client B1 和 Client B2 收到 Socket 提醒,提示有未读消息
(7)User B 在其中一个 Client 打开对话框,Client Bx 从 Api 读取该消息,此时 Api 判断到接收者读取了消息,将消息状态更新为已读并通知 WS-API
(8)WS-API 内部调用 WS-Socket 接口
(9)WS-Socket 判断 User A 和 User B 的 Socket 在线状态,并告诉 User A的所有客户端你刚发的消息 msg_id 已经被阅读了,再告诉 User B 的所有客户端你刚收到的消息 msg_id 已读了,可以去掉未读提醒了。

经过以上步骤,整个发送流程完成,符合 Workerman 官方推荐的 workerman 与其他第三方框架的协作模式建议。

功能实现和框架改动

在此过程中,对 Workerman-chat 这个框架做了一些改动:

1. 引入必要的依赖模块
  • Redis : 用于记录 User 及其 Client 的一对多关联
2. 代码层面的改动

官方建议使用 workerman-chat 做实时通讯业务,只需要关心 ~\Applications\Chat\Events.php 一个文件中的业务逻辑代码

鉴于实际业务需求,调整自动加载目录文件 ~\vendor\composer\autoload_static.php

1
2
3
4
5
6
7
8
9
10
11
12
13
public static $prefixLengthsPsr4 = array (
'L' => // 新增Library命名空间
array (
'Library\\' => 8
)
);

public static $prefixDirsPsr4 = array (
'Library\\' => // 新增Library目录
array (
0 => __DIR__ . '/../..' . '/Applications/Library',
)
);

这里是为了在 ~/Applications/ 目录下添加 ~/Applications/Library/ 目录,并在此添加一些常用的工具类。
主要包括:

  • Redis 单例实现
  • Controller 格式化处理 WS-API 请求
  • Config 类处理环境变量和环境隔离(在项目根目录下添加 config.php 并加入 .gitignore,记录一些区分环境的变量参数)
  • BaseUtil 基础类,实现一些 Http 请求封装、签名计算和校验等的通用方法。
  • UserService 类,实现用户 token 校验和 user-id & client-id 的关联逻辑处理
  • SimpleLog 类,文件操作IO,记录一些日志文件。