Skip to content

sherlockzzw/gochat

Repository files navigation

GoChat 服务架构

🏗️ 服务架构

服务分离原则

  • 单一职责:每个服务只负责一个核心功能
  • 独立部署:可以独立启动、停止、扩展
  • 清晰边界:服务间通过API通信,避免直接依赖

服务列表

服务 端口 功能 协议 启动命令
API 8080 用户接口、业务逻辑、WebSocket HTTP/WebSocket go run main.go api
Admin 8081 管理接口、后台管理 HTTP go run main.go admin

🚀 启动方式

方式1:使用脚本(推荐)

# 启动单个服务
./scripts/start.sh api      # 启动API服务
./scripts/start.sh admin    # 启动Admin服务  
./scripts/start.sh ws       # 启动WebSocket服务

# 启动所有服务
./scripts/start.sh all

方式2:直接命令

# 分别启动
go run main.go api
go run main.go admin  
go run main.go ws

📡 API 接口

API服务 (端口 8080)

  • POST /api/login - 用户登录
  • POST /api/user/register - 用户注册
  • GET /api/user/info - 获取用户信息 (需要认证)
  • POST /api/user/profile - 更新用户资料 (需要认证)
  • POST /api/chat/message/send - 发送消息 (需要认证)
  • GET /api/chat/message/history - 获取消息历史 (需要认证)
  • POST /api/chat/upload - 上传文件 (需要认证)
  • GET /api/friend/list - 获取好友列表 (需要认证)
  • GET /api/friend/detail/:friend_id - 获取好友详情 (需要认证)

Admin服务 (端口 8081)

  • POST /admin/user/add - 创建用户
  • GET /admin/user/list - 获取用户列表

WebSocket服务 (端口 8080,与API服务同端口)

  • GET /ws/connect?token=<token>&user_id=<ID> - 建立WebSocket连接
  • 消息推送:通过WebSocket实时推送消息给在线用户

🗄️ 数据库

数据库 用途 连接信息
MySQL 用户基础数据、关系数据、消息索引 localhost:3306
MongoDB 消息存储(主要存储)、历史消息查询 localhost:27017

消息存储架构

双存储策略

  • MongoDB(主存储)

    • 存储所有聊天消息的完整数据
    • 支持高效的分页查询和复杂查询
    • 适合存储大量历史消息
    • 集合名称:chat_messages
  • MySQL(辅助存储)

    • 存储消息索引和元数据
    • 用于快速统计和关系查询
    • 与MongoDB同步存储,确保数据一致性

消息存储流程

发送消息 → 生成消息ID → 同时写入MongoDB和MySQL → 通过WebSocket实时推送

消息查询策略

  • 历史消息查询:优先从MongoDB查询
  • 分页支持:支持按时间倒序分页查询
  • 查询优化:使用索引加速查询(from_user_id, to_user_id, created_at)

🔧 开发建议

1. 本地开发

# 启动所有服务
./scripts/start.sh all

2. 生产部署

# 分别部署到不同服务器
# API服务 - 负载均衡
# WebSocket服务 - 独立服务器
# Admin服务 - 内网访问

3. 监控和日志

  • 每个服务都有独立的日志
  • 使用健康检查接口监控服务状态
  • 建议使用Prometheus + Grafana监控

🎯 优势

  1. 清晰分离:每个服务职责明确
  2. 独立扩展:可以根据负载独立扩展
  3. 故障隔离:一个服务故障不影响其他服务
  4. 技术栈灵活:不同服务可以使用不同技术
  5. 易于维护:代码结构清晰,便于团队协作

💬 消息处理技术

WebSocket实时通信

架构设计

  • Hub模式:使用单Hub管理所有WebSocket连接
  • Goroutine模型:每个连接使用独立的goroutine处理读写
  • 通道通信:使用Go channel实现线程安全的消息传递

稳定性保障机制

1. 心跳机制(Heartbeat)

目的:检测连接是否存活,及时发现并处理断线情况。

实现方式

前端(客户端)

  • 每30秒自动发送ping消息
  • 监听pong响应,更新最后心跳时间
  • 如果60秒内未收到pong,主动断开连接并触发重连

后端(服务端)

  • 接收客户端的ping消息,立即回复pong
  • 使用SetPongHandler处理WebSocket协议层的pong帧
  • 设置读超时(SetReadDeadline),超时自动断开
  • 启动心跳超时检测goroutine,定期检查最后pong时间
  • 如果超过配置的超时时间(默认60秒)未收到心跳,主动断开连接

