09.12 完整服务项目:从内存单聊到有边界的持久教学 IM
返回第九卷 · 起点:09.01 · 当前 HTTP 合同:09.02 · 验证前置:09.11 · TLS 前置:04.07
09.12 完整服务项目:从内存单聊到有边界的持久教学 IM
这是一章静态课程与学习者项目说明,不是本仓库新增一个可运行 IM 服务。DeepTutor 初稿经技术与教学审阅;系统图、接口、SQL 状态、故障和验收均为虚构纸上方案。本次没有运行 Go、数据库、WebSocket、TLS、IM 服务、CI 或站点。学习者未来在自己的隔离环境实现后,才把实际结果单独记为实测证据。
一、先说清“完整”在 S3 到哪一步
前面章节分开学了 Go 语言、本地消息工具、网络、并发、数据库、HTTP、认证、安全和验证。本章把它们围绕同一条虚构消息 m-a 组织成逐步完成的教学服务项目:u-a 是 c-a 当前成员,u-b 是另一成员,已登录但不是成员的 u-c 不得读取 c-a。A 可按权限查看历史、提交消息;若 B 正在线,可以尝试经 WebSocket 通知 B。每一阶段都必须解释对谁承诺什么,失败后还知道什么。
“完整服务”在此指S3 当前范围的功能与证据链完整,不是所有 IM 保证已经具备。S3 可设计登录身份、成员授权、持久消息、历史分页和在线尝试;离线补拉、多设备最终一致、群聊大规模扇出、恰好一次、设备送达/已读仍在 S4/S5 后续章节。网络 04.04–04.07 已补字节确认、长连接、流控与 TLS,故本章不会把 TCP ACK、Pong 或小锁图标写成业务送达。
| 旧/新教学合同 | 正文上限 | 成功回应能表示 | 尚未表示 |
|---|---|---|---|
09.02 S2 v1 | 当前 6 个 UTF-8 字节 | 200 accepted_in_memory:本进程内存受理 | 持久化、A 一定收到回应、B 收到 |
本章提出的 S3 v2 纸上合同 | 继续采用 6 个 UTF-8 字节 | 例如 200 stored_in_teaching_db:教学 SQL 事务已知提交 | 崩溃恢复实测、设备送达/已读 |
| 10.09 R9 待审变更 | 拟从 6 提到 9 字节 | 只有批准并明确目标接口版本后才能生效 | 当前 v1/v2 自动变成 9 字节 |
v2 只是避免悄悄改变 v1 成功语义的课程设计建议;不是已部署 API,也不是 OpenIM 的接口版本。若学习者最终选择不同版本迁移策略,应在 09.10 的协议兼容章节给出客户端矩阵和正式合同。本章先把三组预期独立保存,避免静态练习中自相矛盾。
二、从需求拆出最少对象、身份和确认点
先不写 server 主函数。列出业务对象:users 稳定标识用户,conversations 标识会话,members 表达当前成员资格,messages 给每条消息稳定 message_id、conversation_id、sender_id、会话内唯一 seq、正文。06.01 的 SQL 四表只是教学关系模型;它的 members(conversation_id,user_id) 当前快照不能记录退出再加入的两段历史,06.03 已说明扩展方式。OpenIM 的实际 MongoDB/队列路径另在第七节对照。
| 用户动作 | 入口提供什么 | 服务端权威来源 | 本阶段输出/状态 |
|---|---|---|---|
| 登录 | 凭据或已有会话 | 认证系统与 Session/Token 状态 | 受信主体 u-a;不代表有权读任意会话 |
读 c-a 历史 | 路径会话 ID、Limit、游标 | u-a 的当前可见规则、消息权威数据 | 一页有序记录或受控拒绝 |
发 m-a | 目标会话、稳定消息 ID、6 B 内正文 | 发件人来自受信身份,成员/事务规则 | 已知提交、明确拒绝或结果未知 |
| 尝试通知在线 B | 已提交消息与当前连接表 | u-b 对应的有效设备连接 | 推送尝试;非设备确认 |
要把 11.01 的确认点放在每张方案图上:请求进入→校验/授权→数据库事务已知提交→A 收到回应→B 设备确认→用户已读。每个箭头都需要独立证据;“应用服务返回 nil”不能自动跨过后四个箭头。当前持久教学方案只有提交这一段可在个人数据库实践中验证,离线与设备回执仍不能假装已经实现。
三、先做有权历史,再做有权发送
建议学习者按一条能力一条失败变式增量实践。第一步是 GET /v2/conversations/c-a/messages?limit=2&before=... 的教学设计:HTTP handler 有界解析;Session/Token 中间件给出受信 u-a;应用按当前成员政策判 c-a 是否可读;数据访问用参数化条件和稳定 seq DESC 查页;有三条纸上记录 m-a(seq=1)、m-b(seq=2)、m-c(seq=3) 时,首页是 m-c,m-b,下一页以 seq<2 取 m-a。u-c 即使知道 c-a 和 m-a 的 ID,也应按 09.02 的隐藏目标政策拒绝,不返回私有正文。
第二步是 POST /v2/conversations/c-a/messages 的新教学合同:请求体最多 4096 B,body 解码后非空且不超过 6 UTF-8 字节;"你好" 6 B 合法,"你好呀" 9 B 超限。未知字段、损坏 JSON、错误媒体类型、正文超限和同 ID 重复各有稳定错误类别;失败后旧消息不改。发件人由已认证主体决定,请求体写 sender_id=u-b 不会让 A 代 B 发信。浏览器 Cookie 入口还要按 09.06 的 CSRF/Origin 设计,任何客户端都要按 09.07 做对象级授权。
HTTP handler:解析方法、路径、内容类型、总大小和错误表示
应用用例:可信主体、会话权限、消息规则、提交点和结果类别
数据访问:参数、Rows/Cursor、唯一约束、事务与资源关闭
领域对象:稳定身份、字节规则、不可变业务事实这张职责表不是要求创建大量空接口或“按每张表一个仓储”的模板。学习者应能从 GET 和 POST 的调用路径指出在哪一层第一次读取不可信字段、在哪一层做授权、在哪一层产生不可逆状态、由谁返回 HTTP 状态。私有测试环境可以注入受控虚构主体,不能把公开服务配置成接受任意 X-Test-User 头的登录后门。
四、教学 SQL 提交如何成为可检查的业务事实
S3 的教学 SQL 写路径可以按纸上顺序设计:确认 A 当前对 c-a 有发送资格,校验 6 B 正文和稳定 ID,分配或核对会话内 seq,插入消息;若会话摘要是本次必须同成同败的权威状态,将它纳入同一事务;若只是可重建的派生预览,则写清允许的陈旧和修复责任。相关数据库操作都要走同一 *sql.Tx,失败回滚、已知成功提交后才向 A 计划返回 stored_in_teaching_db。成员退出与发送并发的裁决顺序需按 06.07 明确,不是 BEGIN 一句就自动正确。
| 写入场景 | 可下的结论 | 不能下的结论 |
|---|---|---|
| 请求在校验前拒绝 | 不应进入写事务 | 数据库里已有的同 ID 消息一定不存在 |
| 唯一约束冲突 | 当前写法未新建第二条同 ID 记录 | 这是同一用户意图的安全重试,须核对合同 |
Commit 已知成功 | 教学数据库内约定变更已提交 | B 设备已收到、A 一定收到 HTTP 回应 |
Commit 附近断网 | 调用方结果未知 | “确定没保存”;不能盲目换新消息 ID |
| A 收到 HTTP 200 | A 观察到服务端给出的那项承诺 | 所有故障模型下已崩溃耐久或已读 |
06.10 的 *sql.DB 是共享池句柄,不逐请求打开关闭;多行历史 Rows 由读取函数关闭并查 Err,旧行可空昵称快照用明确的未知表示。模式迁移遵守 06.03 的扩展→新写入→兼容读取→可信回填→核对→有条件收紧;没有发送时昵称的旧行不可用现在昵称伪造历史。此章不运行数据库,更深的写入确认与崩溃恢复将在 06.09 按具体数据库配置核对。
五、WebSocket 只做在线通道,不偷换成离线保证
若 B 当前有有效连接,服务端可在数据库事务提交之后尝试经 WebSocket 发送“有 m-a 可用”的通知。浏览器通道先经历 TCP、TLS(wss)、HTTP 升级和应用认证;长连接上的每条订阅/发送仍按当前会话成员权限检查。若 B 已退群,旧连接不能因为握手曾成功就继续收到 c-a 的受保护消息。
推送尝试与数据库事务分开意味着:消息可已存而 B 暂时离线;WebSocket Write 成功、TCP ACK 或 Pong 都不证明 B 的 IM 应用展示了消息。A 若要知道“B 已收到/已读”,需要设备端确认、去重身份和可信状态更新,留给 S5。当前项目课程不承诺离线补拉自动完成,也不把通知失败回滚成消息不存在。
04.06 的背压在此变成业务决策:每设备出站队列要有容量/字节上限;慢 B 使 socket 写阻塞时,上游不能无限堆积。可暂停投递、拒绝新的实时任务、仅丢弃明确可再生的提示,或关闭慢连接;对聊天正文,只有当权威消息已保存且后来补拉合同真实存在,才能说“实时通知可丢而内容可恢复”。本 S3 练习尚未建立完整 S5 补拉保证,所以任何丢弃都要标出用户可见失败与未覆盖范围。
TLS 只保护被配置的每一跳。若浏览器与代理间是 WSS、代理到 Go 服务是另一连接,须分别核对两段传输和转发头;证书名称验证成功不等于 u-a 已认证,也不等于 B 有权访问。不要为省事在客户端设置 InsecureSkipVerify,也不在课程仓库放真实密钥。
六、把错误放到提交点前后重新审查
完整服务设计最容易犯的错是把每个 err != nil 都解释成“没发出去”。沿一条 m-a 请求按阶段推演:
| 失败变式 | 发生位置 | 服务端/客户端此刻应表述 | 下一份证据 |
|---|---|---|---|
| JSON 损坏、未知字段、请求体/正文超限 | 解析或校验前 | 拒绝,不修改教学消息状态 | handler 状态与前后快照 |
| 未认证、C 非成员、A 已退群 | 身份/授权 | 拒绝目标动作;不泄露私有历史 | 受控权限日志、成员状态版本 |
同会话 m-a 冲突 | 写入前或唯一约束处 | 不覆盖旧行;重试是否同意图尚须核对 | 稳定消息/操作身份与旧结果 |
| 数据库明确回滚 | 提交前/冲突 | 本事务未提交 | 事务错误类别与状态核对 |
| 提交附近连接断开 | 结果返回阶段 | 结果未知,不立即换 ID | 权威数据源按稳定 ID 查验 |
| A 的 HTTP 回应丢失 | 提交之后、客户端收到之前 | 服务端可能已存,A 未确认 | 服务端提交记录与 A 观察 |
| B 离线、旧 WS 被撤销、慢设备队列满 | 在线尝试阶段 | 不能报 B 已送达 | 连接/权限/队列与未来设备回执 |
| TLS 证书名称不匹配 | 应用 HTTP 之前 | 握手失败,不是会话 404 | 目标名、SAN、链和时间 |
这些结论在已实现并有证据时才可报告。纸上方案只是让学习者知道到哪一层寻找证据。若接口状态要从 S2 accepted_in_memory 改为 S3 stored_in_teaching_db,必须另审客户端理解和兼容范围,不在同一 /v1 路径里暗改语义。11.01/11.03 的日志、指标与 Trace 只作为各阶段的有限观测,不自动证明未埋点的设备侧事实。
七、把教学实现与固定 OpenIM 源码并排,别混成一条链
本项目课程的 SQL/事务模型是为了让初学者先亲手理解权限、提交和故障。固定 openimsdk/open-im-server 提交 f6411a8a1a31d3df36f4c2b3ad28481a94141e1f 中,已核对的 internal/rpc/msg/send.go 群聊和单聊发送分支会调用 MsgDatabase.MsgToMQ,无错后组装发送响应;另一个已核对的 internal/msgtransfer/online_msg_to_mongo_handler.go消费路径调用 BatchInsertChat2DB。单聊仅在 isSend 分支走前述入队调用,不能概括所有分支。
由这两处源码可确定:发送 RPC 返回与这段 MongoDB 消费写入位于不同处理阶段。不能由此再推断 Kafka 生产者确认级别、消息消费失败重试、MongoDB 写关注、跨组件事务或 B 设备送达;那些需要分别追源码和配置。教学 SQL v2 的“本地事务已知提交”与上游 MsgToMQ 返回是两种不同承诺,不能把练习数据表画成 OpenIM 真正使用的 SQL 结构。
交付证据沿 09.11 分层:需求/接口当前合同与拟议变更分开;纯规则和 handler 用例说明输入/错误;隔离数据库验证约束/事务/迁移;真实客户端与网络观察另记;CI 报告绑定提交 SHA、工具链、虚构数据版本与命令,未验证项明确列出。课程作者本次只完成静态正文,没有生成这些运行结果。
八、五个递进里程碑与分层练习
学习者未来在自己的隔离环境按下列顺序实践,每个里程碑先交纸上合同,再自行实现与记录实际证据:
| 阶段 | 新增的能力 | 纸上验收问题 | 真实实践后才填写的记录 |
|---|---|---|---|
| 步骤一:本地消息模型 | 一条到多条、稳定 ID、6 B 字节规则 | 拒绝会不会改旧消息? | 个人输入/输出与测试 |
| 步骤二:S2 内存 HTTP/WS | 有界 GET/POST、在线转发尝试 | 200 accepted_in_memory 到哪一步? | 私有环境 HTTP/WS 与断连观察 |
| 步骤三:S3 受信身份 | Session/Token 与当前会话成员权限 | u-c 改路径能否读 c-a? | 正负权限与注销旧连接结果 |
| 步骤四:教学持久历史 | 四表/独立文档对照、事务、游标与迁移 | Commit 未知、旧快照 NULL 如何表达? | 隔离数据库版本、种子、故障记录 |
| 步骤五:服务答辩与交付 | 失败矩阵、有界队列、逐跳 TLS、CI 文档 | 哪些“已存/已发/已读”仍未验证? | 提交/制品身份、实测与未覆盖范围 |
分层练习:先答,再展开反馈
1. 本章“完整”表示已具备所有 IM 可靠性保证吗?
不表示。它只把 S3 当前功能和证据边界组合完整。
2. 09.02 当前 v1 的正文上限和成功状态是什么?
最多 6 个 UTF-8 字节,200 accepted_in_memory。
3. R9 的 9 字节规则现在能当 v1/v2 已生效合同吗?
不能。它仍是待审变更,需版本和客户端兼容评审。
4. 本章 v2 `stored_in_teaching_db` 是已部署接口吗?
不是,是教学持久服务的纸上新合同。
5. 四张教学表中的 members 能自动保存退群重入两段历史吗?
不能。当前复合键只表达当前关系,需要资格历史扩展。
6. u-c 已登录却请求 c-a 历史会怎样?
按当前隐藏目标政策拒绝,不能返回私有正文。
7. 客户端填 sender_id=u-b 能让 u-a 代 B 发信吗?
不能。发送者由服务端认证主体确定。
8. `"你好呀"` 在当前 6 B 合同下怎样?
它是 9 B,应拒绝且不改消息状态。
9. `GET` 首页 m-c/m-b 后下一游标如何取 m-a?
在同一会话和排序规则下以末项 seq=2 作 seq<2 条件。
10. SQL 参数安全能替代会话成员授权吗?
不能。结构安全与资源权限是不同判断。
11. `*sql.DB` 应每个请求打开并关闭吗?
不应。它是应用复用的连接池句柄。
12. Commit 附近断网能直接说数据库未写吗?
不能。结果未知,沿稳定消息/操作身份查验。
13. 消息库内提交后 WebSocket Write 成功能证明 B 展示了吗?
不能,写调用与设备应用确认不同。
14. B 离线时,当前 S3 课程已承诺自动补拉吗?
没有。完整离线恢复属于 S5 后续合同。
15. 慢设备队列满可以无条件丢弃聊天正文吗?
不能。须有权威保存、失败反馈和真实可用的恢复合同。
16. WSS 证书正确能证明 A 有 c-a 发言权吗?
不能。TLS 服务端身份与应用用户/资源授权分开。
17. 浏览器到代理 TLS 能自动保护代理到数据库吗?
不能。每一跳独立判断。
18. 错误发生在数据库提交后、A 收到回应前,A 应怎样表述?
A 看到结果未知;服务端应凭提交证据和稳定消息 ID 查验。
19. 已核对 OpenIM 发送 RPC 返回的位置等于 MongoDB 消费写入完成吗?
不等于,两处源码在不同阶段调用。
20. 仅看 MsgToMQ 和 BatchInsertChat2DB 两处能证明队列失败重试吗?
不能,还要核对生产者、消费者和配置。
21. CI 绿色能替代真实 B 设备送达观察吗?
不能。需按运行层次和设备确认点分别记录。
22. 交付文档为何要列“未验证”?
避免把纸上设计、局部测试或别的确认点误报为完整服务保证。
来源与下一步
- Go
net/http、数据库事务与httptest:服务入口、数据提交与验证工具的官方 API 语义。 - OWASP 授权指南、RFC 9293:TCP与RFC 8446:TLS 1.3:资源权限和传输边界。
- 固定 OpenIM 发送源码与MongoDB 消费源码:只对本章实际核对的调用位置作有限对照。
按学习路线,S3 服务项目课程到此形成一份可由学习者逐步实践的说明。下一步进入 S4 的02.06 树与有序索引,再深化网络 I/O、存储内部、慢设备与容量;11.08 的服务目标在观测和性能前置齐备后展开,S5 才设计完整异步重试、离线补拉和多设备同步。离开本章前,请能对每个“成功”分别指出谁观察、在哪个版本、哪个提交/时间窗、证据覆盖到哪一步。