聊天(IM)即时通信说明
适用于 奇点CMS v0.3.96 及以后。本轮把聊天从"15 秒全量轮询"优化为"4 秒增量轮询 + 已读回执", 完整检测数据见交付记录中的《聊天插件即时通信检测报告》。
一、传输模型
| 项 | 实现 |
|---|---|
| 传输方式 | HTTP 轮询(准实时;无 WebSocket / SSE) |
| 会话窗口轮询 | 4 秒(assets/im-plugin/modules/chat.js → ajax-chat-history.php,增量) |
| 聊天主页轮询 | 4 秒(messages.php 内联脚本,仅当 chat_with > 0 时启用) |
| 未读徽标 | 20 秒(assets/im-plugin/core/im-sdk.js → ajax-unread.php) |
| 后台标签页 | 暂停轮询(浏览器也会节流定时器);切回时立即补拉一次 |
| 隐藏期间新消息 | 标题闪烁提示(不申请通知权限,避免打扰) |
二、接口
| 接口 | 说明 |
|---|---|
ajax-chat-history.php?chat_with=N | 全量:最近 30 条(按 id 升序返回),附 has_more、peer_read_id、incremental=false |
ajax-chat-history.php?chat_with=N&since_id=M | 增量:只返回 id > M 的消息(最多 50 条),附 incremental=true;无新消息时响应约 128 字节 |
ajax-chat-send.php | 发送(需登录 + CSRF);支持文本与媒体(≤10 MB) |
ajax-unread.php?csrf_token=… | 站内信/聊天未读数 |
两个关键的服务端约定:
- 仅在确有未读时才写库:只有"对方发给我且
is_read=0的消息数 > 0"时才执行UPDATE chat_messages SET is_read=1。 旧实现每次轮询都会 UPDATE 一次(无新消息也写),属于写放大。 - 消息顺序按主键
id:旧实现按created_at(秒级 TIMESTAMP)排序,同一秒内的多条消息顺序不稳定。
三、已读回执
- 数据来源:
chat_messages.is_read(当对方打开你们的会话时由服务端置 1)。 - 每次历史响应携带
peer_read_id= 我已发送、且对方已读的最大消息 id(0 表示对方尚未读)。 - 前端在己方消息的时间行显示
已读/未读(.msg-read[data-read]),轮询时由peer_read_id实时校正。
四、实测数据(本机 Windows + 宝塔 + MySQL 8.0.29)
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 接收延迟(会话窗口) | 0–15 s(均值 7.5 s) | 0–4 s(均值 2 s) |
聊天主页 messages.php | 不刷新(需手动刷新页面) | 0–4 s 自动刷新 |
| 未读徽标刷新 | 60 s | 20 s |
| 空轮询响应体积 | 15,328 B(重拉 30 条) | 128 B |
| 无未读时的写库 | 每次轮询 1 次 UPDATE | 0 次 |
| 已读回执 | 无 | 发送方可见"已读/未读" |
| 单次轮询延迟 | 20–47 ms | 10–47 ms |
| 读查询计划(5 万行、目标会话 20 条) | OR + ORDER BY created_at | idx_conversation 范围扫描 + 20 行排序,0.41 ms |
| 新增两个查询 | — | 走 idx_receiver_unread,0.03 ms |
| 40 并发轮询 | 2.2 s 完成(≈18 req/s) | 未变(容量非瓶颈) |
结论:延迟问题解决,流量与写放大大幅下降,无需新增索引(已用
EXPLAIN ANALYZE在 5 万行数据上验证)。OR改写成UNION ALL实测仅快约 2 倍(微秒级),收益不足以承担改动风险,故未采纳。
五、改前端资源时的注意点(重要)
assets/im-plugin/** 被 nginx 强缓存(JS/CSS 12h/30d)。**改动 im-plugin 下任何资源后,必须递增
assets/js/tab-card.js 里的 IM_BUILD 常量**(本版已从 20260929-5 递增为 20261002-1),
该构建号会随 im-sdk.js 的 URL 下发并传播给全部子资源,形成完整的缓存失效链;忘记递增会导致老用户继续执行旧代码。
六、C 档:Go SSE 推送网关(v0.3.97 已落地)
在 A+B 的 4 秒轮询之上增加实时推送通道,端到端延迟由秒级降到毫秒级;轮询保留为兜底,任何异常都不影响消息投递。
6.1 组件与数据流
浏览器 ──(1) GET ajax-im-token.php ──→ PHP:用 GO_API_KEY 签发短时令牌
│ sig = HMAC-SHA256(key, "im:<user_id>|<ts>"),有效期 ±300s
│
└──(2) new EventSource("<站点>/im-stream?user_id&ts&sig")
│ nginx: location = /im-stream → proxy_buffering off → 127.0.0.1:8080/im/stream
▼
Go 服务:校验令牌 → 注册到 imHub(按 user_id 多路订阅)→ 20s 心跳保活
▲
PHP ajax-chat-send.php ──(3) POST /api/im/publish(CSRF+HMAC 鉴权)──→ imHub 广播给接收者的所有连接
6.2 新增接口
| 接口 | 鉴权 | 说明 |
|---|---|---|
GET /im/stream?user_id&ts&sig | 短时令牌(PHP 签发) | SSE 事件流;首帧 event: ready;event: chat 为聊天推送;: ping 每 20 s 保活 |
POST /api/im/publish | X-API-Key + X-CSRF-Token + X-CSRF-Signature | 内部发布(PHP 调用),{"to":[uid...],"event":"chat","data":{...}} → 返回 delivered |
GET /api/im/stats | 同上 | 诊断:当前订阅连接数 / 用户数 |
GET ajax-im-token.php | 登录会话 | 浏览器取令牌与同源订阅 URL;未配置 GO_API_KEY 时返回 enabled=false |
6.3 安全设计
- 浏览器不接触
GO_API_KEY,只拿短时令牌(300 s,且与 user_id 绑定,签名覆盖两者)。 - 令牌校验失败/过期/缺失一律 403;
GO_API_KEY为空或默认值时直接禁用推送(避免可伪造)。 - 事件负载只含提示性字段(发送者、预览片段、媒体类型、时间),不含消息正文全文;客户端收到事件后仍走鉴权接口增量拉取。
- nginx 对该 location
access_log off(令牌在查询串中);/api/im/*沿用 Go 侧的 IP 白名单(仅回环)。
6.4 降级与容错
| 场景 | 行为 |
|---|---|
未配置 GO_API_KEY / Go 未部署 | 令牌接口返回 enabled=false,前端不再尝试,纯轮询照常 |
| Go 服务重启 / 网络中断 | 客户端指数退避重连(1→30 s),每次重连重新取令牌;期间轮询兜底 |
| 订阅者积压(慢客户端) | Go 侧丢弃该事件(非阻塞),由轮询补齐 |
| 浏览器不支持 EventSource | 前端自动退出,纯轮询 |
6.5 实测(本机 Windows + 宝塔 + Go 1.22.12)
| 项 | 结果 |
|---|---|
| 令牌校验 | 错误签名 / 缺少签名 / 过期时间戳 → 403 |
| 订阅建立 | 收到 event: ready;/api/im/stats → {connections:1, users:1} |
| 发布 | {"success":true,"data":{"delivered":1,"targets":1}};无订阅者时 delivered:0 |
| 端到端延迟(测试实例直连) | 5 次连续发布→送达 14/16/17/24/29 ms(含测试端 10 ms 读取粒度) |
| 端到端延迟(生产链路) | 浏览器 → nginx(/im-stream 反代,proxy_buffering off)→ Go:user1 真实 AJAX 发送(47 ms)→ user2 的 SSE 约 28 ms 收到事件 |
| 生产链路订阅 | /api/im/stats → {connections:1, users:1};首帧 event: ready |
| 事件负载 | {"from":1,"from_name":"站长","preview":"…","media":"none","at":…} —— 只含预览片段,不含正文全文 |
| UTF-8 | 中文与 emoji 经 SSE 完整无损 |
| 推送失败容错 | Go 返回 404(旧版本 exe)时,ajax-chat-send.php 静默跳过,消息照常落库 |
6.6 部署要点
- Go 服务需重新编译(新增
im_hub.go/im_sse.go):go build -trimpath -ldflags "-s -w" -o x-dream-go.exe . - nginx 需加
location = /im-stream反代(nginx.conf.example与qdcms-deploy.ps1生成的 vhost 已内置),必须proxy_buffering off GO_API_KEY已在config/generated.php中(Windows 由qdcms-go-win.ps1生成),PHP 与 Go 两侧一致即可- 验证:浏览器登录后打开聊天窗口 → 用另一账号发消息,应在毫秒级出现(未启用时最多 4 秒)