跳转到内容

Chat System — 聊天系统

V1:想做个简单的聊天,先在自己电脑上试试

Section titled “V1:想做个简单的聊天,先在自己电脑上试试”

你一直想做一个聊天应用,但从零搭 WebSocket 服务感觉太复杂了。 你决定先在自己电脑上模拟:打开两个浏览器标签页,让它们互相发消息。 浏览器有个 BroadcastChannel API,可以让同源的多个标签页互相通信——完美模拟两个”用户”聊天。 消息存在 localStorage 里,关掉标签页再打开也能看到聊天记录。先把前端交互做出来,后端以后再加。

用 BroadcastChannel API 实现跨标签页通信,模拟两个用户在同一台电脑上聊天。

帮我创建一个纯前端的聊天应用,要求如下:
1. 单个 index.html 文件,内联 CSS 和 JS,不需要任何构建工具和外部依赖
2. 核心机制:
- 使用 BroadcastChannel API 实现跨标签页通信
- Channel 名称固定为 "chat-room"
- 打开页面时弹窗输入用户名,存入 sessionStorage(每个标签页独立)
3. 功能:
- 底部输入框 + 发送按钮,支持 Enter 键发送
- 发送消息时通过 BroadcastChannel.postMessage 广播给其他标签页
- 接收消息时通过 onmessage 事件监听
- 消息格式:{id, sender, content, timestamp}
- 自己发的消息靠右(蓝色气泡),别人的靠左(灰色气泡)
4. 聊天记录持久化:
- 所有消息存入 localStorage,key 为 "chat-history"
- 打开页面时加载历史记录并展示
- 新消息同时写入 localStorage 和通过 BroadcastChannel 广播
5. 界面:
- 模仿微信聊天界面的布局
- 消息列表自动滚动到底部
- 显示发送者昵称和时间
- 在线状态:显示当前打开了几个标签页(通过定时心跳广播统计)
  • 打开第一个标签页,输入用户名”小张”,发送一条消息
  • 打开第二个标签页,输入用户名”小李”,能看到小张的消息
  • 小李回复消息,小张的标签页实时收到(无需刷新)
  • 关闭所有标签页再打开,聊天记录还在
  • 自己的消息在右侧蓝色气泡,对方的在左侧灰色气泡
  • 在线人数随标签页的打开/关闭实时变化
  • BroadcastChannel API:同源标签页间的消息通信机制 → M5 消息传递
  • 消息格式设计:id + sender + content + timestamp 的基本消息模型 → M2 数据模型
  • 实时通信的前端体验:消息气泡、自动滚动、在线状态等 UX 设计 → M1 API 基础

V2:同事要用,但我们不在同一台电脑

Section titled “V2:同事要用,但我们不在同一台电脑”

你把聊天页面分享给同事,结果他说:“我收不到你的消息啊。” 对——BroadcastChannel 只在同一个浏览器里有效。不同电脑之间通信,必须经过服务器中转。 你需要搭一个 WebSocket 服务,让多台电脑的浏览器连上来,服务器负责转发消息。 先不考虑消息存数据库,用 JSON 文件记录聊天历史就行——反正现在就三五个人用。

搭建 Go WebSocket 服务,让不同电脑的用户可以实时聊天,消息通过服务器中转。

帮我创建一个基于 WebSocket 的实时聊天服务,要求如下:
1. 技术栈:Go + Gin + gorilla/websocket
2. WebSocket 服务端:
- 端点 /ws?username=xxx,客户端连接时传用户名
- 维护一个连接池 map[string]*websocket.Conn(key 为用户名)
- 收到消息后广播给所有其他已连接的客户端
- 客户端断开时从连接池移除,广播"xxx 已离开"
- 新客户端连接时广播"xxx 已加入"
3. 消息格式(JSON):
- 客户端发送:{"content": "消息内容"}
- 服务端广播:{"id": "uuid", "sender": "用户名", "content": "内容", "type": "message|join|leave", "timestamp": "ISO8601"}
4. 聊天记录持久化:
- 所有消息追加写入 chat_history.json 文件
- 格式为 JSON Lines(每行一条消息)
- GET /api/history?limit=50 接口返回最近 50 条历史消息
- 新用户连接时自动发送最近 20 条历史消息
5. 前端(单个 index.html,由 Gin 提供静态服务):
- 打开时输入用户名,建立 WebSocket 连接
- 聊天界面:消息列表 + 底部输入框
- 显示在线用户列表(侧边栏)
- 连接断开时自动重连(每 3 秒尝试一次,最多 5 次)
6. 心跳检测:
- 服务端每 30 秒向客户端发送 ping
- 客户端 45 秒内没有 pong 则断开连接
  • 启动服务,浏览器打开页面,输入用户名后 WebSocket 连接成功
  • 两台电脑(或两个浏览器)同时连接,可以互相发送和接收消息
  • 有人加入/离开时,其他人看到系统通知
  • 在线用户列表实时更新
  • 重启服务后,新连接时收到之前的历史消息
  • 手动断开网络再恢复,客户端自动重连成功
  • WebSocket 协议:全双工通信 vs HTTP 的请求-响应模式 → M13 网络基础
  • 连接池管理:维护多个长连接,处理连接/断开事件 → M8 扩展性
  • 心跳检测:Ping/Pong 机制保活连接,超时清理死连接 → M13 网络基础
  • JSON Lines 格式:追加写入友好的日志格式,为什么不用 JSON 数组 → M2 数据模型