配置参数config/app.yml):

websocket:
  ping_interval: 30  # 心跳ping间隔(秒)
  pong_wait: 60      # 心跳超时时间(秒)

工作原理

客户端                   服务端
  |                       |
  |--- ping (30s) ------->|
  |<-- pong --------------|
  |                       |
  |--- ping (30s) ------->|
  |<-- pong --------------|
  |                       |
  |--- ping (30s) ------->|
  |    (网络断开)          |
  |                       | (60秒未收到ping)
  |                       | 主动断开连接
2. 断线重连机制(Reconnection)

目的:网络波动或服务重启时,自动恢复连接。

实现策略

指数退避算法(Exponential Backoff)

  • 第1次重连:3秒后
  • 第2次重连:6秒后(3 × 2¹)
  • 第3次重连:12秒后(3 × 2²)
  • 第4次重连:24秒后(3 × 2³)
  • 最大间隔:30秒(防止间隔过长)

重连次数限制

  • 默认最大重连次数:10次
  • 超过最大次数后,停止自动重连,触发reconnect_failed事件
  • 用户可以通过手动调用connect()重新连接

实现代码(前端):

// 指数退避计算
const delay = Math.min(
  this.reconnectInterval * Math.pow(2, this.reconnectAttempts),
  this.maxReconnectInterval
)

重连触发条件

  1. WebSocket连接意外关闭(onclose事件)
  2. 心跳超时检测失败
  3. 网络错误(onerror事件)
3. 读写超时保护

目的:防止连接因网络问题长时间阻塞。

实现方式

读超时

  • 每次收到消息后,重置读超时为pong_wait时间
  • 如果超时未收到任何数据,自动断开连接
  • 使用SetReadDeadline实现

写超时

  • 每次发送消息前,设置写超时为10秒
  • 如果10秒内无法发送,断开连接
  • 使用SetWriteDeadline实现

代码示例(后端):

// 读超时
c.conn.SetReadDeadline(time.Now().Add(c.hub.pongWait))

// 写超时
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
4. 连接状态监控

目的:实时了解连接健康状态,便于问题排查。

监控指标

  • 在线用户数量
  • 连接建立时间
  • 最后心跳时间
  • 消息发送成功率
  • 重连次数统计

日志记录

  • 连接建立/断开事件
  • 心跳超时警告
  • 重连尝试记录
  • 消息发送失败警告
5. 优雅关闭

目的:确保连接关闭时资源正确释放。

