前文
在设计和开发 IM 聊天室时,很多人会首先想到 MQTT、XMPP、WebSocket 等技术,因为它们具有高效的实时性和良好的用户体验。然而,当我们将这些方案应用到实际项目中并推向线上后,往往会遇到意想不到的问题:消息数据对不上、消息丢失、两条消息之间丢失数据等。你可能会尝试增加消息订阅的回执操作来补救,但问题依然频繁出现,让开发者手足无措。
这种情况下,是否有更简单且更可靠的解决方案呢?答案是有的,它出乎意料的简单—那就是 HTTP。
HTTP 作为一种稳定可靠的协议,通过同步的请求-响应机制,可以有效地确保消息的有序传递和一致性。虽然它在实时推送方面不如 WebSocket 高效,但它的稳定性和数据一致性使其在 IM 聊天室中具有独特的优势。通过合理设计数据库表结构,并结合通知机制,HTTP 同样能够提供稳定的消息同步体验。
本文将详细介绍如何构建一个基于 HTTP 的 IM 聊天室,从数据库表设计、与服务器的交互流程,到如何利用 HTTP 结合通知机制来确保消息的可靠传递和数据对齐,为用户带来稳定可靠的即时通讯体验。
数据处理与表关系
flowchart TD
subgraph A[消息同步流程]
A1[1. 获取数据] --> A2[2. 数据写入]
A2 -->|好友申请| B[用户表]
A2 -->|好友申请| C[用户关系表]
A2 -->|转账/普通消息| D[消息表]
A3[3. 创建/更新会话] --> |写入| E[会话表]
A4[4. 异步获取用户详情] --> |更新| B
A4 --> |更新| E
A5[5. 批次号处理]
end
A --> F[批次号检查]
F -->|新批次号| A1
F -->|无新数据| G[数据同步结束]
- 根据本地最后批次号向服务端获取数据
- 获取到的(历史/增量)数据
- 好友申请等事件消息,写入用户表与用户关系表中
- 转账、普通文本消息,写入消息表
- 前置数据库写入完毕后,对用户表与用户关系表进行检查
- 是好友的则创建/更新一条数据写入会话表
- 会话表处理完毕后,检查用户表是否获取详细数据
- 已经获取详细数据的,检查会话表是否需要更新头像
- 未获取详细数据的,异步请求服务端获取用户详细数据,获取后更新用户表和会话表
- 数据处理完毕后,比较返回的新批次号
- 新批次号:写入消息同步批次表,重新再执行步骤 1
- 无新数据:说明数据同步已完毕
数据刷新时机
- App首次启动或后台转前台
- App内部推送:可由 MQTT、XMPP、WebSocket 等技术实现
- App外部推送:目前 Android 各厂商推送或 iOS 的 APNs 无法后台运行,需后台转前台后更新
基于这种方式向服务端 HTTP 请求数据,最大限度地保证了同步数据的实效性与完整性,唯一需要注意的是服务端需要做好有效历史数据的截取,例如历史数据最多保留 7 天。
表结构设计
本文不会将所有表或表中的所有字段都列举出来,仅会选取一些必要的字段进行说明。
App 数据库 chat_database_<USER_ID>
, 以下是一些关键表的设计:
App可能会有多个用户登录, 要为每个用户创建独立的 DB文件
消息同步批次表
CREATE TABLE IF NOT EXISTS `chat_inbox_index` (
`batch_id` VARCHAR NOT NULL, -- 批次ID
`user_id` VARCHAR NOT NULL, -- 用户ID
`status` TINYINT NOT NULL DEFAULT 0 -- 状态(0: 开始处理,1: 处理完毕,2: 处理失败)
);
-- 创建索引以提高 user_id 列的查询性能
CREATE INDEX IF NOT EXISTS `idx_chat_inbox_index_batch_id` ON `chat_inbox_index` (`batch_id`);
CREATE INDEX IF NOT EXISTS `idx_chat_inbox_index_user_id` ON `chat_inbox_index` (`user_id`);
消息会话表
CREATE TABLE IF NOT EXISTS `chat_session`(
`join_member_ids` VARCHAR NOT NULL, -- 参与此聊天会话的用户ID
`type` INT NOT NULL, -- 会话类型(100: 普通会话,200: 聊天室会话,300: 系统通知会话)
`title` VARCHAR DEFAULT NULL, -- 会话标题
`last_message_id` VARCHAR DEFAULT NULL, -- 会话消息展示
`unread_count` INT DEFAULT 0 -- 会话未读消息总数
);
-- 创建索引
CREATE INDEX IF NOT EXISTS `idx_chat_session_join_member_ids` ON `chat_session` (`join_member_ids`);
消息表
CREATE TABLE IF NOT EXISTS `chat_session_message` (
`message_id` VARCHAR NOT NULL, -- 会话消息ID
`session_id` INT NOT NULL, -- 聊天会话ID
`class_type` TINYINT NOT NULL, -- 会话消息类型
`content` TEXT DEFAULT NULL, -- 会话消息内容
`attach` TEXT DEFAULT NULL, -- 附加信息
`ref_message_id` VARCHAR DEFAULT NULL, -- 会话消息引用
`type` INT NOT NULL, -- 会话消息类型(如文本、图片、语音等)
`raw_type` INT NOT NULL, -- raw 消息类型
`funds_status` INT DEFAULT NULL, -- 资金状态(仅针对转账、红包)
`status` INT DEFAULT NULL, -- 消息发送状态
`read_type` TINYINT DEFAULT 0, -- 消息是否已读
`read_type_report` TINYINT DEFAULT 0, -- 消息已读是否上报
`send_member_id` VARCHAR NOT NULL, -- 会话消息发送人ID
`send_member_status` TINYINT NOT NULL, -- 是否是自己发言
`receive_member_id` VARCHAR NOT NULL, -- 会话消息接收人ID
`send_date_time` TIMESTAMP DEFAULT NULL, -- 消息发送时间
`focus_me_status` TINYINT NOT NULL -- 是否 @我
);
-- 创建索引以提高查询性能
CREATE INDEX IF NOT EXISTS `idx_chat_session_message_message_id` ON `chat_session_message` (`message_id`);
CREATE INDEX IF NOT EXISTS `idx_chat_session_message_send_member_id` ON `chat_session_message` (`send_member_id`);
CREATE INDEX IF NOT EXISTS `idx_chat_session_message_receive_member_id` ON `chat_session_message` (`receive_member_id`);
CREATE INDEX IF NOT EXISTS `idx_chat_session_message_session_id` ON `chat_session_message` (`session_id`);
用户表
部分列的文本内容会使用 base64 进行编码,主要是考虑到文本中如果有表情等在 SQL 库中可能会被转义处理,导致格式不正确,无法在页面正确显示。
CREATE TABLE IF NOT EXISTS `chat_user`(
`member_id` VARCHAR UNIQUE NOT NULL, -- 用户标识
`phone_number` VARCHAR DEFAULT NULL, -- 用户手机号
`raw_username` VARCHAR DEFAULT NULL, -- 原始用户姓名
`username` VARCHAR DEFAULT NULL, -- 用户姓名 base64编码
`username_pinyin` VARCHAR DEFAULT NULL, -- 用户名拼音
`username_pinyin_short` VARCHAR DEFAULT NULL, -- 用户名拼音缩写
`avatar` VARCHAR DEFAULT NULL, -- 用户头像
`belong_status` TINYINT NOT NULL, -- 是否是当前用户(0: 非当前用户,1: 当前用户)
`raw_user_description` TEXT DEFAULT NULL, -- 原始用户描述
`user_description` TEXT DEFAULT NULL -- 用户描述 base64编码
);
用户关系表
CREATE TABLE IF NOT EXISTS `chat_user_friends` (
`friend_member_id` VARCHAR UNIQUE NOT NULL, -- 好友标识
`relationship_agree_time` TIMESTAMP DEFAULT NULL, -- 确立好友关系时间
`status` TINYINT NOT NULL, -- 关系状态(0: 等待对方同意,2: 好友,等)
`read_status` TINYINT DEFAULT 0 -- 已读状态
);
好友主动/被动申请
文字描述可能不够直观,直接上时序图进行说明
sequenceDiagram
好友搜索页面(UserA) ->> 服务端: 11位手机号查询
服务端 -->> 好友搜索页面(UserA): 返回查询结果
好友搜索页面(UserA) ->> 用户详情页面(UserA): 点击添加好友
用户详情页面(UserA) ->> 用户详情页面(UserA): 编辑申请好友信息
用户详情页面(UserA) ->> 服务端: 发起好友申请
服务端 -->> 用户详情页面(UserA): 响应成功
用户详情页面(UserA) ->> 用户详情页面(UserA): 等待 UserB 同意
用户好友申请列表(UserB) ->> 服务端: 请求新批次数据
服务端 -->> 用户好友申请列表(UserB): 返回数据
用户好友申请列表(UserB) ->> 服务端: 同意UserA好友申请
服务端 -->> 用户好友申请列表(UserB): 响应成功
用户好友申请列表(UserB) ->> 用户好友申请列表(UserB): 将好友关系写入DB
APP(UserA) ->> 服务端: 请求新批次数据
服务端 -->> APP(UserA): UserB 已经同意申请
APP(UserA) ->> APP(UserA): 将好友关系写入DB
总结
HTTP 作为 IM 聊天室的底层传输协议,虽然看似传统,但在可靠性和一致性上具有独特的优势。结合合理的数据库表设计和 HTTP 的请求-响应模式,IM 系统可以确保每一条消息的准确送达,避免复杂的丢包和消息不一致问题。通过通知机制与增量拉取的结合,本文展示了如何构建一个稳定可靠的即时通讯系统,为用户提供可预期和可靠的交互体验。