V3:用户说消息有时候丢了,有时候收到两遍,离线的消息也看不到

Section titled “V3:用户说消息有时候丢了,有时候收到两遍,离线的消息也看不到”

聊天系统用户增长到几百人,三个致命问题暴露了: 第一,网络抖动时消息丢失——发送方以为发出去了,接收方没收到。 第二,客户端重连后重新拉取历史消息,有些消息展示了两遍。 第三,用户 A 离线时用户 B 发了消息,A 上线后完全看不到这些消息。 JSON 文件也扛不住了——几百个人同时聊天,文件读写冲突导致消息偶尔乱序。 你需要重新设计消息系统:数据库存储、消息确认机制、离线消息队列、客户端去重。

用 PostgreSQL 存储消息,实现消息确认机制、离线消息推送和客户端幂等去重。

帮我重构聊天系统为可靠消息传递架构,要求如下:
1. 技术栈:Go + Gin + gorilla/websocket + PostgreSQL
2. 数据库设计:
- messages 表:id BIGSERIAL, client_message_id TEXT (客户端生成的唯一ID),
sender_id INT, chat_room_id INT, content TEXT,
created_at TIMESTAMP, INDEX(chat_room_id, created_at)
- message_delivery 表:id, message_id, recipient_id,
status (pending/delivered/read), delivered_at, read_at
- client_message_id 加唯一索引,用于幂等去重
3. 消息发送流程(可靠传递):
- 客户端生成 client_message_id(UUID),发送消息
- 服务端收到后先查 client_message_id 是否已存在(幂等检查)
- 不存在则写入 messages 表,为每个在线接收者创建 delivery 记录
- 服务端返回 ACK:{"type": "ack", "client_message_id": "...", "server_id": 123}
- 客户端收到 ACK 才标记消息为"已发送",否则 3 秒后重试(最多 3 次)
4. 消息投递确认:
- 在线用户通过 WebSocket 实时推送
- 接收方收到消息后发送 delivery ACK:{"type": "delivery_ack", "message_id": 123}
- 服务端更新 message_delivery 状态为 delivered
- 发送方收到投递回执,显示"已送达"状态
5. 离线消息:
- 用户不在线时,消息写入 messages 表,delivery 状态保持 pending
- 用户上线时,查询所有 pending 的 delivery 记录,推送未读消息
- 推送完成后等待客户端逐条确认
6. 客户端去重:
- 客户端维护已接收的 message_id 集合(最近 1000 条)
- 收到重复 message_id 的消息直接忽略
7. 消息序号:
- 每个聊天室维护一个自增序号 sequence(用 PostgreSQL SEQUENCE)
- 客户端记住最后收到的 sequence,重连时只拉取之后的消息
- GET /api/messages?room_id=1&after_sequence=100&limit=50
8. 提供 docker-compose.yml 包含 PostgreSQL
  • 发送消息后客户端收到 ACK,显示”已发送”状态
  • 接收方确认后发送方显示”已送达”状态
  • 模拟网络断开:消息重试 3 次后仍显示”发送中”
  • 网络恢复后重发相同 client_message_id,服务端幂等处理不产生重复消息
  • 用户 A 离线,B 发消息,A 上线后自动收到离线消息
  • 客户端收到重复 message_id 的消息不会重复展示
  • 重连后通过 after_sequence 只拉取新消息,不重复拉取全部历史
  • 消息可靠传递:ACK 机制、重试策略、at-least-once 语义 → M6 事务与一致性
  • 幂等性设计:client_message_id 唯一索引防止重复写入 → M6 事务与一致性
  • 离线消息队列:delivery 表作为按用户维度的消息队列 → M5 消息传递
  • 消息序号与增量同步:sequence 机制让客户端只拉取新消息 → M2 数据模型
  • 投递状态追踪:pending → delivered → read 的状态机 → M13 网络基础