关闭流程

  1. 停止心跳定时器
  2. 停止心跳超时检测
  3. 关闭发送通道(close(client.send)
  4. 发送WebSocket关闭帧(CloseMessage
  5. 关闭TCP连接
  6. 从Hub中注销客户端
  7. 清理相关资源

代码示例

defer func() {
    c.hub.unregister <- c
    c.conn.Close()
    c.stopHeartbeat()
}()
6. 异常处理

目的:提高系统容错能力,避免单个连接问题影响整体。

处理策略

  • 发送通道满:关闭连接,让客户端重连
  • 消息解析失败:记录日志,忽略该消息
  • 连接数超限:拒绝新连接,记录警告
  • 用户重复连接:替换旧连接,关闭旧连接

代码示例

// 发送通道满时的处理
select {
case client.send <- message:
    // 发送成功
default:
    // 通道满,关闭连接
    close(client.send)
    delete(h.clients, client)
}
稳定性保障总结
机制 前端 后端 作用
心跳检测 ✅ 发送ping ✅ 回复pong + 超时检测 及时发现断线
自动重连 ✅ 指数退避 - 自动恢复连接
读写超时 - ✅ SetRead/WriteDeadline 防止长时间阻塞
状态监控 ✅ 连接状态 ✅ 日志记录 便于问题排查
优雅关闭 ✅ 清理资源 ✅ 清理资源 防止资源泄漏
异常处理 ✅ 错误捕获 ✅ 容错机制 提高系统稳定性

最佳实践建议

  1. 心跳间隔:建议30秒,平衡及时性和网络开销
  2. 超时时间:建议60秒(2倍心跳间隔),给网络波动留出缓冲
  3. 重连策略:使用指数退避,避免频繁重连造成服务器压力
  4. 监控告警:设置连接数、心跳超时率等监控指标
  5. 日志记录:记录关键事件,便于问题排查

性能优化

缓冲通道(Buffered Channel)详解

什么是缓冲通道? 缓冲通道是Go语言中带缓冲区的channel,可以在没有接收者的情况下先存储一定数量的数据,避免发送操作阻塞。

缓冲通道 vs 无缓冲通道:

// 无缓冲通道:发送和接收必须同时准备好,否则阻塞
ch := make(chan int)

// 缓冲通道:可以存储N个元素,只有缓冲区满时才阻塞
ch := make(chan int, 1000)

本系统缓冲通道配置:

通道名称 缓冲大小 作用 为什么需要缓冲
register 1000 客户端注册队列 处理突发连接请求,避免注册操作阻塞
unregister 1000 客户端注销队列 处理大量同时断开,避免注销操作阻塞
broadcast 1000 广播消息队列 处理系统通知等广播消息,避免阻塞
sendToUser 10000 点对点消息队列 核心队列,处理所有用户消息,需要大缓冲应对突发流量
client.send 1024 单个客户端发送队列 每个连接独立缓冲,处理网络延迟导致的发送堆积

缓冲通道的作用:

  1. 解耦生产者和消费者

    • 发送消息的goroutine(生产者)不需要等待接收者立即处理
    • 接收消息的goroutine(消费者)可以批量处理,提高效率
  2. 平滑流量峰值

    • 当消息发送速度 > 处理速度时,缓冲通道可以暂存消息
    • 避免消息丢失或系统阻塞
    • 例如:10000缓冲可以应对短时间内的消息洪峰
  3. 提高并发性能

    • 非阻塞发送:缓冲区未满时,发送操作立即返回,不阻塞
    • 减少goroutine等待时间,提高CPU利用率
    • 允许更多goroutine并发工作
  4. 防止系统崩溃

    • 当通道满时,使用selectdefault分支可以优雅处理
    • 记录警告日志,而不是让系统阻塞或崩溃
    • 给系统恢复和降级的机会

实际应用场景:

// 场景1:突发消息洪峰
// 假设1000个用户同时发送消息
// 如果没有缓冲通道,Hub.Run()处理不过来会导致阻塞
// 有了10000缓冲,可以暂存这些消息,逐步处理

// 场景2:网络延迟
// 客户端网络慢,writePump()发送速度慢
// client.send的1024缓冲可以暂存待发送消息
// 避免因为网络慢导致整个系统阻塞

// 场景3:连接管理
// 大量用户同时上线/下线
// register/unregister的1000缓冲可以平滑处理

缓冲大小的选择原则:

  1. 根据业务量估算

    • sendToUser: 10000 = 假设每秒1000条消息,缓冲10秒的量
    • client.send: 1024 = 假设网络延迟1秒,每秒1024条消息
  2. 平衡内存和性能

    • 缓冲太大:占用内存多,但性能好
    • 缓冲太小:内存占用少,但容易阻塞
    • 本系统选择:在内存可接受范围内,尽量大
  3. 考虑最坏情况

    • 峰值流量是平均流量的10倍
    • 缓冲大小应该能应对峰值流量

性能指标:

  • 并发能力:理论上支持数万并发连接
  • 消息处理能力:每秒数万条消息
  • 线程安全:使用sync.RWMutex保证线程安全
  • 非阻塞设计:关键路径使用非阻塞操作,避免死锁
资源消耗分析

每个WebSocket连接的内存占用:

组件 内存占用 说明
Client结构体 ~200字节 包含conn、send通道、userID、hub引用
send通道缓冲 ~200KB 1024缓冲 × 平均200字节/消息
Goroutine栈 ~4KB readPump + writePump,每个约2KB
WebSocket连接 ~5KB TCP连接缓冲区、WebSocket协议开销
单连接总计 ~210KB 约0.2MB

Hub全局内存占用:

组件 内存占用 说明
clients map ~8字节/连接 指针存储
userClients map ~16字节/连接 uint + 指针
通道缓冲 ~5MB register/unregister/broadcast/sendToUser
Hub总计 ~5MB + 24字节/连接 可忽略不计

系统基础开销:

组件 内存占用 说明
Go运行时 ~50-100MB GC、调度器等
数据库连接池 ~10-20MB MySQL、MongoDB连接
应用框架 ~20-30MB Gin、中间件等
系统总计 ~80-150MB 基础开销
服务器配置评估

1核2G服务器并发能力分析:

内存计算:

总内存:2GB = 2048MB
系统开销:~150MB(Go运行时 + 数据库 + 框架)
可用内存:~1900MB
单连接内存:~210KB(0.2MB)
理论最大连接数:1900MB ÷ 0.2MB ≈ 9500个连接

CPU计算:

单核CPU限制:
- 每个连接:2个goroutine(readPump + writePump)
- 9500连接 = 19000个goroutine
- Go调度器可以高效调度,但单核CPU是瓶颈
- 实际CPU利用率建议控制在70-80%
- 考虑消息处理、网络I/O等开销

实际建议并发数:

场景 并发连接数 说明
保守估计 2000-3000 保证系统稳定,CPU利用率60-70%
正常使用 3000-5000 平衡性能和稳定性,CPU利用率70-80%
极限压测 5000-7000 CPU接近满载,可能出现延迟
理论最大值 ~9500 内存限制,但CPU会成为瓶颈

影响因素:

  1. 消息频率

    • 低频率(每秒<10条):可支持更多连接
    • 高频率(每秒>100条):需要减少连接数
  2. 消息大小

    • 小消息(<1KB):可支持更多连接
    • 大消息(>10KB):需要减少连接数
  3. 网络带宽

    • 1核2G服务器通常带宽有限(1-10Mbps)
    • 带宽可能成为瓶颈
  4. 数据库性能

    • MongoDB和MySQL的查询性能
    • 连接池大小限制

优化建议:

  1. 连接数限制

    • 已在配置文件中实现:config/app.yml 中的 websocket.max_connections
    • 默认值:5000(可根据服务器配置调整)
    • 设置为0表示无限制(不推荐)
    • 连接数达到上限时,会拒绝新连接(除非是替换同一用户的旧连接)
    • 配置示例:
      websocket:
        max_connections: 3000  # 1核2G服务器建议值
  2. 监控指标

    • 监控内存使用率(建议<80%)
    • 监控CPU使用率(建议<80%)
    • 监控goroutine数量
    • 监控消息队列长度
  3. 降级策略

    • 连接数接近上限时,拒绝新连接
    • 消息队列满时,记录日志并丢弃
    • CPU使用率过高时,限制新连接

生产环境建议:

  • 1核2G服务器:建议并发连接数 2000-3000
  • 2核4G服务器:建议并发连接数 5000-8000
  • 4核8G服务器:建议并发连接数 10000-15000

消息路由机制

发送消息 → SendToUser(userID, message)
  → 入队到sendToUser通道(非阻塞)
  → Hub.Run()从通道取出
  → sendToUserMessage()查找userClients[userID]
  → 找到对应客户端 → 发送到client.send通道
  → writePump() goroutine发送到WebSocket

路由准确性保障

  • 精确映射:使用userClients map[uint]*Client精确映射用户ID到连接
  • 单连接保证:同一用户多个连接时,自动关闭旧连接,只保留最新连接
  • ID验证:发送消息前验证用户ID有效性
  • 线程安全:所有操作都在Hub的Run goroutine中执行

消息推送策略

  • 双向推送:消息同时推送给发送者和接收者,确保双方都能实时看到消息
  • 离线处理:用户不在线时,消息已存储到数据库,用户上线后可查询历史消息
  • 消息格式
    {
      "type": "message",
      "data": {
        "id": "消息ID",
        "from_user_id": 发送者ID,
        "to_user_id": 接收者ID,
        "message_type": 消息类型,
        "content": "消息内容",
        "created_at": 时间戳
      }
    }

连接管理

  • 自动重连:客户端断开后自动清理连接
  • 心跳检测:客户端定期发送心跳保持连接
  • 异常处理:连接异常时自动注销,防止资源泄漏

消息类型支持

  • 文本消息 (type=0):普通文本
  • 图片消息 (type=1):图片文件
  • 文件消息 (type=2):文件附件
  • 表情包 (type=3):表情图片
  • 系统消息 (type=4):系统通知

文件上传

  • 存储位置resources/upload/{fileType}/{date}/
  • 访问路径/static/upload/{相对路径}
  • 文件类型:支持图片、文件等
  • URL返回:返回完整URL(包含域名)

📝 注意事项

  1. 服务发现:生产环境建议使用服务发现机制
  2. 负载均衡:API服务需要负载均衡
  3. 消息队列:高并发时考虑引入消息队列(当前已实现缓冲通道)
  4. 监控告警:建立完善的监控和告警机制
  5. 消息持久化:当前实现中,用户不在线时消息会存储到数据库,但需要前端主动查询历史消息
  6. 连接数限制:当前无连接数限制,生产环境建议添加限制防止资源耗尽
  7. 消息大小限制:单条消息最大1MB,防止内存溢出

About

golng+gin IMchat Service

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages