一个可以实现高并发的IM(及时通信) api项目
数据库: mysql + gorm
日志框架: zap
映射配置: viper
实时加载: air
web框架: gin
api文档: swag
本项目使用websocket协议建立客户端与服务端的长连接进行及时通讯,关于该协议的了解可以参考websocket基础
项目使用了golang websocket库, 关于该库的使用可以参考 websocket_tutorial
下图是完整的IM系统架构:包含了C端、接入层、S端处理逻辑和消息分发、存储层用来持久化数据。
本项目重点实现服务端和存储端(业务模块包括包括注册,鉴权登录,关系管理,单聊,群聊),代码结构清晰,性能优秀,单机支持几万人在线聊天。
注册的post请求 -> 服务器路由"/v1/members/register" -> 服务层m.Register()方法 -> 模型层models.RegisterMember(data)方法
// ./api/routes/v1/members/register.go
func register(c *gin.Context) {
r := app.Gin{C: c}
// 创建Member结构体实例m
m := service.Member{}
// 绑定请求参数到实例m
if err := c.ShouldBind(&m); err != nil {
r.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)
}
// 调用m实例的Register()服务方法
member, err := m.Register()
if err != nil {
r.Response(http.StatusInternalServerError, e.ERROR_REGISTER_MEMBER, nil)
} else {
r.Response(http.StatusOK, e.SUCCESS, member)
}
}
// ./internal/members.go
func (m *Member) Register() (models.Member, error) {
// 把 绑定的参数 拼接一个data map
data := map[string]interface{}{
"phone_num": m.PhoneNum,
"plain_pwd": m.Password,
"nickname": m.Nickname,
"avatar": m.Avatar,
"gender": m.Gender,
"memo": m.Memo,
}
// data传入model层RegisterMember方法执行创建(所有crud都在model层) 并返回创建后的member实例
member, err := models.RegisterMember(data)
return member, err
}
// ./internal/models/member.go
func RegisterMember(data map[string]interface{}) (Member, error) {
member := Member{}
// 查询数据库中是否存在该用户
if err := db.Where("phone_num = ?", data["phone_num"].(string)).Take(&member).Error; err != nil && !gorm.IsRecordNotFoundError(err) {
logger.Error("model member register find error", zap.String("phone_num", data["phone_num"].(string)), zap.String("error", err.Error()))
return Member{}, err
}
// id大于0表示存在该用户,返回注册失败
if member.ID > 0 {
err := errors.New("phone number has been registered")
logger.Error("model member register phone_num has been registered error", zap.String("phone_num", data["phone_num"].(string)), zap.String("error", err.Error()))
return Member{}, err
}
// 传参给新用户实例
member.PhoneNum = data["phone_num"].(string)
member.Avatar = data["avatar"].(string)
member.Gender = data["gender"].(string)
member.Nickname = data["nickname"].(string)
member.Memo = data["memo"].(string)
member.Salt = fmt.Sprintf("%06d", rand.Int31n(10000)) // 随机生成密码盐值
member.Password = util.MakePwd(data["plain_pwd"].(string), member.Salt) // 执行加密保存
// 存入数据库,创建用户成功
if err := db.Create(&member).Error; err != nil {
logger.Error("model member register create error", zap.Any("member", member), zap.String("error", err.Error()))
return Member{}, err
}
return member, nil
}
项目结构遵循 https://github.com/golang-standards/project-layout
.
├── api // 接口模块
│ └── routes // 路由
│ └── v1 // 路由版本
│ ├── attaches // 用户上传附件接收路由
│ ├── chats // 聊天路由
│ ├── contacts // 用户关系(加好友、加群、建群等)路由
│ └── members // 用户登录注册路由
├── cmd // 控制模块(包含项目启动调用的main.go文件)
│ ├── api_test // 接口测试
│ └── base // 项目启动前预配置(如gin添加监听日志等)
├── config // 项目配置信息(详见config.yml, conf.go负责读取yml)
├── docs // 存放swagger文档信息
├── examples // 示例代码
│ └── websocket_tutorial // websocket库调用教程
│ └── resources
├── internal // 内部代码模块
│ ├── models // 模型(models.go使用gorm连接数据库,contact.go、group.go、member.go实现建模,对数据库crud)
│ └── service // 服务 (路由把请求发送给服务,服务拼接参数发送给模型执行crud)
├── log // 日志输出(目前主要使用[info,error,debug]三个级别的日志,前两个对应info.log,err.log; debug对应event/event.log)
│ └── event
├── pkg // 公共代码模块
│ ├── app // 应用响应
│ ├── e // 错误码
│ ├── logger // 日志配置
│ └── util // 通用工具包
├── resources // 用户上传文件保存目录(先暂时存到服务器)
├── sql // 建表sql语句
├── third_party // 第三方服务目录
└── tmp // 临时目录,存放go项目编译生成的二进制文件
-
安装MySQL
-
创建数据库karlmalone,执行sql/create_table.sql,完成初始化表的创建
-
修改config文件夹下配置文件config.yml,使之和你本地配置一致
-
执行以下命令运行项目
$ air # 你没看错,只用敲三个字母就可以启动项目,具体配置可见`./.air.conf`文件
或者执行
$ swag init -g ./cmd/main.go # 更新swagger接口文档(如果更新了接口代码,则执行此句) $ go build -o ./tmp/malone ./cmd $ ./tmp/malone
-
浏览器查看swagger api文档:
localhost:[your_port]/swagger/index.html
(备注:[your_port] 默认为 5288)
非常欢迎你的加入!提一个 Issue 或者提交一个 Pull Request。
KarlMalone 遵循 Contributor Covenant 行为规范。
MIT © Drew Lee