聊天系统用户增长到 1000 人,大家开始要求群聊功能。项目组要建群讨论、部门要建群通知、兴趣小组要建群交流。 但群聊比 1 对 1 复杂得多:一条消息要扇出给群里所有成员;群可能有几百人,消息量是 1 对 1 的几十倍; 用户想知道谁在打字、消息被多少人读了;群的成员管理(加人、踢人、退出)也需要处理。

在可靠消息传递的基础上实现群聊功能,包括消息扇出、输入状态指示和群级已读回执。

帮我为聊天系统增加群聊功能,要求如下:
1. 技术栈:Go + Gin + gorilla/websocket + PostgreSQL + Redis
2. 群组管理:
- 新增 chat_groups 表:id, name, avatar_url, owner_id, max_members (默认500), created_at
- 新增 group_members 表:group_id, user_id, role (owner/admin/member), nickname,
joined_at, muted_until(禁言截止时间)
- API 接口:
- POST /api/groups:创建群组
- POST /api/groups/:id/members:邀请成员(批量,最多一次加 50 人)
- DELETE /api/groups/:id/members/:user_id:移除成员(仅 owner/admin)
- PUT /api/groups/:id/members/me:修改自己的群昵称
- DELETE /api/groups/:id/members/me:退出群组
- PUT /api/groups/:id:修改群名称/头像(仅 owner/admin)
3. 群消息扇出:
- 群消息写入 messages 表(chat_room_id = group_id,新增 room_type 字段区分群聊/私聊)
- 扇出策略:查询群成员列表,为每个在线成员通过 WebSocket 推送
- 大群优化(成员 > 100):扇出操作异步执行,通过 Redis List 做任务队列
- 每条群消息只在 messages 表存一条,delivery 记录按需创建(仅在线成员 + 离线成员)
4. 输入状态指示(Typing Indicator):
- 客户端输入时发送 {"type": "typing", "room_id": "group_123"}
- 服务端广播给该群其他在线成员:{"type": "typing", "user_id": 1, "username": "小张", "room_id": "group_123"}
- 节流:每个用户每个群每 3 秒最多发一次 typing 事件
- 前端显示:"小张正在输入...",3 秒无新 typing 事件则自动消失
- 多人同时输入时显示:"小张、小李正在输入..."
5. 群级已读回执:
- 每条群消息记录已读人数而非逐个记录(大群逐个记录太占空间)
- Redis Hash:msg_read_count:{message_id} → 已读人数
- 用户打开群聊时,批量上报已读的最新消息 ID:{"type": "group_read", "room_id": "group_123", "last_read_message_id": 456}
- 更新 group_members 表的 last_read_message_id 字段
- 群消息显示"X人已读"而非具体已读名单(私聊仍保留精确已读状态)
6. 群消息未读数:
- 每个群维护 Redis key:group_unread:{group_id}:{user_id}
- 新消息时 INCR 所有群成员的未读计数
- 用户上报已读后 SET 为 0
- 群列表接口返回每个群的未读数
7. 提供 docker-compose.yml 包含 PostgreSQL 和 Redis
  • 创建群组、邀请成员成功,群成员列表正确
  • 群内发消息,所有在线成员实时收到
  • 成员超过 100 的大群,消息扇出异步执行(不阻塞发送方)
  • 用户 A 在群内打字,其他成员看到”A 正在输入…”提示
  • 快速连续打字,typing 事件被节流(不会刷屏)
  • 群消息显示”X人已读”,人数随成员查看而增加
  • 群列表显示每个群的未读消息数,打开群后未读数清零
  • 退出群组后不再收到该群消息
  • 消息扇出策略:小群同步扇出 vs 大群异步扇出的权衡 → M5 消息传递
  • 输入状态指示:高频事件的节流设计,降低服务端压力 → M13 网络基础
  • 群级已读回执:计数 vs 精确记录的空间权衡,大群场景的近似方案 → M2 数据模型
  • 未读计数管理:Redis 原子操作实现高性能计数器 → M4 缓存

