IM APP设计全解析:从数据库到数据交互的完整方案

329 阅读6分钟

IM前文配图.webp

前文

在设计和开发 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. 根据本地最后批次号向服务端获取数据
  2. 获取到的(历史/增量)数据
    • 好友申请等事件消息,写入用户表与用户关系表中
    • 转账、普通文本消息,写入消息表
  3. 前置数据库写入完毕后,对用户表与用户关系表进行检查
    • 是好友的则创建/更新一条数据写入会话表
  4. 会话表处理完毕后,检查用户表是否获取详细数据
    • 已经获取详细数据的,检查会话表是否需要更新头像
    • 未获取详细数据的,异步请求服务端获取用户详细数据,获取后更新用户表和会话表
  5. 数据处理完毕后,比较返回的新批次号
    • 新批次号:写入消息同步批次表,重新再执行步骤 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 系统可以确保每一条消息的准确送达,避免复杂的丢包和消息不一致问题。通过通知机制与增量拉取的结合,本文展示了如何构建一个稳定可靠的即时通讯系统,为用户提供可预期和可靠的交互体验。

撰写不易,请给个赞👍吧