V5:单台WebSocket服务器连接数到极限

Section titled “V5:单台WebSocket服务器连接数到极限”

用户增长到 1 万人,高峰期同时在线 5000 个 WebSocket 连接。单台服务器的文件描述符和内存都接近极限。 更严重的问题:一旦服务器重启或崩溃,所有 5000 个连接同时断开,重连风暴直接把服务器打趴。 你还发现:用户 A 在服务器 1 上,用户 B 在服务器 2 上,A 给 B 发消息,B 收不到——因为消息只在本机广播。

部署多台 WebSocket 服务器,用 Redis Pub/Sub 实现跨服务器消息路由,解决连接管理和重连风暴问题。

帮我将聊天系统扩展为多 WebSocket 服务器架构,要求如下:
1. 技术栈:Go + Gin + gorilla/websocket + PostgreSQL + Redis + Nginx
2. 多 WebSocket 服务器:
- 启动 3 个 WebSocket 服务器实例(不同端口:8081, 8082, 8083)
- Nginx 作为 WebSocket 负载均衡器,支持 WebSocket 协议升级(proxy_set_header Upgrade)
- 每个服务器维护自己的本地连接池
3. Redis Pub/Sub 跨服务器消息路由:
- 每个服务器启动时订阅 Redis channel:chat_messages
- 用户发消息时:写入数据库 → 发布到 Redis channel → 所有服务器收到 → 各自推送给本地在线用户
- 群聊消息发布到 channel:chat_messages:{room_id},只有该群成员所在的服务器订阅
- 消息格式:{"message_id": 123, "room_id": "group_1", "sender_id": 1, "content": "...", "exclude_server": "server_1"}
- exclude_server 字段避免发送方所在服务器重复推送
4. 连接注册表(Redis):
- 每个用户的连接信息存入 Redis Hash:connections:{user_id} → {"server_id": "server_1", "connected_at": "..."}
- 用户连接时注册,断开时删除
- 支持查询用户在哪台服务器上:GET /api/users/:id/connection
- 私聊消息可以精准推送:先查注册表,只发布到目标用户所在服务器的 channel
5. 一致性哈希 Sticky Session:
- Nginx 按用户 ID 做一致性哈希,同一用户的 WebSocket 连接始终路由到同一台服务器
- 好处:减少 Redis 注册表的更新频率,同一用户的多个连接在同一台服务器管理
- 配置:nginx upstream 使用 hash $arg_user_id consistent
- 服务器故障时 Nginx 自动 rehash 到其他节点
6. 重连风暴防护:
- 客户端重连增加随机延迟:base_delay * (1 + random(0, 0.5)),指数退避(1s, 2s, 4s, 8s, 最大 30s)
- 服务端连接数限制:单台服务器最多接受 3000 个连接,超限返回 503 并告知客户端延迟重连
- 服务器启动时设置连接接入速率限制:每秒最多接受 100 个新连接
- 优雅关闭:服务器关闭前发送 {"type": "server_shutdown", "reconnect_after": 5} 通知客户端
7. docker-compose.yml 包含 Nginx、3 个 WebSocket 服务器、PostgreSQL、Redis
  • 3 台 WebSocket 服务器启动,Nginx 正确代理 WebSocket 连接
  • 用户 A 在 server_1,用户 B 在 server_2,A 发消息 B 能收到(跨服务器路由)
  • Redis connections 注册表正确记录用户连接信息
  • 同一用户多次连接始终路由到同一台服务器(sticky session)
  • 停掉 1 台服务器,其上的用户自动重连到其他服务器
  • 重连时有随机延迟和指数退避(不是所有客户端同时重连)
  • 单台服务器连接数达到 3000 时拒绝新连接并返回 503
  • WebSocket 负载均衡:Nginx 代理 WebSocket 的配置,协议升级的处理 → M13 网络基础
  • Redis Pub/Sub:发布订阅模式实现跨进程通信,消息路由的设计 → M5 消息传递
  • 连接注册表:分布式环境下追踪用户连接位置的方案 → M8 扩展性
  • Sticky Session:一致性哈希实现会话亲和性,减少状态同步开销 → M8 扩展性
  • 重连风暴防护:指数退避 + 随机抖动 + 速率限制的组合策略 → M13 网络基础

V6:要支持文件/图片发送,消息历史搜索

Section titled “V6:要支持文件/图片发送,消息历史搜索”

用户规模突破 10 万,产品经理带来了两个大需求: 第一,用户要发图片和文件——聊天不能只发文字。图片要能预览缩略图,文件要能下载,还不能太占服务器存储。 第二,用户经常说”之前聊过的那个方案在哪来着?“——需要在海量聊天记录中搜索关键词。 messages 表已经有 5000 万条记录了,LIKE 模糊查询跑一次要 30 秒。旧的聊天记录也需要归档,不能全放在主表里。

实现媒体消息(图片/文件)的上传、存储和展示流程,以及基于 Elasticsearch 的消息全文搜索和历史归档。

帮我为聊天系统增加媒体消息和消息搜索能力,要求如下:
1. 技术栈:Go + Gin + PostgreSQL + Redis + Elasticsearch + MinIO(对象存储)
2. 媒体消息支持:
- 支持消息类型:text(文本)、image(图片)、file(文件)
- messages 表新增字段:message_type, media_url, media_thumbnail_url,
file_name, file_size, mime_type
- 上传流程:
a. 客户端先调用 POST /api/upload 上传文件到服务端
b. 服务端将文件存入 MinIO 对象存储,返回 file_key
c. 客户端发送消息时附带 file_key:{"type": "image", "content": "图片描述", "file_key": "xxx"}
d. 服务端根据 file_key 生成访问 URL 和缩略图 URL,写入 messages 表
- 图片处理:
- 上传图片时自动生成缩略图(最大 200x200),存入 MinIO
- 聊天列表显示缩略图,点击查看原图
- 支持格式:JPG, PNG, GIF, WebP,最大 10MB
- 文件处理:
- 支持任意文件类型,最大 50MB
- 消息气泡显示文件名、大小、下载按钮
- 文件图标根据 mime_type 显示不同样式
- 存储优化:
- 同一文件的 SHA256 哈希相同时不重复存储(deduplication)
- MinIO 配置生命周期策略:超过 1 年的文件自动转入低频存储
3. 消息全文搜索(Elasticsearch):
- 索引名 chat_messages,mapping:
- message_id (keyword), room_id (keyword), sender_id (keyword),
sender_name (text), content (text with ik_max_word analyzer),
message_type (keyword), created_at (date)
- 消息写入数据库后异步索引到 Elasticsearch(通过 Redis 队列)
- 搜索 API:GET /api/search/messages?q=关键词&room_id=group_1&from=0&size=20
- 支持:
- 按聊天室/群组过滤
- 按发送人过滤
- 按时间范围过滤
- 关键词高亮
- 搜索结果返回消息上下文(前后各 2 条消息),方便用户定位对话
4. 消息历史归档:
- 超过 6 个月的消息从 messages 主表迁移到 messages_archive 归档表
- 归档任务每天凌晨 2:00 执行,每次迁移 10 万条
- 归档表结构与主表相同,按月分区(messages_archive_2026_01, messages_archive_2026_02...)
- 查看历史消息时:先查主表,主表没有则查归档表
- 归档消息在 Elasticsearch 中保留索引,搜索不受影响
- 归档统计接口:GET /api/admin/archive-stats 返回各月归档数量
5. docker-compose.yml 包含 PostgreSQL、Redis、Elasticsearch、MinIO
  • 上传图片成功,聊天中显示缩略图,点击能查看原图
  • 上传文件成功,聊天中显示文件信息和下载按钮,下载正常
  • 上传相同文件两次,MinIO 中只存一份(SHA256 去重生效)
  • 搜索关键词返回匹配的消息列表,关键词高亮显示
  • 搜索支持按群组和发送人过滤
  • 搜索结果包含消息上下文(前后各 2 条消息)
  • 归档任务执行后,旧消息从主表迁移到归档表
  • 查看 3 个月前的聊天记录仍然正常(从主表读取)
  • 查看 8 个月前的聊天记录仍然正常(从归档表读取)
  • 归档后的消息仍然可以被搜索到
  • 对象存储:MinIO/S3 存储非结构化数据,与数据库分离存储的架构 → M17 基础设施
  • 文件去重:SHA256 内容寻址,避免重复存储相同文件 → M9 数据处理
  • 全文搜索:Elasticsearch 索引设计、中文分词、高亮匹配 → M10 搜索
  • 数据归档:冷热分离、按时间分区、归档迁移的生命周期管理 → M2 数据模型
  • 异步索引:数据库写入与搜索索引解耦,保证写入性能 → M5 消息传递