├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── build_proto.sh ├── cmd ├── connect │ ├── Dockerfile │ └── main.go ├── file │ ├── Dockerfile │ └── main.go ├── logic │ ├── Dockerfile │ └── main.go └── user │ ├── Dockerfile │ └── main.go ├── config ├── compose_builder.go ├── config.go ├── k8s_builder.go └── local_builder.go ├── deploy ├── compose │ ├── compose.yaml │ ├── my.cnf │ └── redis.conf └── k8s │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── configmap │ │ └── configmap.yaml │ ├── namespace.yaml │ ├── role │ │ └── cluster_role.yaml │ └── server │ │ ├── connect.yaml │ │ ├── logic.yaml │ │ └── user.yaml │ └── values.yaml ├── docs ├── k8s.md ├── plan.md ├── 流程图 │ ├── 心跳.puml │ ├── 消息单发.puml │ ├── 消息群发.puml │ ├── 登录.puml │ └── 离线消息同步.puml └── 错误处理.md ├── go.mod ├── go.sum ├── internal ├── connect │ ├── api.go │ ├── client_test.go │ ├── conn.go │ ├── conn_manager.go │ ├── mq.go │ ├── room.go │ ├── tcp_server.go │ └── ws_server.go ├── logic │ ├── device │ │ ├── api.go │ │ ├── api_test.go │ │ ├── app.go │ │ ├── entity.go │ │ ├── repo.go │ │ └── repo_test.go │ ├── friend │ │ ├── api.go │ │ ├── api_test.go │ │ ├── app.go │ │ ├── entity.go │ │ ├── repo.go │ │ └── repo_test.go │ ├── group │ │ ├── api.go │ │ ├── api_test.go │ │ ├── app.go │ │ ├── domain │ │ │ ├── group.go │ │ │ └── group_user.go │ │ └── repo │ │ │ ├── group_cache.go │ │ │ ├── group_dao.go │ │ │ ├── group_dao_test.go │ │ │ ├── group_repo.go │ │ │ ├── group_user_repo.go │ │ │ └── group_user_repo_test.go │ ├── message │ │ ├── api.go │ │ ├── api_test.go │ │ ├── app.go │ │ ├── domain │ │ │ ├── message.go │ │ │ └── user_message.go │ │ └── repo │ │ │ ├── device_ack_repo.go │ │ │ ├── message_repo.go │ │ │ ├── message_repo_test.go │ │ │ ├── seq_repo.go │ │ │ ├── seq_repo_test.go │ │ │ ├── user_message_repo.go │ │ │ └── user_message_repo_test.go │ └── room │ │ ├── api.go │ │ ├── api_test.go │ │ ├── app.go │ │ ├── message_repo.go │ │ └── seq_repo.go └── user │ ├── api │ ├── user_ext.go │ ├── user_ext_test.go │ ├── user_int.go │ └── user_int_test.go │ ├── app │ ├── auth_app.go │ └── user_app.go │ ├── domain │ ├── device.go │ └── user.go │ └── repo │ ├── auth_repo.go │ └── user_repo.go ├── pkg ├── codec │ └── uvarint.go ├── db │ └── db.go ├── gerrors │ ├── connect.go │ ├── define.go │ ├── error.go │ ├── logic.go │ └── user.go ├── grpclib │ ├── picker │ │ └── addr_picker.go │ └── resolver │ │ ├── addrs │ │ └── addrs_resolver.go │ │ └── k8s │ │ ├── k8s_resolver.go │ │ └── k8s_resolver_test.go ├── interceptor │ └── interceptor.go ├── k8sutil │ └── k8sutil.go ├── local │ └── local.go ├── logger │ └── logger.go ├── md │ └── context.go ├── mq │ └── mq.go ├── protocol │ ├── pb │ │ ├── connectpb │ │ │ ├── connect.ext.pb.go │ │ │ ├── connect.int.pb.go │ │ │ └── connect.int_grpc.pb.go │ │ ├── logicpb │ │ │ ├── device.ext.pb.go │ │ │ ├── device.ext_grpc.pb.go │ │ │ ├── device.int.pb.go │ │ │ ├── device.int_grpc.pb.go │ │ │ ├── friend.ext.pb.go │ │ │ ├── friend.ext_grpc.pb.go │ │ │ ├── group.ext.pb.go │ │ │ ├── group.ext_grpc.pb.go │ │ │ ├── message.ext.pb.go │ │ │ ├── message.int.pb.go │ │ │ ├── message.int_grpc.pb.go │ │ │ ├── room.ext.pb.go │ │ │ ├── room.ext_grpc.pb.go │ │ │ ├── room.int.pb.go │ │ │ └── room.int_grpc.pb.go │ │ └── userpb │ │ │ ├── user.ext.pb.go │ │ │ ├── user.ext_grpc.pb.go │ │ │ ├── user.int.pb.go │ │ │ └── user.int_grpc.pb.go │ └── proto │ │ ├── connect │ │ ├── connect.ext.proto │ │ └── connect.int.proto │ │ ├── logic │ │ ├── device.ext.proto │ │ ├── device.int.proto │ │ ├── friend.ext.proto │ │ ├── group.ext.proto │ │ ├── message.ext.proto │ │ ├── message.int.proto │ │ ├── room.ext.proto │ │ └── room.int.proto │ │ └── user │ │ ├── user.ext.proto │ │ └── user.int.proto ├── rpc │ └── rpc.go ├── session │ └── session.go ├── urlwhitelist │ └── urlwhitelist.go └── util │ ├── json.go │ ├── message.go │ ├── panic.go │ ├── redis.go │ ├── string.go │ ├── time.go │ └── uid │ ├── uid.go │ └── uid_test.go ├── publish.sh └── sql └── create_table.sql /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 简要介绍 2 | gim是一个即时通讯服务器,代码全部使用golang完成。主要特性 3 | 1.支持tcp,websocket接入 4 | 2.离线消息同步 5 | 3.单用户多设备同时在线 6 | 4.单聊,群聊,以及房间聊天场景 7 | 5.支持服务水平扩展 8 | 6.使用领域驱动设计 9 | 7.支持裸机部署和k8s部署 10 | gim可以作为以业务服务器的一个组件,为现有业务服务器提供im的能力,业务服务器 11 | 只需要实现user.int.proto协议中定义的GRPC接口,为gim服务提供基本的用户功能即可 12 | ### 使用技术: 13 | 数据库:MySQL+Redis 14 | 通讯框架:GRPC 15 | 长连接通讯协议:Protocol Buffers 16 | ORM框架:GORM 17 | ### 安装部署 18 | 1.首先安装MySQL,Redis 19 | 2.创建数据库gim,执行sql/create_table.sql,完成初始化表的创建(数据库包含提供测试的一些初始数据) 20 | 3.修改config下配置文件,使之和你本地配置一致,如果没有配置gim_env环境变量,默认会加载config/local_conf.go配置 21 | 4.分别切换到cmd的connect,logic,business目录下,执行go run main.go,启动TCP连接层服务器,WebSocket连接层服务器,逻辑层服务器,用户服务器 22 | (注意:connect只能在linux下启动,如果想在其他平台下启动,请安装docker,执行cmd/connect/run.sh) 23 | ### 项目目录简介 24 | 项目结构遵循 https://github.com/golang-standards/project-layout 25 | ``` 26 | cmd: 服务启动入口 27 | config: 服务配置 28 | deploy 部署配置文件 29 | internal: 服务私有代码 30 | pkg: 服务共有代码 31 | sql: 项目sql文件 32 | ``` 33 | ### 服务简介 34 | 1.connect 35 | 维持与客户端的TCP和WebSocket长连接,心跳,以及TCP拆包粘包,消息编解码 36 | 2.logic 37 | 设备信息,好友信息,群组信息管理,消息转发逻辑 38 | 3.user 39 | 一个简单的用户服务,可以根据自己的业务需求,进行扩展,但是前提是,你的业务服务器实现了user.int.proto接口 40 | ### 客户端接入流程 41 | 1.调用RegisterDevice接口,完成设备注册,获取设备ID(device_id),注意,一个设备只需完成一次注册即可,后续如果本地有device_id,就不需要注册了,举个例子,如果是APP第一次安装,就需要调用这个接口,后面即便是换账号登录,也不需要重新注册。 42 | 2.调用SignIn接口,完成账户登录,获取账户登录的token。 43 | 3.建立长连接,使用步骤2拿到的token,完成长连接登录。 44 | 如果是web端,需要调用建立WebSocket时,如果是APP端,就需要建立TCP长连接。 45 | 在完成建立TCP长连接时,第一个包应该是长连接登录包(SignInInput),如果信息无误,客户端就会成功建立长连接。 46 | 4.使用长连接发送消息同步包(SyncInput),完成离线消息同步,注意:seq字段是客户端接收到消息的最大同步序列号,如果用户是换设备登录或者第一次登录,seq应该传0。 47 | 接下来,用户可以使用LogicExt.SendMessage接口来发送消息,消息接收方可以使用长连接接收到对应的消息。 48 | ### 单用户多设备支持,离线消息同步 49 | 每个用户都会维护一个自增的序列号,当用户A给用户B发送消息是,首先会获取A的最大序列号,设置为这条消息的seq,持久化到用户A的消息列表, 50 | 再通过长连接下发到用户A账号登录的所有设备,再获取用户B的最大序列号,设置为这条消息的seq,持久化到用户B的消息列表,再通过长连接下发 51 | 到用户B账号登录的所有设备。 52 | 假如用户的某个设备不在线,在设备长连接登录时,用本地收到消息的最大序列号,到服务器做消息同步,这样就可以保证离线消息不丢失。 53 | ### 读扩散和写扩散 54 | 首先解释一下,什么是读扩散,什么是写扩散 55 | #### 读扩散 56 | **简介**:群组成员发送消息时,先建立一个会话,都将这个消息写入这个会话中,同步离线消息时,需要同步这个会话的未同步消息 57 | **优点**:每个消息只需要写入数据库一次就行,减少数据库访问次数,节省数据库空间 58 | **缺点**:一个用户有n个群组,客户端每次同步消息时,要上传n个序列号,服务器要对这n个群组分别做消息同步 59 | #### 写扩散 60 | **简介**:在群组中,每个用户维持一个自己的消息列表,当群组中有人发送消息时,给群组的每个用户的消息列表插入一条消息即可 61 | **优点**:每个用户只需要维护一个序列号和消息列表 62 | **缺点**:一个群组有多少人,就要插入多少条消息,当群组成员很多时,DB的压力会增大 63 | ### 消息转发逻辑选型以及特点 64 | #### 群组: 65 | 采用写扩散,群组成员信息持久化到数据库保存。支持消息离线同步。 66 | #### 房间: 67 | 采用读扩散,会将消息短暂的保存到Redis,长连接登录消息同步不会同步离线消息。 68 | ### 核心流程时序图 69 | #### 长连接登录 70 | ![登录.png](https://upload-images.jianshu.io/upload_images/5760439-2e54d3c5dd0a44c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 71 | #### 离线消息同步 72 | ![离线消息同步.png](https://upload-images.jianshu.io/upload_images/5760439-aa513ea0de851e12.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 73 | #### 心跳 74 | ![心跳.png](https://upload-images.jianshu.io/upload_images/5760439-26d491374da3843b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 75 | #### 消息单发 76 | c1.d1和c1.d2分别表示c1用户的两个设备d1和d2,c2.d3和c2.d4同理 77 | ![消息单发.png](https://upload-images.jianshu.io/upload_images/5760439-35f1a91c8d7fffa6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 78 | #### 群组消息群发 79 | c1,c2.c3表示一个群组中的三个用户 80 | ![消息群发.png](https://upload-images.jianshu.io/upload_images/5760439-47a87c45b899b3f9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 81 | ### github 82 | https://github.com/alberliu/gim 83 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | server=$1 6 | version=$2 7 | tag=$server:$version 8 | 9 | echo "构建"$server"服务----------------------" 10 | 11 | cd cmd/$server 12 | # 打包可执行文件 13 | GOOS=linux go build -o $server main.go 14 | 15 | # 构建镜像 16 | docker build -t $tag . 17 | 18 | rm -rf $server 19 | 20 | echo $tag 21 | echo 22 | 23 | -------------------------------------------------------------------------------- /build_proto.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | rm -rf pkg/protocol/pb 6 | 7 | buildDir(){ 8 | dir=$1 9 | protoc -I pkg/protocol/proto --go_out=.. --go-grpc_out=.. pkg/protocol/proto/$dir/*.proto 10 | } 11 | 12 | buildDir connect 13 | buildDir logic 14 | buildDir user 15 | -------------------------------------------------------------------------------- /cmd/connect/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dockerpull.cn/library/alpine:3.14 2 | COPY connect . 3 | CMD ["/connect"] -------------------------------------------------------------------------------- /cmd/connect/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "google.golang.org/grpc" 12 | 13 | "gim/config" 14 | "gim/internal/connect" 15 | "gim/pkg/interceptor" 16 | "gim/pkg/logger" 17 | pb "gim/pkg/protocol/pb/connectpb" 18 | "gim/pkg/protocol/pb/logicpb" 19 | "gim/pkg/rpc" 20 | ) 21 | 22 | func main() { 23 | logger.Init("connect") 24 | 25 | // 启动TCP长链接服务器 26 | go func() { 27 | connect.StartTCPServer(config.Config.ConnectTCPListenAddr) 28 | }() 29 | 30 | // 启动WebSocket长链接服务器 31 | go func() { 32 | connect.StartWSServer(config.Config.ConnectWSListenAddr) 33 | }() 34 | 35 | // 启动服务订阅 36 | connect.StartSubscribe() 37 | 38 | server := grpc.NewServer(grpc.ChainUnaryInterceptor(interceptor.NewInterceptor(nil))) 39 | 40 | // 监听服务关闭信号,服务平滑重启 41 | go func() { 42 | c := make(chan os.Signal, 1) 43 | signal.Notify(c, syscall.SIGTERM) 44 | s := <-c 45 | slog.Info("server stop start", "signal", s) 46 | _, _ = rpc.GetDeviceIntClient().ServerStop(context.TODO(), &logicpb.ServerStopRequest{ConnAddr: config.Config.ConnectLocalAddr}) 47 | slog.Info("server stop end") 48 | 49 | server.GracefulStop() 50 | }() 51 | 52 | pb.RegisterConnectIntServiceServer(server, &connect.ConnIntService{}) 53 | listener, err := net.Listen("tcp", config.Config.ConnectRPCListenAddr) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | slog.Info("rpc服务已经开启") 59 | err = server.Serve(listener) 60 | if err != nil { 61 | slog.Error("serve error", "error", err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/file/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dockerpull.cn/library/alpine:3.14 2 | COPY file . 3 | CMD ["/file"] -------------------------------------------------------------------------------- /cmd/file/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | 12 | "gim/pkg/logger" 13 | "gim/pkg/util" 14 | ) 15 | 16 | const baseUrl = "http://111.229.238.28:8085/file/" 17 | 18 | type Response struct { 19 | Code int `json:"code"` 20 | Message string `json:"message"` 21 | Data interface{} `json:"data"` 22 | } 23 | 24 | func main() { 25 | logger.Init("file") 26 | 27 | router := gin.Default() 28 | router.Static("/file", "/data/file") 29 | 30 | // Set a lower memory limit for multipart forms (default is 32 MiB) 31 | router.MaxMultipartMemory = 8 << 20 // 8 MiB 32 | router.POST("/upload", func(c *gin.Context) { 33 | // single file 34 | file, err := c.FormFile("file") 35 | if err != nil { 36 | c.JSON(http.StatusOK, Response{Code: 1001, Message: err.Error()}) 37 | return 38 | } 39 | 40 | filenames := strings.Split(file.Filename, ".") 41 | name := strconv.FormatInt(time.Now().UnixNano(), 10) + "-" + util.RandString(30) + "." + filenames[len(filenames)-1] 42 | filePath := "/data/file/" + name 43 | err = c.SaveUploadedFile(file, filePath) 44 | if err != nil { 45 | c.JSON(http.StatusOK, Response{Code: 1001, Message: err.Error()}) 46 | return 47 | } 48 | 49 | c.JSON(http.StatusOK, Response{ 50 | Code: 0, 51 | Message: "success", 52 | Data: map[string]string{"url": baseUrl + name}, 53 | }) 54 | }) 55 | err := router.Run(":8085") 56 | if err != nil { 57 | slog.Error("Run error", "error", err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cmd/logic/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dockerpull.cn/library/alpine:3.14 2 | COPY logic . 3 | CMD ["/logic"] -------------------------------------------------------------------------------- /cmd/logic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "net" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "google.golang.org/grpc" 11 | 12 | "gim/config" 13 | "gim/internal/logic/device" 14 | "gim/internal/logic/friend" 15 | "gim/internal/logic/group" 16 | "gim/internal/logic/message" 17 | "gim/internal/logic/room" 18 | "gim/pkg/interceptor" 19 | "gim/pkg/logger" 20 | pb "gim/pkg/protocol/pb/logicpb" 21 | "gim/pkg/urlwhitelist" 22 | ) 23 | 24 | func main() { 25 | logger.Init("logic") 26 | 27 | server := grpc.NewServer(grpc.ChainUnaryInterceptor(interceptor.NewInterceptor(urlwhitelist.Logic))) 28 | 29 | // 监听服务关闭信号,服务平滑重启 30 | go func() { 31 | c := make(chan os.Signal, 1) 32 | signal.Notify(c, syscall.SIGTERM) 33 | s := <-c 34 | slog.Info("server stop", "signal", s) 35 | server.GracefulStop() 36 | }() 37 | 38 | pb.RegisterDeviceExtServiceServer(server, &device.DeviceExtService{}) 39 | pb.RegisterDeviceIntServiceServer(server, &device.DeviceIntService{}) 40 | pb.RegisterMessageIntServiceServer(server, &message.MessageIntService{}) 41 | pb.RegisterFriendExtServiceServer(server, &friend.FriendExtService{}) 42 | pb.RegisterGroupExtServiceServer(server, &group.GroupExtService{}) 43 | pb.RegisterRoomExtServiceServer(server, &room.RoomExtService{}) 44 | pb.RegisterRoomIntServiceServer(server, &room.RoomIntService{}) 45 | 46 | listen, err := net.Listen("tcp", config.Config.LogicRPCListenAddr) 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | slog.Info("rpc服务已经开启") 52 | err = server.Serve(listen) 53 | if err != nil { 54 | slog.Error("serve error", "error", err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/user/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dockerpull.cn/library/alpine:3.14 2 | COPY user . 3 | CMD ["/user"] -------------------------------------------------------------------------------- /cmd/user/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "net" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "google.golang.org/grpc" 11 | 12 | "gim/config" 13 | "gim/internal/user/api" 14 | "gim/pkg/interceptor" 15 | "gim/pkg/logger" 16 | pb "gim/pkg/protocol/pb/userpb" 17 | "gim/pkg/urlwhitelist" 18 | ) 19 | 20 | func main() { 21 | logger.Init("user") 22 | 23 | server := grpc.NewServer(grpc.ChainUnaryInterceptor(interceptor.NewInterceptor(urlwhitelist.User))) 24 | 25 | // 监听服务关闭信号,服务平滑重启 26 | go func() { 27 | c := make(chan os.Signal, 1) 28 | signal.Notify(c, syscall.SIGTERM) 29 | s := <-c 30 | slog.Info("server stop", "signal", s) 31 | server.GracefulStop() 32 | }() 33 | 34 | pb.RegisterUserIntServiceServer(server, &api.UserIntService{}) 35 | pb.RegisterUserExtServiceServer(server, &api.UserExtService{}) 36 | listen, err := net.Listen("tcp", config.Config.UserRPCListenAddr) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | slog.Info("rpc服务已经开启") 42 | err = server.Serve(listen) 43 | if err != nil { 44 | slog.Error("serve error", "error", err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/compose_builder.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net" 7 | 8 | "google.golang.org/grpc/balancer/roundrobin" 9 | 10 | "gim/pkg/grpclib/picker" 11 | _ "gim/pkg/grpclib/resolver/addrs" 12 | "gim/pkg/protocol/pb/connectpb" 13 | "gim/pkg/protocol/pb/logicpb" 14 | "gim/pkg/protocol/pb/userpb" 15 | ) 16 | 17 | type composeBuilder struct{} 18 | 19 | func (*composeBuilder) Build() Configuration { 20 | addrs, err := net.LookupHost("connect") 21 | if err != nil { 22 | slog.Error("composeBuilder Build error", "error", err) 23 | panic(err) 24 | } 25 | if len(addrs) == 0 { 26 | slog.Error("composeBuilder Build error addrs is nil") 27 | panic(err) 28 | } 29 | connectLocalIP := addrs[0] 30 | 31 | return Configuration{ 32 | LogLevel: slog.LevelDebug, 33 | LogFile: func(server string) string { 34 | return fmt.Sprintf("/data/log/%s/log.log", server) 35 | }, 36 | 37 | MySQL: "root:123456@tcp(mysql:3306)/gim?charset=utf8mb4&parseTime=true&loc=Local", 38 | RedisHost: "redis:6379", 39 | RedisPassword: "123456", 40 | PushRoomSubscribeNum: 100, 41 | PushAllSubscribeNum: 100, 42 | 43 | ConnectLocalAddr: connectLocalIP + ":8000", 44 | ConnectRPCListenAddr: ":8000", 45 | ConnectTCPListenAddr: ":8001", 46 | ConnectWSListenAddr: ":8002", 47 | 48 | LogicRPCListenAddr: ":8010", 49 | UserRPCListenAddr: ":8020", 50 | FileHTTPListenAddr: "8030", 51 | 52 | ConnectIntClientBuilder: func() connectpb.ConnectIntServiceClient { 53 | conn := newGrpcClient("dns:///connect:8000", picker.AddrPickerName) 54 | return connectpb.NewConnectIntServiceClient(conn) 55 | }, 56 | 57 | DeviceIntClientBuilder: func() logicpb.DeviceIntServiceClient { 58 | conn := newGrpcClient("dns:///logic:8010", roundrobin.Name) 59 | return logicpb.NewDeviceIntServiceClient(conn) 60 | }, 61 | MessageIntClientBuilder: func() logicpb.MessageIntServiceClient { 62 | conn := newGrpcClient("dns:///logic:8010", roundrobin.Name) 63 | return logicpb.NewMessageIntServiceClient(conn) 64 | }, 65 | RoomIntClientBuilder: func() logicpb.RoomIntServiceClient { 66 | conn := newGrpcClient("dns:///logic:8010", roundrobin.Name) 67 | return logicpb.NewRoomIntServiceClient(conn) 68 | }, 69 | 70 | UserIntClientBuilder: func() userpb.UserIntServiceClient { 71 | conn := newGrpcClient("dns:///user:8020", roundrobin.Name) 72 | return userpb.NewUserIntServiceClient(conn) 73 | }, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/credentials/insecure" 11 | "google.golang.org/grpc/metadata" 12 | 13 | "gim/pkg/protocol/pb/connectpb" 14 | "gim/pkg/protocol/pb/logicpb" 15 | "gim/pkg/protocol/pb/userpb" 16 | ) 17 | 18 | const EnvLocal = "local" 19 | 20 | var ENV = os.Getenv("ENV") 21 | 22 | var builders = map[string]Builder{ 23 | "local": &localBuilder{}, 24 | "compose": &composeBuilder{}, 25 | "k8s": &k8sBuilder{}, 26 | } 27 | 28 | var Config Configuration 29 | 30 | type Builder interface { 31 | Build() Configuration 32 | } 33 | 34 | type Configuration struct { 35 | LogLevel slog.Level 36 | LogFile func(server string) string 37 | 38 | MySQL string 39 | RedisHost string 40 | RedisPassword string 41 | PushRoomSubscribeNum int 42 | PushAllSubscribeNum int 43 | 44 | ConnectLocalAddr string 45 | ConnectRPCListenAddr string 46 | ConnectTCPListenAddr string 47 | ConnectWSListenAddr string 48 | 49 | LogicRPCListenAddr string 50 | UserRPCListenAddr string 51 | FileHTTPListenAddr string 52 | 53 | ConnectIntClientBuilder func() connectpb.ConnectIntServiceClient 54 | DeviceIntClientBuilder func() logicpb.DeviceIntServiceClient 55 | MessageIntClientBuilder func() logicpb.MessageIntServiceClient 56 | RoomIntClientBuilder func() logicpb.RoomIntServiceClient 57 | UserIntClientBuilder func() userpb.UserIntServiceClient 58 | } 59 | 60 | func init() { 61 | builder, ok := builders[ENV] 62 | if !ok { 63 | builder = new(localBuilder) 64 | } 65 | Config = builder.Build() 66 | } 67 | 68 | func interceptor(ctx context.Context, method string, request, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { 69 | err := invoker(ctx, method, request, reply, cc, opts...) 70 | 71 | md, _ := metadata.FromOutgoingContext(ctx) 72 | slog.Debug("client interceptor", "method", method, "metadata", md, "request", request, "reply", reply, "error", err) 73 | return err 74 | } 75 | 76 | func newGrpcClient(target, loadBalance string) *grpc.ClientConn { 77 | conn, err := grpc.NewClient(target, 78 | grpc.WithTransportCredentials(insecure.NewCredentials()), 79 | grpc.WithChainUnaryInterceptor(interceptor), 80 | grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"LoadBalancingPolicy": "%s"}`, loadBalance))) 81 | if err != nil { 82 | panic(err) 83 | } 84 | return conn 85 | } 86 | -------------------------------------------------------------------------------- /config/k8s_builder.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "strconv" 9 | 10 | "google.golang.org/grpc/balancer/roundrobin" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | 13 | "gim/pkg/grpclib/picker" 14 | "gim/pkg/grpclib/resolver/k8s" 15 | "gim/pkg/k8sutil" 16 | "gim/pkg/protocol/pb/connectpb" 17 | "gim/pkg/protocol/pb/logicpb" 18 | "gim/pkg/protocol/pb/userpb" 19 | ) 20 | 21 | type k8sBuilder struct{} 22 | 23 | func (*k8sBuilder) Build() Configuration { 24 | const ( 25 | RPCListenAddr = ":8000" 26 | RPCDialAddr = "8000" 27 | ) 28 | const namespace = "gim" 29 | 30 | k8sClient, err := k8sutil.GetK8sClient() 31 | if err != nil { 32 | panic(err) 33 | } 34 | configmap, err := k8sClient.CoreV1().ConfigMaps(namespace).Get(context.TODO(), "config", metav1.GetOptions{}) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | return Configuration{ 40 | LogLevel: slog.LevelDebug, 41 | LogFile: func(server string) string { 42 | return fmt.Sprintf("/data/log/%s/log.log", server) 43 | }, 44 | 45 | MySQL: configmap.Data["mysql"], 46 | RedisHost: configmap.Data["redisIP"], 47 | RedisPassword: configmap.Data["redisPassword"], 48 | PushRoomSubscribeNum: getInt(configmap.Data, "pushRoomSubscribeNum"), 49 | PushAllSubscribeNum: getInt(configmap.Data, "pushAllSubscribeNum"), 50 | 51 | ConnectLocalAddr: os.Getenv("POD_IP") + RPCListenAddr, 52 | ConnectRPCListenAddr: RPCListenAddr, 53 | ConnectTCPListenAddr: ":8001", 54 | ConnectWSListenAddr: ":8002", 55 | 56 | LogicRPCListenAddr: RPCListenAddr, 57 | UserRPCListenAddr: RPCListenAddr, 58 | FileHTTPListenAddr: "8030", 59 | 60 | ConnectIntClientBuilder: func() connectpb.ConnectIntServiceClient { 61 | conn := newGrpcClient(k8s.GetK8STarget(namespace, "connect", RPCDialAddr), picker.AddrPickerName) 62 | return connectpb.NewConnectIntServiceClient(conn) 63 | }, 64 | DeviceIntClientBuilder: func() logicpb.DeviceIntServiceClient { 65 | conn := newGrpcClient(k8s.GetK8STarget(namespace, "logic", RPCDialAddr), roundrobin.Name) 66 | return logicpb.NewDeviceIntServiceClient(conn) 67 | }, 68 | MessageIntClientBuilder: func() logicpb.MessageIntServiceClient { 69 | conn := newGrpcClient(k8s.GetK8STarget(namespace, "logic", RPCDialAddr), roundrobin.Name) 70 | return logicpb.NewMessageIntServiceClient(conn) 71 | }, 72 | RoomIntClientBuilder: func() logicpb.RoomIntServiceClient { 73 | conn := newGrpcClient(k8s.GetK8STarget(namespace, "logic", RPCDialAddr), roundrobin.Name) 74 | return logicpb.NewRoomIntServiceClient(conn) 75 | }, 76 | UserIntClientBuilder: func() userpb.UserIntServiceClient { 77 | conn := newGrpcClient(k8s.GetK8STarget(namespace, "user", RPCDialAddr), roundrobin.Name) 78 | return userpb.NewUserIntServiceClient(conn) 79 | }, 80 | } 81 | } 82 | 83 | func getInt(m map[string]string, key string) int { 84 | value, _ := strconv.Atoi(m[key]) 85 | return value 86 | } 87 | -------------------------------------------------------------------------------- /config/local_builder.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "google.golang.org/grpc/balancer/roundrobin" 7 | 8 | "gim/pkg/grpclib/picker" 9 | _ "gim/pkg/grpclib/resolver/addrs" 10 | "gim/pkg/protocol/pb/connectpb" 11 | "gim/pkg/protocol/pb/logicpb" 12 | "gim/pkg/protocol/pb/userpb" 13 | ) 14 | 15 | type localBuilder struct{} 16 | 17 | func (*localBuilder) Build() Configuration { 18 | return Configuration{ 19 | LogLevel: slog.LevelDebug, 20 | LogFile: func(server string) string { 21 | return "" 22 | }, 23 | 24 | MySQL: "root:123456@tcp(127.0.0.1:3306)/gim?charset=utf8mb4&parseTime=true&loc=Local", 25 | RedisHost: "127.0.0.1:6379", 26 | RedisPassword: "123456", 27 | PushRoomSubscribeNum: 100, 28 | PushAllSubscribeNum: 100, 29 | 30 | ConnectLocalAddr: "127.0.0.1:8000", 31 | ConnectRPCListenAddr: ":8000", 32 | ConnectTCPListenAddr: ":8001", 33 | ConnectWSListenAddr: ":8002", 34 | 35 | LogicRPCListenAddr: ":8010", 36 | UserRPCListenAddr: ":8020", 37 | FileHTTPListenAddr: "8030", 38 | 39 | ConnectIntClientBuilder: func() connectpb.ConnectIntServiceClient { 40 | conn := newGrpcClient("addrs:///127.0.0.1:8000", picker.AddrPickerName) 41 | return connectpb.NewConnectIntServiceClient(conn) 42 | }, 43 | DeviceIntClientBuilder: func() logicpb.DeviceIntServiceClient { 44 | conn := newGrpcClient("addrs:///127.0.0.1:8010", roundrobin.Name) 45 | return logicpb.NewDeviceIntServiceClient(conn) 46 | }, 47 | MessageIntClientBuilder: func() logicpb.MessageIntServiceClient { 48 | conn := newGrpcClient("addrs:///127.0.0.1:8010", roundrobin.Name) 49 | return logicpb.NewMessageIntServiceClient(conn) 50 | }, 51 | RoomIntClientBuilder: func() logicpb.RoomIntServiceClient { 52 | conn := newGrpcClient("addrs:///127.0.0.1:8010", roundrobin.Name) 53 | return logicpb.NewRoomIntServiceClient(conn) 54 | }, 55 | UserIntClientBuilder: func() userpb.UserIntServiceClient { 56 | conn := newGrpcClient("addrs:///127.0.0.1:8020", roundrobin.Name) 57 | return userpb.NewUserIntServiceClient(conn) 58 | }, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /deploy/compose/compose.yaml: -------------------------------------------------------------------------------- 1 | # 本地开发环境 2 | name: gim 3 | 4 | services: 5 | mysql: 6 | container_name: mysql 7 | restart: always 8 | image: mysql:8.4.3 9 | environment: 10 | MYSQL_ROOT_PASSWORD: 123456 11 | ENV: compose 12 | volumes: 13 | - /etc/localtime:/etc/localtime 14 | - /Users/alber/data/mysql:/var/lib/mysql 15 | - ./my.cnf:/etc/mysql/conf.d/my.cnf 16 | ports: 17 | - "3306:3306" 18 | 19 | redis: 20 | container_name: redis 21 | restart: always 22 | image: redis:7.4.2 23 | volumes: 24 | - /etc/localtime:/etc/localtime 25 | - ./redis.conf:/etc/redis.conf 26 | - /Users/alber/data/redis:/data 27 | command: 28 | redis-server /etc/redis.conf 29 | ports: 30 | - "6379:6379" 31 | environment: 32 | ENV: compose 33 | 34 | connect: 35 | container_name: connect 36 | restart: always 37 | image: connect:20250430.115000 38 | volumes: 39 | - /etc/localtime:/etc/localtime 40 | - /Users/alber/data/log/connect:/data/log/connect 41 | ports: 42 | - "8000:8000" 43 | - "8001:8001" 44 | - "8002:8002" 45 | environment: 46 | ENV: compose 47 | 48 | depends_on: 49 | mysql: 50 | condition: service_started 51 | redis: 52 | condition: service_started 53 | 54 | logic: 55 | container_name: logic 56 | restart: always 57 | image: logic:20250430.115021 58 | volumes: 59 | - /etc/localtime:/etc/localtime 60 | - /Users/alber/data/log/logic:/data/log/logic 61 | ports: 62 | - "8010:8010" 63 | environment: 64 | ENV: compose 65 | depends_on: 66 | mysql: 67 | condition: service_started 68 | redis: 69 | condition: service_started 70 | 71 | user: 72 | container_name: user 73 | restart: always 74 | image: user:20250430.115013 75 | volumes: 76 | - /etc/localtime:/etc/localtime 77 | - /Users/alber/data/log/user:/data/log/user 78 | ports: 79 | - "8020:8020" 80 | environment: 81 | ENV: compose 82 | depends_on: 83 | mysql: 84 | condition: service_started 85 | redis: 86 | condition: service_started 87 | 88 | file: 89 | container_name: file 90 | restart: always 91 | image: file:20250430.113636 92 | volumes: 93 | - /etc/localtime:/etc/localtime 94 | - /Users/alber/data/file:/data/file 95 | - /Users/alber/data/log/file:/data/log/file 96 | ports: 97 | - "8030:8030" 98 | environment: 99 | ENV: compose 100 | depends_on: 101 | mysql: 102 | condition: service_started 103 | redis: 104 | condition: service_started -------------------------------------------------------------------------------- /deploy/compose/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | sql_mode='' 3 | 4 | -------------------------------------------------------------------------------- /deploy/compose/redis.conf: -------------------------------------------------------------------------------- 1 | # 开启保护 2 | protected-mode no 3 | 4 | # 绑定监听IP地址 5 | bind 0.0.0.0 6 | 7 | # 自定义密码 8 | requirepass 123456 9 | 10 | # 启动端口 11 | port 6379 12 | 13 | # redis 默认就开启 rdb 全量备份,以下是默认的备份触发机制 14 | # 900s内至少一次写操作则执行bgsave进行RDB持久化 15 | save 900 1 16 | save 300 10 17 | save 60 10000 18 | 19 | # 是否压缩 rdb 备份文件,默认是压缩 20 | # 如果 redis 承载的数据量非常大的话,建议不要压缩 21 | # 因为压缩过程中需要耗费大量 cpu 和内存资源,磁盘相对而言比较廉价 22 | rdbcompression yes 23 | 24 | # rdb 备份的文件名 25 | dbfilename dump.rdb 26 | 27 | # Redis 备份文件存储目录,注意:该路径是 docker 容器内的路径 28 | dir /data 29 | 30 | # 是否开启 aof 增量备份功能,默认是否 31 | appendonly yes 32 | # AOF文件的名称,这里使用默认值 33 | appendfilename appendonly.aof 34 | # aof 增量备份的策略,这里是每秒钟一次,将累积的写命令持久化到硬盘中 35 | appendfsync everysec -------------------------------------------------------------------------------- /deploy/k8s/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deploy/k8s/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: gim 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 1.0.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /deploy/k8s/templates/configmap/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config 5 | namespace: {{ $.Values.namespace }} 6 | data: 7 | # 类属性键;每一个键都映射到一个简单的值,仅仅支持键值对,不支持嵌套 8 | mysql: "root:gim123456@tcp(111.229.238.28:3306)/gim?charset=utf8&parseTime=true" 9 | redisIP: "111.229.238.28:6379" 10 | redisPassword: "alber123456" 11 | pushRoomSubscribeNum: "100" 12 | pushAllSubscribeNum: "100" 13 | -------------------------------------------------------------------------------- /deploy/k8s/templates/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: {{ .Values.namespace }} -------------------------------------------------------------------------------- /deploy/k8s/templates/role/cluster_role.yaml: -------------------------------------------------------------------------------- 1 | # 为pod中的服务赋予发现服务和读取配置的权限 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: pod-role 6 | namespace: {{ $.Values.namespace }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | - pods/status 13 | - services 14 | - services/status 15 | - endpoints 16 | - endpoints/status 17 | - configmaps 18 | - configmaps/status 19 | verbs: 20 | - get 21 | - list 22 | - watch 23 | - apiGroups: 24 | - "discovery.k8s.io" 25 | resources: 26 | - endpointslices 27 | - endpointslices/status 28 | verbs: 29 | - get 30 | - list 31 | - watch 32 | --- 33 | apiVersion: rbac.authorization.k8s.io/v1 34 | kind: ClusterRoleBinding 35 | metadata: 36 | name: argo-namespaces-binding 37 | namespace: {{ $.Values.namespace }} 38 | roleRef: 39 | apiGroup: rbac.authorization.k8s.io 40 | kind: ClusterRole 41 | name: pod-role 42 | subjects: 43 | - kind: ServiceAccount 44 | name: default 45 | namespace: {{ .Release.Namespace }} 46 | -------------------------------------------------------------------------------- /deploy/k8s/templates/server/connect.yaml: -------------------------------------------------------------------------------- 1 | # deployment 配置 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: connect-deployment 6 | namespace: {{ $.Values.namespace }} 7 | labels: 8 | app: {{ .Values.server.connect.name }} 9 | spec: 10 | replicas: {{ .Values.server.connect.replicas }} 11 | selector: 12 | matchLabels: 13 | app: {{ .Values.server.connect.name }} 14 | template: 15 | metadata: 16 | labels: 17 | app: {{ .Values.server.connect.name }} 18 | spec: 19 | containers: 20 | - name: {{ .Values.server.connect.name }} 21 | image: {{ .Values.server.connect.image }} 22 | imagePullPolicy: IfNotPresent # 在kind中需要指定,不然会强制到远程拉取镜像,导致部署失败 23 | ports: 24 | - containerPort: 8000 25 | - containerPort: 8001 26 | - containerPort: 8002 27 | volumeMounts: # 映射文件为宿主机文件 28 | - mountPath: /log/ 29 | name: log 30 | env: 31 | - name: POD_IP 32 | valueFrom: 33 | fieldRef: 34 | fieldPath: status.podIP 35 | - name: ENV 36 | value: {{ $.Values.env }} 37 | volumes: 38 | - name: log 39 | hostPath: 40 | path: /log/ 41 | --- 42 | # service 配置 43 | apiVersion: v1 44 | kind: Service 45 | metadata: 46 | name: {{ .Values.server.connect.name }} 47 | namespace: {{ $.Values.namespace }} 48 | labels: 49 | app: {{ .Values.server.connect.name }} # 只有设置label,才能被服务发现找到 50 | spec: 51 | selector: 52 | app: {{ .Values.server.connect.name }} 53 | ports: 54 | - name: rpc 55 | protocol: TCP 56 | port: 8000 57 | targetPort: 8000 58 | - name: tcp 59 | protocol: TCP 60 | port: 8001 61 | targetPort: 8001 62 | - name: websocket 63 | protocol: TCP 64 | port: 8002 65 | targetPort: 8002 66 | -------------------------------------------------------------------------------- /deploy/k8s/templates/server/logic.yaml: -------------------------------------------------------------------------------- 1 | # deployment 配置 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: logic-deployment 6 | namespace: {{ $.Values.namespace }} 7 | labels: 8 | app: {{ .Values.server.logic.name }} 9 | spec: 10 | replicas: {{ .Values.server.logic.replicas }} 11 | selector: 12 | matchLabels: 13 | app: {{ .Values.server.logic.name }} 14 | template: 15 | metadata: 16 | labels: 17 | app: {{ .Values.server.logic.name }} 18 | spec: 19 | containers: 20 | - name: {{ .Values.server.logic.name }} 21 | image: {{ .Values.server.logic.image }} 22 | imagePullPolicy: IfNotPresent # 在kind中需要指定,不然会强制到远程拉取镜像,导致部署失败 23 | ports: 24 | - containerPort: 8000 25 | volumeMounts: # 映射文件为宿主机文件 26 | - mountPath: /log/ 27 | name: log 28 | env: 29 | - name: POD_IP 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: status.podIP 33 | - name: ENV 34 | value: {{ $.Values.env }} 35 | volumes: 36 | - name: log 37 | hostPath: 38 | path: /log/ 39 | --- 40 | # service 配置 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: {{ .Values.server.logic.name }} 45 | namespace: {{ $.Values.namespace }} 46 | labels: 47 | app: {{ .Values.server.logic.name }} # 只有设置label,才能被服务发现找到 48 | spec: 49 | selector: 50 | app: {{ .Values.server.logic.name }} 51 | ports: 52 | - name: rpc 53 | protocol: TCP 54 | port: 8000 55 | targetPort: 8000 56 | -------------------------------------------------------------------------------- /deploy/k8s/templates/server/user.yaml: -------------------------------------------------------------------------------- 1 | # deployment 配置 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: user-deployment 6 | namespace: {{ $.Values.namespace }} 7 | labels: 8 | app: {{ .Values.server.user.name }} 9 | spec: 10 | replicas: {{ .Values.server.logic.replicas }} 11 | selector: 12 | matchLabels: 13 | app: {{ .Values.server.user.name }} 14 | template: 15 | metadata: 16 | labels: 17 | app: {{ .Values.server.user.name }} 18 | spec: 19 | containers: 20 | - name: {{ .Values.server.user.name }} 21 | image: {{ .Values.server.user.image }} 22 | imagePullPolicy: IfNotPresent # 在kind中需要指定,不然会强制到远程拉取镜像,导致部署失败 23 | ports: 24 | - containerPort: 8000 25 | volumeMounts: # 映射文件为宿主机文件 26 | - mountPath: /log/ 27 | name: log 28 | env: 29 | - name: POD_IP 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: status.podIP 33 | - name: ENV 34 | value: {{ $.Values.env }} 35 | volumes: 36 | - name: log 37 | hostPath: 38 | path: /log/ 39 | --- 40 | # service 配置 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: {{ .Values.server.user.name }} 45 | namespace: {{ $.Values.namespace }} 46 | labels: 47 | app: {{ .Values.server.user.name }} # 只有设置label,才能被服务发现找到 48 | spec: 49 | selector: 50 | app: {{ .Values.server.user.name }} 51 | ports: 52 | - name: rpc 53 | protocol: TCP 54 | port: 8000 55 | targetPort: 8000 56 | -------------------------------------------------------------------------------- /deploy/k8s/values.yaml: -------------------------------------------------------------------------------- 1 | namespace: gim 2 | env: k8s 3 | server: 4 | connect: 5 | name: connect 6 | image: connect 7 | replicas: 1 8 | logic: 9 | name: logic 10 | image: logic 11 | replicas: 1 12 | user: 13 | name: user 14 | image: user 15 | replicas: 1 16 | -------------------------------------------------------------------------------- /docs/k8s.md: -------------------------------------------------------------------------------- 1 | ### 编译服务 2 | ./build_docker.sh connect 3 | ./build_docker.sh logic 4 | ./build_docker.sh business 5 | ### 部署: 6 | helm install gim ./chart 7 | ### 升级 8 | 增量升级: 9 | helm upgrade gim ./chart --reuse-values --set server.$1.image=$image_name 10 | 全量升级: 11 | helm upgrade gim ./chart 12 | 13 | ### 流量转发 14 | 转发到pod 15 | kubectl port-forward b-deployment-5c845465f9-4w4xv 30080:80 前面是宿主机端口,后面是容器端口 16 | 转发到service 17 | kubectl port-forward service/b 30080:80 -------------------------------------------------------------------------------- /docs/plan.md: -------------------------------------------------------------------------------- 1 | pb编译命令 2 | protoc --go_out=plugins=grpc:../../../ *.proto -------------------------------------------------------------------------------- /docs/流程图/心跳.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | participant client 3 | participant connect 4 | participant logic 5 | 6 | client -> connect: 心跳包请求 7 | connect --> client: 心跳包响应 8 | 9 | client -> client: 等待一段时间 10 | 11 | client -> connect: 心跳包请求 12 | connect --> client: 心跳包响应 13 | 14 | 15 | connect -> connect: 连续n次没有收到,释放连接 16 | connect -> logic: 通知设备下线 17 | @enduml 18 | -------------------------------------------------------------------------------- /docs/流程图/消息单发.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | participant c1.d1 3 | participant c1.d2 4 | participant c2.d3 5 | participant c2.d4 6 | participant connect 7 | participant logic 8 | 9 | c1.d1 -> logic: c1给c2用户发送消息 10 | logic --> c1.d1 : 返回消息发送成功 11 | 12 | logic -> logic: 获取c1用户下一个消息序列号 13 | logic -> logic: 将消息持久化到c1用户的消息列表 14 | logic -> logic: 查询c1用户其他在线设备 15 | logic --> connect: 给设备d2发送消息 16 | connect --> c1.d2: 给设备d2发送消息 17 | c1.d2 ->connect : 消息ack 18 | connect -> logic: 消息ack 19 | 20 | logic -> logic: 获取c2用户下一个消息序列号 21 | logic -> logic: 将消息持久化到c2用户的消息列表 22 | logic -> logic: 查询c2用户所有在线设备 23 | logic -> connect: 给设备d3发送消息 24 | connect -> c2.d3: 给设备d3发送消息 25 | c2.d3 ->connect : 消息ack 26 | connect -> logic: 消息ack 27 | logic -> connect: 给设备d4发送消息 28 | connect -> c2.d4: 给设备d4发送消息 29 | c2.d4 ->connect : 消息ack 30 | connect -> logic: 消息ack 31 | @enduml 32 | -------------------------------------------------------------------------------- /docs/流程图/消息群发.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | participant c1 3 | participant c2 4 | participant c3 5 | 6 | participant connect 7 | participant logic 8 | 9 | c1 -> logic: 发送消息到群组 10 | logic --> c1: 消息发送成功 11 | 12 | logic -> logic: 查询群组所有成员 13 | 14 | logic -> logic: 将消息持久化到c1的消息列表 15 | logic -> connect: 发送消息给c1的其他在线设备 16 | connect -> c1: 发送消息给c1的其他在线设备 17 | c1 -> connect: 消息ack 18 | connect -> logic: 消息ack 19 | 20 | logic -> logic: 将消息持久化到c2的消息列表 21 | logic -> connect: 发送消息给c2的其他在线设备 22 | connect -> c2: 发送消息给c2的其他在线设备 23 | c2 -> connect: 消息ack 24 | connect -> logic: 消息ack 25 | 26 | logic -> logic: 将消息持久化到c3的消息列表 27 | logic -> connect: 发送消息给c3的其他在线设备 28 | connect -> c3: 发送消息给c3的其他在线设备 29 | c3 -> connect: 消息ack 30 | connect -> logic: 消息ack 31 | @enduml 32 | -------------------------------------------------------------------------------- /docs/流程图/登录.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | participant client 3 | participant connect 4 | participant logic 5 | 6 | client -> connect: 设备鉴权 7 | connect -> logic: 设备鉴权 8 | 9 | logic -> logic: 检查token是否合法 10 | 11 | logic --> connect: 返回校验结果 12 | connect --> client: 返回校验结果 13 | 14 | connect -> connect: 如果鉴权失败,断开连接 15 | @enduml 16 | -------------------------------------------------------------------------------- /docs/流程图/离线消息同步.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | participant client 3 | participant connect 4 | participant logic 5 | 6 | client -> connect: 离线消息同步 7 | connect -> logic: 离线消息同步 8 | 9 | logic -> logic: 如果seq!=0,同步序列号大于seq的消息,否则,同步用户未收到的消息 10 | 11 | logic --> connect: 返回离线消息 12 | connect --> client: 返回离线消息 13 | 14 | client -> connect: 消息ack 15 | connect -> logic: 消息ack 16 | @enduml 17 | -------------------------------------------------------------------------------- /docs/错误处理.md: -------------------------------------------------------------------------------- 1 | ### 错误处理,链路追踪,日志打印 2 | 系统中的错误一般可以归类为两种,一种是业务定义的错误,一种就是未知的错误,在业务正式上线的时候,业务定义的错误的属于正常业务逻辑,不需要打印出来, 3 | 但是未知的错误,我们就需要打印出来,我们不仅要知道是什么错误,还要知道错误的调用堆栈,所以这里我对GRPC的错误进行了一些封装,使之包含调用堆栈。 4 | ```go 5 | func WrapError(err error) error { 6 | if err == nil { 7 | return nil 8 | } 9 | 10 | s := &spb.Status{ 11 | Code: int32(codes.Unknown), 12 | Message: err.Error(), 13 | Details: []*any.Any{ 14 | { 15 | TypeUrl: TypeUrlStack, 16 | Value: util.Str2bytes(stack()), 17 | }, 18 | }, 19 | } 20 | return status.FromProto(s).Err() 21 | } 22 | // Stack 获取堆栈信息 23 | func stack() string { 24 | var pc = make([]uintptr, 20) 25 | n := runtime.Callers(3, pc) 26 | 27 | var build strings.Builder 28 | for i := 0; i < n; i++ { 29 | f := runtime.FuncForPC(pc[i] - 1) 30 | file, line := f.FileLine(pc[i] - 1) 31 | n := strings.Index(file, name) 32 | if n != -1 { 33 | s := fmt.Sprintf(" %s:%d \n", file[n:], line) 34 | build.WriteString(s) 35 | } 36 | } 37 | return build.String() 38 | } 39 | ``` 40 | 这样,不仅可以拿到错误的堆栈,错误的堆栈也可以跨RPC传输,但是,但是这样你只能拿到当前服务的堆栈,却不能拿到调用方的堆栈,就比如说,A服务调用 41 | B服务,当B服务发生错误时,在A服务通过日志打印错误的时候,我们只打印了B服务的调用堆栈,怎样可以把A服务的堆栈打印出来。我们在A服务调用的地方也获取 42 | 一次堆栈。 43 | ```go 44 | func WrapRPCError(err error) error { 45 | if err == nil { 46 | return nil 47 | } 48 | e, _ := status.FromError(err) 49 | s := &spb.Status{ 50 | Code: int32(e.Code()), 51 | Message: e.Message(), 52 | Details: []*any.Any{ 53 | { 54 | TypeUrl: TypeUrlStack, 55 | Value: util.Str2bytes(GetErrorStack(e) + " --grpc-- \n" + stack()), 56 | }, 57 | }, 58 | } 59 | return status.FromProto(s).Err() 60 | } 61 | 62 | func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { 63 | err := invoker(ctx, method, req, reply, cc, opts...) 64 | return gerrors.WrapRPCError(err) 65 | } 66 | 67 | var LogicIntClient pb.LogicIntClient 68 | 69 | func InitLogicIntClient(addr string) { 70 | conn, err := grpc.DialContext(context.TODO(), addr, grpc.WithInsecure(), grpc.WithUnaryInterceptor(interceptor)) 71 | if err != nil { 72 | logger.Sugar.Error(err) 73 | panic(err) 74 | } 75 | 76 | LogicIntClient = pb.NewLogicIntClient(conn) 77 | } 78 | ``` 79 | 像这样,就可以获取完整一次调用堆栈。 80 | 错误打印也没有必要在函数返回错误的时候,每次都去打印。因为错误已经包含了堆栈信息 81 | ```go 82 | // 错误的方式 83 | if err != nil { 84 | logger.Sugar.Error(err) 85 | return err 86 | } 87 | 88 | // 正确的方式 89 | if err != nil { 90 | return err 91 | } 92 | ``` 93 | 然后,我们在上层统一打印就可以 94 | ```go 95 | func startServer { 96 | extListen, err := net.Listen("tcp", conf.LogicConf.ClientRPCExtListenAddr) 97 | if err != nil { 98 | panic(err) 99 | } 100 | extServer := grpc.NewServer(grpc.UnaryInterceptor(LogicClientExtInterceptor)) 101 | pb.RegisterLogicClientExtServer(extServer, &LogicClientExtServer{}) 102 | err = extServer.Serve(extListen) 103 | if err != nil { 104 | panic(err) 105 | } 106 | } 107 | 108 | func LogicClientExtInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 109 | defer logPanic("logic_client_ext_interceptor", ctx, req, info, &err) 110 | 111 | 112 | resp, err = handler(ctx, req) 113 | logger.Logger.Debug("logic_client_ext_interceptor", zap.Any("info", info), zap.Any("ctx", ctx), zap.Any("req", req), 114 | zap.Any("resp", resp), zap.Error(err)) 115 | 116 | s, _ := status.FromError(err) 117 | if s.Code() != 0 && s.Code() < 1000 { 118 | md, _ := metadata.FromIncomingContext(ctx) 119 | logger.Logger.Error("logic_client_ext_interceptor", zap.String("method", info.FullMethod), zap.Any("md", md), zap.Any("req", req), 120 | zap.Any("resp", resp), zap.Error(err), zap.String("stack", gerrors.GetErrorStack(s))) 121 | } 122 | return 123 | } 124 | ``` 125 | 这样做的前提就是,在业务代码中透传context,golang不像其他语言,可以在线程本地保存变量,像Java的ThreadLocal,所以只能通过函数参数的形式进行传递,im中,service层函数的第一个参数 126 | 都是context,但是dao层和cache层就不需要了,不然,显得代码臃肿。 127 | 最后可以在客户端的每次请求添加一个随机的request_id,这样客户端到服务的每次请求都可以串起来了。 128 | ```go 129 | func getCtx() context.Context { 130 | token, _ := util.GetToken(1, 2, 3, time.Now().Add(1*time.Hour).Unix(), util.PublicKey) 131 | return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs( 132 | "app_id", "1", 133 | "user_id", "2", 134 | "device_id", "3", 135 | "token", token, 136 | "request_id", strconv.FormatInt(time.Now().UnixNano(), 10))) 137 | } 138 | ``` 139 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gim 2 | 3 | go 1.23.8 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/go-redis/redis v6.15.9+incompatible 8 | github.com/go-sql-driver/mysql v1.9.2 9 | github.com/gorilla/websocket v1.5.3 10 | github.com/json-iterator/go v1.1.12 11 | google.golang.org/grpc v1.71.1 12 | google.golang.org/protobuf v1.36.6 13 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 14 | gorm.io/driver/mysql v1.5.7 15 | gorm.io/gorm v1.25.12 16 | k8s.io/api v0.32.3 17 | k8s.io/apimachinery v0.32.3 18 | k8s.io/client-go v0.32.3 19 | ) 20 | 21 | require ( 22 | filippo.io/edwards25519 v1.1.0 // indirect 23 | github.com/bytedance/sonic v1.11.6 // indirect 24 | github.com/bytedance/sonic/loader v0.1.1 // indirect 25 | github.com/cloudwego/base64x v0.1.4 // indirect 26 | github.com/cloudwego/iasm v0.2.0 // indirect 27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 28 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 29 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 30 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 31 | github.com/gin-contrib/sse v0.1.0 // indirect 32 | github.com/go-logr/logr v1.4.2 // indirect 33 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 34 | github.com/go-openapi/jsonreference v0.20.2 // indirect 35 | github.com/go-openapi/swag v0.23.0 // indirect 36 | github.com/go-playground/locales v0.14.1 // indirect 37 | github.com/go-playground/universal-translator v0.18.1 // indirect 38 | github.com/go-playground/validator/v10 v10.20.0 // indirect 39 | github.com/goccy/go-json v0.10.2 // indirect 40 | github.com/gogo/protobuf v1.3.2 // indirect 41 | github.com/golang/protobuf v1.5.4 // indirect 42 | github.com/google/gnostic-models v0.6.8 // indirect 43 | github.com/google/go-cmp v0.6.0 // indirect 44 | github.com/google/gofuzz v1.2.0 // indirect 45 | github.com/google/uuid v1.6.0 // indirect 46 | github.com/jinzhu/inflection v1.0.0 // indirect 47 | github.com/jinzhu/now v1.1.5 // indirect 48 | github.com/josharian/intern v1.0.0 // indirect 49 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 50 | github.com/leodido/go-urn v1.4.0 // indirect 51 | github.com/mailru/easyjson v0.7.7 // indirect 52 | github.com/mattn/go-isatty v0.0.20 // indirect 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 54 | github.com/modern-go/reflect2 v1.0.2 // indirect 55 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 56 | github.com/onsi/ginkgo v1.16.5 // indirect 57 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 58 | github.com/pkg/errors v0.9.1 // indirect 59 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 60 | github.com/ugorji/go/codec v1.2.12 // indirect 61 | github.com/x448/float16 v0.8.4 // indirect 62 | golang.org/x/arch v0.8.0 // indirect 63 | golang.org/x/crypto v0.32.0 // indirect 64 | golang.org/x/net v0.34.0 // indirect 65 | golang.org/x/oauth2 v0.25.0 // indirect 66 | golang.org/x/sys v0.29.0 // indirect 67 | golang.org/x/term v0.28.0 // indirect 68 | golang.org/x/text v0.24.0 // indirect 69 | golang.org/x/time v0.7.0 // indirect 70 | google.golang.org/genproto v0.0.0-20211207154714-918901c715cf // indirect 71 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 72 | gopkg.in/inf.v0 v0.9.1 // indirect 73 | gopkg.in/yaml.v3 v3.0.1 // indirect 74 | k8s.io/klog/v2 v2.130.1 // indirect 75 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 76 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 77 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 78 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 79 | sigs.k8s.io/yaml v1.4.0 // indirect 80 | ) 81 | -------------------------------------------------------------------------------- /internal/connect/api.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "google.golang.org/protobuf/types/known/emptypb" 8 | 9 | "gim/pkg/gerrors" 10 | "gim/pkg/md" 11 | pb "gim/pkg/protocol/pb/connectpb" 12 | ) 13 | 14 | type ConnIntService struct { 15 | pb.UnsafeConnectIntServiceServer 16 | } 17 | 18 | // PushToDevices 投递消息 19 | func (s *ConnIntService) PushToDevices(ctx context.Context, request *pb.PushToDevicesRequest) (*emptypb.Empty, error) { 20 | reply := &emptypb.Empty{} 21 | 22 | for _, dm := range request.DeviceMessageList { 23 | conn := GetConn(dm.DeviceId) 24 | if conn == nil { 25 | slog.Warn("PushToDevices warn conn not found", "device_id", dm.DeviceId) 26 | return reply, gerrors.ErrConnNotFound 27 | } 28 | 29 | if conn.DeviceId != dm.DeviceId { 30 | slog.Warn("PushToDevices warn deviceID not equal", "device_id", dm.DeviceId) 31 | return reply, gerrors.ErrConnDeviceIdNotEqual 32 | } 33 | 34 | packet := &pb.Packet{ 35 | Command: pb.Command_MESSAGE, 36 | RequestId: md.GetCtxRequestId(ctx), 37 | } 38 | conn.Send(packet, dm.Message, nil) 39 | } 40 | return reply, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/connect/client_test.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "net/http" 10 | "testing" 11 | "time" 12 | 13 | "github.com/gorilla/websocket" 14 | jsoniter "github.com/json-iterator/go" 15 | "google.golang.org/protobuf/proto" 16 | 17 | "gim/pkg/codec" 18 | pb "gim/pkg/protocol/pb/connectpb" 19 | "gim/pkg/protocol/pb/logicpb" 20 | "gim/pkg/util" 21 | ) 22 | 23 | func TestTCPClient(t *testing.T) { 24 | runClient("tcp", "127.0.0.1:8001", 1, 1, 1) 25 | 26 | } 27 | 28 | func TestWSClient(t *testing.T) { 29 | runClient("ws", "ws://127.0.0.1:8002/ws", 1, 1, 1) 30 | 31 | } 32 | 33 | func TestGroupTCPClient(t *testing.T) { 34 | log.SetFlags(log.Lshortfile) 35 | 36 | go runClient("tcp", "127.0.0.1:8001", 1, 1, 1) 37 | go runClient("tcp", "127.0.0.1:8001", 2, 2, 1) 38 | select {} 39 | } 40 | 41 | type conn interface { 42 | write(buf []byte) error 43 | receive(handler func([]byte)) 44 | } 45 | 46 | type tcpConn struct { 47 | conn net.Conn 48 | reader *bufio.Reader 49 | } 50 | 51 | func newTCPConn(url string) (*tcpConn, error) { 52 | // demo "127.0.0.1:8001" 53 | conn, err := net.Dial("tcp", url) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return &tcpConn{ 59 | conn: conn, 60 | reader: bufio.NewReader(conn), 61 | }, nil 62 | } 63 | 64 | func (c *tcpConn) write(buf []byte) error { 65 | _, err := c.conn.Write(codec.Encode(buf)) 66 | return err 67 | } 68 | 69 | func (c *tcpConn) receive(handler func([]byte)) { 70 | for { 71 | buf, err := codec.Decode(c.reader) 72 | if err != nil { 73 | log.Println(err) 74 | return 75 | } 76 | 77 | handler(buf) 78 | } 79 | } 80 | 81 | type wsConn struct { 82 | conn *websocket.Conn 83 | } 84 | 85 | func newWsConn(url string) (*wsConn, error) { 86 | // demo "ws://127.0.0.1:8002/ws" 87 | conn, resp, err := websocket.DefaultDialer.Dial(url, http.Header{}) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | bytes, err := io.ReadAll(resp.Body) 93 | if err != nil { 94 | fmt.Println(err) 95 | return nil, err 96 | } 97 | fmt.Println(string(bytes)) 98 | return &wsConn{conn: conn}, nil 99 | } 100 | 101 | func (c *wsConn) write(buf []byte) error { 102 | return c.conn.WriteMessage(websocket.BinaryMessage, buf) 103 | } 104 | 105 | func (c *wsConn) receive(handler func([]byte)) { 106 | for { 107 | _, bytes, err := c.conn.ReadMessage() 108 | if err != nil { 109 | fmt.Println(err) 110 | return 111 | } 112 | 113 | handler(bytes) 114 | } 115 | } 116 | 117 | func jsonString(any any) string { 118 | bytes, _ := jsoniter.Marshal(any) 119 | return string(bytes) 120 | } 121 | 122 | type client struct { 123 | UserID uint64 124 | DeviceID uint64 125 | Seq uint64 126 | conn conn 127 | } 128 | 129 | func runClient(network string, url string, userID, deviceID, seq uint64) { 130 | var conn conn 131 | var err error 132 | switch network { 133 | case "tcp": 134 | conn, err = newTCPConn(url) 135 | case "ws": 136 | conn, err = newWsConn(url) 137 | default: 138 | panic("unsupported network") 139 | } 140 | if err != nil { 141 | panic(err) 142 | } 143 | 144 | client := &client{ 145 | UserID: userID, 146 | DeviceID: deviceID, 147 | Seq: seq, 148 | conn: conn, 149 | } 150 | client.run() 151 | } 152 | 153 | func (c *client) run() { 154 | go c.conn.receive(c.handlePackage) 155 | c.signIn() 156 | c.syncTrigger() 157 | c.subscribeRoom() 158 | c.heartbeat() 159 | } 160 | 161 | func (c *client) info() string { 162 | return fmt.Sprintf("%-5d%-5d", c.UserID, c.DeviceID) 163 | } 164 | 165 | func (c *client) send(pt pb.Command, requestId int64, message proto.Message) { 166 | var packet = pb.Packet{ 167 | Command: pt, 168 | RequestId: requestId, 169 | } 170 | 171 | if message != nil { 172 | bytes, err := proto.Marshal(message) 173 | if err != nil { 174 | log.Println(c.info(), err) 175 | return 176 | } 177 | packet.Data = bytes 178 | } 179 | 180 | buf, err := proto.Marshal(&packet) 181 | if err != nil { 182 | log.Println(c.info(), err) 183 | return 184 | } 185 | 186 | err = c.conn.write(buf) 187 | if err != nil { 188 | log.Println(c.info(), err) 189 | } 190 | } 191 | 192 | func (c *client) signIn() { 193 | signIn := pb.SignInInput{ 194 | UserId: c.UserID, 195 | DeviceId: c.DeviceID, 196 | Token: "0", 197 | } 198 | c.send(pb.Command_SIGN_IN, time.Now().UnixNano(), &signIn) 199 | log.Println(c.info(), "发送登录指令") 200 | time.Sleep(1 * time.Second) 201 | } 202 | 203 | func (c *client) syncTrigger() { 204 | c.send(pb.Command_SYNC, time.Now().UnixNano(), &pb.SyncInput{Seq: c.Seq}) 205 | log.Println(c.info(), "开始同步") 206 | } 207 | 208 | func (c *client) heartbeat() { 209 | ticker := time.NewTicker(time.Minute * 5) 210 | for range ticker.C { 211 | c.send(pb.Command_HEARTBEAT, time.Now().UnixNano(), nil) 212 | fmt.Println(c.info(), "心跳发送") 213 | } 214 | } 215 | 216 | func (c *client) subscribeRoom() { 217 | var roomID uint64 = 1 218 | c.send(pb.Command_SUBSCRIBE_ROOM, 0, &pb.SubscribeRoomInput{ 219 | RoomId: roomID, 220 | Seq: 0, 221 | }) 222 | log.Println(c.info(), "订阅房间:", roomID) 223 | } 224 | 225 | func (c *client) handlePackage(bytes []byte) { 226 | var packet pb.Packet 227 | err := proto.Unmarshal(bytes, &packet) 228 | if err != nil { 229 | log.Println(err) 230 | return 231 | } 232 | 233 | switch packet.Command { 234 | case pb.Command_SIGN_IN: 235 | log.Println(c.info(), "登录响应:", jsonString(&packet)) 236 | case pb.Command_HEARTBEAT: 237 | log.Println(c.info(), "心跳响应") 238 | case pb.Command_SYNC: 239 | log.Println(c.info(), "离线消息同步开始------") 240 | syncResp := pb.SyncOutput{} 241 | err := proto.Unmarshal(packet.Data, &syncResp) 242 | if err != nil { 243 | log.Println(err) 244 | return 245 | } 246 | log.Println(c.info(), "离线消息同步响应:code", packet.Code, "message:", packet.Message) 247 | for _, msg := range syncResp.Messages { 248 | log.Println(c.info(), util.MessageToString(msg)) 249 | c.Seq = msg.Seq 250 | } 251 | 252 | ack := pb.MessageACK{ 253 | DeviceAck: c.Seq, 254 | ReceiveTime: util.UnixMilliTime(time.Now()), 255 | } 256 | c.send(pb.Command_MESSAGE, packet.RequestId, &ack) 257 | log.Println(c.info(), "离线消息同步结束------") 258 | case pb.Command_MESSAGE: 259 | msg := logicpb.Message{} 260 | err := proto.Unmarshal(packet.Data, &msg) 261 | if err != nil { 262 | log.Println(err) 263 | return 264 | } 265 | 266 | log.Println(c.info(), util.MessageToString(&msg)) 267 | c.Seq = msg.Seq 268 | ack := pb.MessageACK{ 269 | DeviceAck: msg.Seq, 270 | ReceiveTime: util.UnixMilliTime(time.Now()), 271 | } 272 | c.send(pb.Command_MESSAGE, packet.RequestId, &ack) 273 | case pb.Command_SUBSCRIBE_ROOM: 274 | log.Println(c.info(), "订阅房间响应", packet.Code, packet.Message) 275 | default: 276 | log.Println(c.info(), "switch other", &packet, len(bytes)) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /internal/connect/conn_manager.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "sync" 5 | 6 | pb "gim/pkg/protocol/pb/connectpb" 7 | "gim/pkg/protocol/pb/logicpb" 8 | ) 9 | 10 | var ConnesManager = sync.Map{} 11 | 12 | // SetConn 存储 13 | func SetConn(deviceId uint64, conn *Conn) { 14 | ConnesManager.Store(deviceId, conn) 15 | } 16 | 17 | // GetConn 获取 18 | func GetConn(deviceId uint64) *Conn { 19 | value, ok := ConnesManager.Load(deviceId) 20 | if ok { 21 | return value.(*Conn) 22 | } 23 | return nil 24 | } 25 | 26 | // DeleteConn 删除 27 | func DeleteConn(deviceId uint64) { 28 | ConnesManager.Delete(deviceId) 29 | } 30 | 31 | // PushAll 全服推送 32 | func PushAll(message *logicpb.Message) { 33 | ConnesManager.Range(func(key, value interface{}) bool { 34 | conn := value.(*Conn) 35 | packet := &pb.Packet{Command: pb.Command_MESSAGE} 36 | conn.Send(packet, message, nil) 37 | return true 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /internal/connect/mq.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "log/slog" 5 | "time" 6 | 7 | "github.com/go-redis/redis" 8 | "google.golang.org/protobuf/proto" 9 | 10 | "gim/config" 11 | "gim/pkg/db" 12 | "gim/pkg/mq" 13 | pb "gim/pkg/protocol/pb/connectpb" 14 | ) 15 | 16 | // StartSubscribe 启动MQ消息处理逻辑 17 | func StartSubscribe() { 18 | pushRoomPriorityChannel := db.RedisCli.Subscribe(mq.PushRoomPriorityTopic).Channel() 19 | pushRoomChannel := db.RedisCli.Subscribe(mq.PushRoomTopic).Channel() 20 | for i := 0; i < config.Config.PushRoomSubscribeNum; i++ { 21 | go handlePushRoomMsg(pushRoomPriorityChannel, pushRoomChannel) 22 | } 23 | 24 | pushAllChannel := db.RedisCli.Subscribe(mq.PushAllTopic).Channel() 25 | for i := 0; i < config.Config.PushAllSubscribeNum; i++ { 26 | go handlePushAllMsg(pushAllChannel) 27 | } 28 | } 29 | 30 | func handlePushRoomMsg(priorityChannel, channel <-chan *redis.Message) { 31 | for { 32 | select { 33 | case msg := <-priorityChannel: 34 | handlePushRoom([]byte(msg.Payload)) 35 | default: 36 | select { 37 | case msg := <-channel: 38 | handlePushRoom([]byte(msg.Payload)) 39 | default: 40 | time.Sleep(100 * time.Millisecond) 41 | continue 42 | } 43 | } 44 | } 45 | } 46 | 47 | func handlePushAllMsg(channel <-chan *redis.Message) { 48 | for msg := range channel { 49 | handlePushAll([]byte(msg.Payload)) 50 | } 51 | } 52 | 53 | func handlePushRoom(bytes []byte) { 54 | var msg pb.PushRoomMsg 55 | err := proto.Unmarshal(bytes, &msg) 56 | if err != nil { 57 | slog.Error("handlePushRoom error", "error", err) 58 | return 59 | } 60 | slog.Debug("handlePushRoom", "msg", &msg) 61 | PushRoom(msg.RoomId, msg.Message) 62 | } 63 | 64 | func handlePushAll(bytes []byte) { 65 | var msg pb.PushAllMsg 66 | err := proto.Unmarshal(bytes, &msg) 67 | if err != nil { 68 | slog.Error("handlePushRoom error", "error", err) 69 | return 70 | } 71 | slog.Debug("handlePushAll", "msg", &msg) 72 | PushAll(msg.Message) 73 | } 74 | -------------------------------------------------------------------------------- /internal/connect/room.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "container/list" 5 | "log/slog" 6 | "sync" 7 | 8 | pb "gim/pkg/protocol/pb/connectpb" 9 | "gim/pkg/protocol/pb/logicpb" 10 | ) 11 | 12 | var RoomsManager sync.Map 13 | 14 | // SubscribedRoom 订阅房间 15 | func SubscribedRoom(conn *Conn, roomID uint64) { 16 | if roomID == conn.RoomId { 17 | return 18 | } 19 | 20 | oldRoomId := conn.RoomId 21 | // 取消订阅 22 | if oldRoomId != 0 { 23 | value, ok := RoomsManager.Load(oldRoomId) 24 | if !ok { 25 | return 26 | } 27 | room := value.(*Room) 28 | room.Unsubscribe(conn) 29 | 30 | if room.Conns.Front() == nil { 31 | RoomsManager.Delete(oldRoomId) 32 | } 33 | slog.Debug("SubscribedRoom un", "userID", conn.UserId, "roomID", roomID) 34 | return 35 | } 36 | 37 | // 订阅 38 | if roomID != 0 { 39 | value, ok := RoomsManager.Load(roomID) 40 | var room *Room 41 | if !ok { 42 | room = NewRoom(roomID) 43 | RoomsManager.Store(roomID, room) 44 | } else { 45 | room = value.(*Room) 46 | } 47 | room.Subscribe(conn) 48 | slog.Debug("SubscribedRoom", "userID", conn.UserId, "roomID", roomID) 49 | return 50 | } 51 | } 52 | 53 | // PushRoom 房间消息推送 54 | func PushRoom(roomID uint64, message *logicpb.Message) { 55 | value, ok := RoomsManager.Load(roomID) 56 | if !ok { 57 | return 58 | } 59 | 60 | slog.Debug("PushRoom", "roomID", roomID, "msg", message) 61 | value.(*Room).Push(message) 62 | } 63 | 64 | type Room struct { 65 | RoomID uint64 // 房间ID 66 | Conns *list.List // 订阅房间消息的连接 67 | lock sync.RWMutex 68 | } 69 | 70 | func NewRoom(roomID uint64) *Room { 71 | return &Room{ 72 | RoomID: roomID, 73 | Conns: list.New(), 74 | } 75 | } 76 | 77 | // Subscribe 订阅房间 78 | func (r *Room) Subscribe(conn *Conn) { 79 | r.lock.Lock() 80 | defer r.lock.Unlock() 81 | 82 | conn.Element = r.Conns.PushBack(conn) 83 | conn.RoomId = r.RoomID 84 | } 85 | 86 | // Unsubscribe 取消订阅 87 | func (r *Room) Unsubscribe(conn *Conn) { 88 | r.lock.Lock() 89 | defer r.lock.Unlock() 90 | 91 | r.Conns.Remove(conn.Element) 92 | conn.Element = nil 93 | conn.RoomId = 0 94 | } 95 | 96 | // Push 推送消息到房间 97 | func (r *Room) Push(message *logicpb.Message) { 98 | r.lock.RLock() 99 | defer r.lock.RUnlock() 100 | 101 | element := r.Conns.Front() 102 | for { 103 | conn := element.Value.(*Conn) 104 | slog.Debug("PushRoom toUser", "userID", conn.UserId, "msg", message) 105 | packet := &pb.Packet{Command: pb.Command_MESSAGE} 106 | conn.Send(packet, message, nil) 107 | 108 | element = element.Next() 109 | if element == nil { 110 | break 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /internal/connect/tcp_server.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "bufio" 5 | "log/slog" 6 | "net" 7 | "time" 8 | 9 | "gim/pkg/codec" 10 | "gim/pkg/util" 11 | ) 12 | 13 | // StartTCPServer 启动TCP服务器 14 | func StartTCPServer(addr string) { 15 | tcpAddr, err := net.ResolveTCPAddr("tcp", addr) 16 | if err != nil { 17 | panic(err) 18 | } 19 | listener, err := net.ListenTCP("tcp", tcpAddr) 20 | if err != nil { 21 | panic(err) 22 | } 23 | slog.Info("tcp server running") 24 | go accept(listener) 25 | } 26 | 27 | func accept(listener *net.TCPListener) { 28 | for { 29 | conn, err := listener.AcceptTCP() 30 | if err != nil { 31 | slog.Error("acceptTCP error", "error", err) 32 | continue 33 | } 34 | 35 | err = conn.SetKeepAlive(true) 36 | if err != nil { 37 | slog.Error("setKeepAlive error", "error", err) 38 | } 39 | 40 | err = conn.SetNoDelay(true) 41 | if err != nil { 42 | slog.Error("setNoDelay error", "error", err) 43 | } 44 | 45 | go handleConn(conn) 46 | } 47 | } 48 | 49 | func handleConn(tcpConn *net.TCPConn) { 50 | defer util.RecoverPanic() 51 | 52 | conn := &Conn{ 53 | CoonType: CoonTypeTCP, 54 | TCP: tcpConn, 55 | Reader: bufio.NewReader(tcpConn), 56 | } 57 | 58 | for { 59 | err := conn.TCP.SetReadDeadline(time.Now().Add(12 * time.Minute)) 60 | if err != nil { 61 | conn.Close(err) 62 | return 63 | } 64 | 65 | buf, err := codec.Decode(conn.Reader) 66 | if err != nil { 67 | conn.Close(err) 68 | return 69 | } 70 | 71 | conn.HandleMessage(buf) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/connect/ws_server.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | var upgrader = websocket.Upgrader{ 12 | ReadBufferSize: 1024, 13 | WriteBufferSize: 65536, 14 | CheckOrigin: func(r *http.Request) bool { 15 | return true 16 | }, 17 | } 18 | 19 | // StartWSServer 启动WebSocket服务器 20 | func StartWSServer(address string) { 21 | http.HandleFunc("/ws", wsHandler) 22 | slog.Info("websocket server running") 23 | err := http.ListenAndServe(address, nil) 24 | if err != nil { 25 | panic(err) 26 | } 27 | } 28 | 29 | func wsHandler(w http.ResponseWriter, r *http.Request) { 30 | wsConn, err := upgrader.Upgrade(w, r, nil) 31 | if err != nil { 32 | slog.Error("upgrade error", "error", err) 33 | return 34 | } 35 | DoConn(wsConn) 36 | } 37 | 38 | // DoConn 处理连接 39 | func DoConn(wsConn *websocket.Conn) { 40 | conn := &Conn{ 41 | CoonType: ConnTypeWS, 42 | WS: wsConn, 43 | } 44 | 45 | for { 46 | err := conn.WS.SetReadDeadline(time.Now().Add(12 * time.Minute)) 47 | if err != nil { 48 | conn.Close(err) 49 | return 50 | } 51 | _, data, err := conn.WS.ReadMessage() 52 | if err != nil { 53 | conn.Close(err) 54 | return 55 | } 56 | 57 | conn.HandleMessage(data) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/logic/device/api.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "google.golang.org/protobuf/types/known/emptypb" 8 | 9 | pb "gim/pkg/protocol/pb/logicpb" 10 | ) 11 | 12 | type DeviceExtService struct { 13 | pb.UnsafeDeviceExtServiceServer 14 | } 15 | 16 | // RegisterDevice 注册设备 17 | func (*DeviceExtService) RegisterDevice(ctx context.Context, request *pb.RegisterDeviceRequest) (*pb.RegisterDeviceReply, error) { 18 | deviceId, err := App.Register(ctx, request) 19 | return &pb.RegisterDeviceReply{DeviceId: deviceId}, err 20 | } 21 | 22 | type DeviceIntService struct { 23 | pb.UnsafeDeviceIntServiceServer 24 | } 25 | 26 | // ConnSignIn 设备登录 27 | func (*DeviceIntService) ConnSignIn(ctx context.Context, request *pb.ConnSignInRequest) (*emptypb.Empty, error) { 28 | err := App.SignIn(ctx, request.UserId, request.DeviceId, request.Token, request.ConnAddr, request.ClientAddr) 29 | return &emptypb.Empty{}, err 30 | } 31 | 32 | // Offline 设备离线 33 | func (*DeviceIntService) Offline(ctx context.Context, request *pb.OfflineRequest) (*emptypb.Empty, error) { 34 | err := App.Offline(ctx, request.DeviceId, request.ClientAddr) 35 | return &emptypb.Empty{}, err 36 | } 37 | 38 | // GetDevice 获取设备信息 39 | func (*DeviceIntService) GetDevice(ctx context.Context, request *pb.GetDeviceRequest) (*pb.GetDeviceReply, error) { 40 | device, err := App.GetDevice(ctx, request.DeviceId) 41 | return &pb.GetDeviceReply{Device: device}, err 42 | } 43 | 44 | // ServerStop 服务停止 45 | func (s *DeviceIntService) ServerStop(ctx context.Context, request *pb.ServerStopRequest) (*emptypb.Empty, error) { 46 | go func() { 47 | err := App.ServerStop(ctx, request.ConnAddr) 48 | if err != nil { 49 | slog.Error("ServerStop error", "error", err) 50 | } 51 | }() 52 | return &emptypb.Empty{}, nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/logic/device/api_test.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/credentials/insecure" 9 | 10 | pb "gim/pkg/protocol/pb/logicpb" 11 | ) 12 | 13 | func getClient() pb.DeviceExtServiceClient { 14 | conn, err := grpc.NewClient("127.0.0.1:8010", grpc.WithTransportCredentials(insecure.NewCredentials())) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return pb.NewDeviceExtServiceClient(conn) 19 | } 20 | 21 | func TestDeviceExtService_RegisterDevice(t *testing.T) { 22 | reply, err := getClient().RegisterDevice(context.TODO(), &pb.RegisterDeviceRequest{ 23 | Type: pb.DeviceType_DT_ANDROID, 24 | Brand: "huawei", 25 | Model: "huawei15", 26 | SystemVersion: "1.0.0", 27 | SdkVersion: "1.0.0", 28 | }) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | t.Log(reply) 33 | } 34 | -------------------------------------------------------------------------------- /internal/logic/device/app.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "time" 7 | 8 | "gim/pkg/gerrors" 9 | pb "gim/pkg/protocol/pb/logicpb" 10 | "gim/pkg/protocol/pb/userpb" 11 | "gim/pkg/rpc" 12 | ) 13 | 14 | type app struct{} 15 | 16 | var App = new(app) 17 | 18 | // Register 注册设备 19 | func (*app) Register(ctx context.Context, in *pb.RegisterDeviceRequest) (uint64, error) { 20 | device := Device{ 21 | Type: in.Type, 22 | Brand: in.Brand, 23 | Model: in.Model, 24 | SystemVersion: in.SystemVersion, 25 | SDKVersion: in.SdkVersion, 26 | } 27 | 28 | // 判断设备信息是否合法 29 | if !device.IsLegal() { 30 | return 0, gerrors.ErrBadRequest 31 | } 32 | 33 | err := Repo.Save(&device) 34 | if err != nil { 35 | return 0, err 36 | } 37 | 38 | return device.ID, nil 39 | } 40 | 41 | // SignIn 登录 42 | func (*app) SignIn(ctx context.Context, userId, deviceId uint64, token string, connAddr string, clientAddr string) error { 43 | _, err := rpc.GetUserIntClient().Auth(ctx, &userpb.AuthRequest{UserId: userId, DeviceId: deviceId, Token: token}) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // 标记用户在设备上登录 49 | device, err := Repo.Get(deviceId) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | device.Online(userId, connAddr, clientAddr) 55 | 56 | return Repo.Save(device) 57 | } 58 | 59 | // Offline 设备离线 60 | func (*app) Offline(ctx context.Context, deviceId uint64, clientAddr string) error { 61 | device, err := Repo.Get(deviceId) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if device.ClientAddr != clientAddr { 67 | return nil 68 | } 69 | device.Status = OffLine 70 | 71 | return Repo.Save(device) 72 | 73 | } 74 | 75 | // ListOnlineByUserId 获取用户所有在线设备 76 | func (*app) ListOnlineByUserId(ctx context.Context, userIds []uint64) ([]*pb.Device, error) { 77 | devices, err := Repo.ListOnlineByUserId(userIds) 78 | if err != nil { 79 | return nil, err 80 | } 81 | pbDevices := make([]*pb.Device, len(devices)) 82 | for i := range devices { 83 | pbDevices[i] = devices[i].ToProto() 84 | } 85 | return pbDevices, nil 86 | } 87 | 88 | // GetDevice 获取设备信息 89 | func (*app) GetDevice(ctx context.Context, deviceId uint64) (*pb.Device, error) { 90 | device, err := Repo.Get(deviceId) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return device.ToProto(), nil 96 | } 97 | 98 | // ServerStop connect服务停止 99 | func (*app) ServerStop(ctx context.Context, connAddr string) error { 100 | devices, err := Repo.ListOnlineByConnAddr(connAddr) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | for i := range devices { 106 | // 因为是异步修改设备转台,要避免设备重连,导致状态不一致 107 | err = Repo.UpdateStatusOffline(devices[i]) 108 | if err != nil { 109 | slog.Error("DeviceRepo.Save error", "device", devices[i], "error", err) 110 | } 111 | time.Sleep(2 * time.Millisecond) 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /internal/logic/device/entity.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "time" 5 | 6 | pb "gim/pkg/protocol/pb/logicpb" 7 | ) 8 | 9 | const ( 10 | OnLine = 1 // 设备在线 11 | OffLine = 0 // 设备离线 12 | ) 13 | 14 | // Device 设备 15 | type Device struct { 16 | ID uint64 // 设备id 17 | CreatedAt time.Time // 创建时间 18 | UpdatedAt time.Time // 更新时间 19 | UserId uint64 // 用户id 20 | Type pb.DeviceType // 设备类型 21 | Brand string // 手机厂商 22 | Model string // 机型 23 | SystemVersion string // 系统版本 24 | SDKVersion string // SDK版本 25 | Status int32 // 在线状态,0:离线;1:在线 26 | ConnAddr string // 连接层服务层地址 27 | ClientAddr string // 客户端地址 28 | } 29 | 30 | func (d *Device) ToProto() *pb.Device { 31 | return &pb.Device{ 32 | DeviceId: d.ID, 33 | UserId: d.UserId, 34 | Type: d.Type, 35 | Brand: d.Brand, 36 | Model: d.Model, 37 | SystemVersion: d.SystemVersion, 38 | SdkVersion: d.SDKVersion, 39 | Status: d.Status, 40 | ConnAddr: d.ConnAddr, 41 | ClientAddr: d.ClientAddr, 42 | CreateTime: d.CreatedAt.Unix(), 43 | UpdateTime: d.UpdatedAt.Unix(), 44 | } 45 | } 46 | 47 | func (d *Device) IsLegal() bool { 48 | if d.Type == 0 || d.Brand == "" || d.Model == "" || 49 | d.SystemVersion == "" || d.SDKVersion == "" { 50 | return false 51 | } 52 | return true 53 | } 54 | 55 | func (d *Device) Online(userId uint64, connAddr string, clientAddr string) { 56 | d.UserId = userId 57 | d.ConnAddr = connAddr 58 | d.ClientAddr = clientAddr 59 | d.Status = OnLine 60 | } 61 | 62 | func (d *Device) Offline(userId uint64, connAddr string, clientAddr string) { 63 | d.UserId = userId 64 | d.ConnAddr = connAddr 65 | d.ClientAddr = clientAddr 66 | d.Status = OnLine 67 | } 68 | -------------------------------------------------------------------------------- /internal/logic/device/repo.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | 8 | "gim/internal/user/domain" 9 | "gim/pkg/db" 10 | "gim/pkg/gerrors" 11 | ) 12 | 13 | type repo struct{} 14 | 15 | var Repo = new(repo) 16 | 17 | // Get 获取设备 18 | func (*repo) Get(deviceID uint64) (*Device, error) { 19 | var device Device 20 | err := db.DB.First(&device, "id = ?", deviceID).Error 21 | if errors.Is(err, gorm.ErrRecordNotFound) { 22 | return nil, gerrors.ErrDeviceNotFound 23 | } 24 | return &device, err 25 | } 26 | 27 | // Save 保存设备信息 28 | func (*repo) Save(device *Device) error { 29 | return db.DB.Save(&device).Error 30 | } 31 | 32 | // ListOnlineByUserId 获取用户的所有在线设备 33 | func (*repo) ListOnlineByUserId(userIds []uint64) ([]Device, error) { 34 | var devices []Device 35 | err := db.DB.Find(&devices, "user_id in (?) and status = ?", userIds, OnLine).Error 36 | return devices, err 37 | } 38 | 39 | // ListOnlineByConnAddr 查询用户所有的在线设备 40 | func (*repo) ListOnlineByConnAddr(connAddr string) ([]Device, error) { 41 | var devices []Device 42 | err := db.DB.Find(&devices, "conn_addr = ? and status = ?", connAddr, OnLine).Error 43 | return devices, err 44 | } 45 | 46 | // UpdateStatusOffline 更新设备为离线状态 47 | func (*repo) UpdateStatusOffline(device Device) error { 48 | return db.DB.Model(&domain.Device{}). 49 | Where("id = ? and conn_addr = ?", device.ConnAddr, device.ConnAddr). 50 | Update("status", device.Status).Error 51 | } 52 | -------------------------------------------------------------------------------- /internal/logic/device/repo_test.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_repo_Get(t *testing.T) { 8 | device, err := Repo.Get(1) 9 | t.Log(err) 10 | t.Log(device) 11 | } 12 | -------------------------------------------------------------------------------- /internal/logic/friend/api.go: -------------------------------------------------------------------------------- 1 | package friend 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/protobuf/types/known/emptypb" 7 | 8 | "gim/pkg/md" 9 | pb "gim/pkg/protocol/pb/logicpb" 10 | ) 11 | 12 | type FriendExtService struct { 13 | pb.UnsafeFriendExtServiceServer 14 | } 15 | 16 | // SendMessage 发送好友消息 17 | func (*FriendExtService) SendMessage(ctx context.Context, request *pb.SendFriendMessageRequest) (*pb.SendFriendMessageReply, error) { 18 | userId, deviceId, err := md.GetCtxData(ctx) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | messageId, err := App.SendToFriend(ctx, deviceId, userId, request) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &pb.SendFriendMessageReply{MessageId: messageId}, nil 28 | } 29 | 30 | func (s *FriendExtService) Add(ctx context.Context, request *pb.FriendAddRequest) (*emptypb.Empty, error) { 31 | userId, _, err := md.GetCtxData(ctx) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | err = App.AddFriend(ctx, userId, request.FriendId, request.Remarks, request.Description) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return &emptypb.Empty{}, nil 42 | } 43 | 44 | func (s *FriendExtService) Agree(ctx context.Context, request *pb.FriendAgreeRequest) (*emptypb.Empty, error) { 45 | userId, _, err := md.GetCtxData(ctx) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | err = App.AgreeAddFriend(ctx, userId, request.UserId, request.Remarks) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return &emptypb.Empty{}, nil 56 | } 57 | 58 | func (s *FriendExtService) Set(ctx context.Context, request *pb.FriendSetRequest) (*pb.FriendSetReply, error) { 59 | userId, _, err := md.GetCtxData(ctx) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | err = App.SetFriend(ctx, userId, request) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return &pb.FriendSetReply{}, nil 69 | } 70 | 71 | func (s *FriendExtService) GetFriends(ctx context.Context, request *emptypb.Empty) (*pb.GetFriendsReply, error) { 72 | userId, _, err := md.GetCtxData(ctx) 73 | if err != nil { 74 | return nil, err 75 | } 76 | friends, err := App.List(ctx, userId) 77 | return &pb.GetFriendsReply{Friends: friends}, err 78 | } 79 | -------------------------------------------------------------------------------- /internal/logic/friend/api_test.go: -------------------------------------------------------------------------------- 1 | package friend 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "google.golang.org/grpc/metadata" 8 | "google.golang.org/protobuf/types/known/emptypb" 9 | 10 | "gim/pkg/local" 11 | "gim/pkg/md" 12 | pb "gim/pkg/protocol/pb/logicpb" 13 | ) 14 | 15 | func TestFriendExtService_Add(t *testing.T) { 16 | local.Init() 17 | 18 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 19 | md.CtxUserID: "2", 20 | md.CtxDeviceID: "2", 21 | })) 22 | 23 | reply, err := new(FriendExtService).Add(ctx, &pb.FriendAddRequest{ 24 | FriendId: 1, 25 | Remarks: "1号朋友", 26 | Description: "我是2号朋友", 27 | }) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | t.Log(reply) 32 | } 33 | 34 | func TestFriendExtService_Agree(t *testing.T) { 35 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 36 | md.CtxUserID: "1", 37 | md.CtxDeviceID: "1", 38 | })) 39 | 40 | reply, err := new(FriendExtService).Agree(ctx, &pb.FriendAgreeRequest{ 41 | UserId: 2, 42 | Remarks: "2号朋友", 43 | }) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | t.Log(reply) 48 | } 49 | 50 | func TestFriendExtService_SendMessage(t *testing.T) { 51 | local.Init() 52 | 53 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 54 | md.CtxUserID: "2", 55 | md.CtxDeviceID: "2", 56 | })) 57 | 58 | reply, err := new(FriendExtService).SendMessage(ctx, &pb.SendFriendMessageRequest{ 59 | UserId: 1, 60 | Content: []byte("hello im 2 2"), 61 | }) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | t.Log(reply) 66 | } 67 | 68 | func TestFriendExtService_GetFriends(t *testing.T) { 69 | local.Init() 70 | 71 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 72 | md.CtxUserID: "2", 73 | md.CtxDeviceID: "2", 74 | })) 75 | 76 | reply, err := new(FriendExtService).GetFriends(ctx, &emptypb.Empty{}) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | for _, friend := range reply.Friends { 81 | t.Log(friend) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/logic/friend/app.go: -------------------------------------------------------------------------------- 1 | package friend 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "google.golang.org/protobuf/proto" 9 | 10 | "gim/internal/logic/message" 11 | "gim/pkg/gerrors" 12 | pb "gim/pkg/protocol/pb/logicpb" 13 | "gim/pkg/protocol/pb/userpb" 14 | "gim/pkg/rpc" 15 | ) 16 | 17 | type app struct{} 18 | 19 | var App = new(app) 20 | 21 | // List 获取好友列表 22 | func (s *app) List(ctx context.Context, userId uint64) ([]*pb.Friend, error) { 23 | friends, err := Repo.List(userId, FriendStatusAgree) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | userIds := make(map[uint64]int32, len(friends)) 29 | for i := range friends { 30 | userIds[friends[i].FriendID] = 0 31 | } 32 | reply, err := rpc.GetUserIntClient().GetUsers(ctx, &userpb.GetUsersRequest{UserIds: userIds}) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | var infos = make([]*pb.Friend, len(friends)) 38 | for i := range friends { 39 | friend := pb.Friend{ 40 | UserId: friends[i].FriendID, 41 | Remarks: friends[i].Remarks, 42 | Extra: friends[i].Extra, 43 | } 44 | 45 | user, ok := reply.Users[friends[i].FriendID] 46 | if ok { 47 | friend.Nickname = user.Nickname 48 | friend.Sex = user.Sex 49 | friend.AvatarUrl = user.AvatarUrl 50 | friend.UserExtra = user.Extra 51 | } 52 | infos[i] = &friend 53 | } 54 | 55 | return infos, nil 56 | } 57 | 58 | // AddFriend 添加好友 59 | func (*app) AddFriend(ctx context.Context, userId, friendId uint64, remarks, description string) error { 60 | friend, err := Repo.Get(userId, friendId) 61 | if err != nil && !errors.Is(err, gerrors.ErrFriendNotFound) { 62 | return err 63 | } 64 | if err == nil { 65 | if friend.Status == FriendStatusApply { 66 | return nil 67 | } 68 | if friend.Status == FriendStatusAgree { 69 | return gerrors.ErrAlreadyIsFriend 70 | } 71 | } 72 | 73 | err = Repo.Create(&Friend{ 74 | UserID: userId, 75 | FriendID: friendId, 76 | Remarks: remarks, 77 | Status: FriendStatusApply, 78 | }) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | reply, err := rpc.GetUserIntClient().GetUser(ctx, &userpb.GetUserRequest{UserId: userId}) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | _, err = message.App.PushToUser(ctx, []uint64{friendId}, pb.PushCode_PC_ADD_FRIEND, &pb.AddFriendPush{ 89 | FriendId: userId, 90 | Nickname: reply.User.Nickname, 91 | AvatarUrl: reply.User.AvatarUrl, 92 | Description: description, 93 | }, true) 94 | return err 95 | } 96 | 97 | // AgreeAddFriend 同意添加好友 98 | func (*app) AgreeAddFriend(ctx context.Context, userId, friendId uint64, remarks string) error { 99 | friend, err := Repo.Get(friendId, userId) 100 | if err != nil { 101 | return err 102 | } 103 | if friend.Status == FriendStatusAgree { 104 | return nil 105 | } 106 | friend.Status = FriendStatusAgree 107 | err = Repo.Save(friend) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | err = Repo.Save(&Friend{ 113 | UserID: userId, 114 | FriendID: friendId, 115 | Remarks: remarks, 116 | Status: FriendStatusAgree, 117 | }) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | reply, err := rpc.GetUserIntClient().GetUser(ctx, &userpb.GetUserRequest{UserId: userId}) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | _, err = message.App.PushToUser(ctx, []uint64{friendId}, pb.PushCode_PC_AGREE_ADD_FRIEND, &pb.AgreeAddFriendPush{ 128 | FriendId: userId, 129 | Nickname: reply.User.Nickname, 130 | AvatarUrl: reply.User.AvatarUrl, 131 | }, true) 132 | return err 133 | } 134 | 135 | // SetFriend 设置好友信息 136 | func (*app) SetFriend(ctx context.Context, userId uint64, req *pb.FriendSetRequest) error { 137 | friend, err := Repo.Get(userId, req.FriendId) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | friend.Remarks = req.Remarks 143 | friend.Extra = req.Extra 144 | friend.UpdatedAt = time.Now() 145 | 146 | return Repo.Save(friend) 147 | } 148 | 149 | // SendToFriend 消息发送至好友 150 | func (*app) SendToFriend(ctx context.Context, fromDeviceID, fromUserID uint64, req *pb.SendFriendMessageRequest) (uint64, error) { 151 | sender, err := rpc.GetSender(fromDeviceID, fromUserID) 152 | if err != nil { 153 | return 0, err 154 | } 155 | 156 | // 发给发送者 157 | push := pb.UserMessagePush{ 158 | Sender: sender, 159 | Content: req.Content, 160 | } 161 | bytes, err := proto.Marshal(&push) 162 | if err != nil { 163 | return 0, err 164 | } 165 | 166 | msg := &pb.Message{ 167 | Code: pb.PushCode_PC_USER_MESSAGE, 168 | Content: bytes, 169 | } 170 | 171 | userIDs := []uint64{fromUserID, req.UserId} 172 | return message.App.SendToUsers(ctx, userIDs, msg, true) 173 | } 174 | -------------------------------------------------------------------------------- /internal/logic/friend/entity.go: -------------------------------------------------------------------------------- 1 | package friend 2 | 3 | import "time" 4 | 5 | const ( 6 | FriendStatusApply = 0 // 申请 7 | FriendStatusAgree = 1 // 同意 8 | ) 9 | 10 | type Friend struct { 11 | UserID uint64 // 用户ID 12 | FriendID uint64 // 好友ID 13 | CreatedAt time.Time // 创建时间 14 | UpdatedAt time.Time // 更新时间 15 | Remarks string // 备注 16 | Extra string // 扩展字段 17 | Status int // 状态 18 | } 19 | -------------------------------------------------------------------------------- /internal/logic/friend/repo.go: -------------------------------------------------------------------------------- 1 | package friend 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | 8 | "gim/pkg/db" 9 | "gim/pkg/gerrors" 10 | ) 11 | 12 | type repo struct{} 13 | 14 | var Repo = new(repo) 15 | 16 | // Get 获取好友 17 | func (*repo) Get(userId, friendId uint64) (*Friend, error) { 18 | friend := Friend{} 19 | err := db.DB.First(&friend, "user_id = ? and friend_id = ?", userId, friendId).Error 20 | if errors.Is(err, gorm.ErrRecordNotFound) { 21 | return nil, gerrors.ErrFriendNotFound 22 | } 23 | return &friend, err 24 | } 25 | 26 | // Create 添加好友 27 | func (*repo) Create(friend *Friend) error { 28 | return db.DB.Create(friend).Error 29 | } 30 | 31 | // Save 添加好友 32 | func (*repo) Save(friend *Friend) error { 33 | return db.DB.Where("user_id = ? and friend_id = ?", friend.UserID, friend.FriendID).Save(friend).Error 34 | } 35 | 36 | // List 获取好友列表 37 | func (*repo) List(userId uint64, status int) ([]Friend, error) { 38 | var friends []Friend 39 | err := db.DB.Where("user_id = ? and status = ?", userId, status).Find(&friends).Error 40 | return friends, err 41 | } 42 | -------------------------------------------------------------------------------- /internal/logic/friend/repo_test.go: -------------------------------------------------------------------------------- 1 | package friend 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_friendDao_Get(t *testing.T) { 8 | friend, err := Repo.Get(1, 2) 9 | if err != nil { 10 | t.Fatal(err) 11 | } 12 | t.Log(friend) 13 | } 14 | 15 | func Test_friendDao_Save(t *testing.T) { 16 | err := Repo.Save(&Friend{ 17 | UserID: 1, 18 | FriendID: 2, 19 | }) 20 | t.Log(err) 21 | } 22 | 23 | func Test_friendDao_List(t *testing.T) { 24 | friends, err := Repo.List(1, FriendStatusAgree) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | for _, friend := range friends { 29 | t.Log(friend) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/logic/group/api.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/protobuf/types/known/emptypb" 7 | 8 | "gim/pkg/md" 9 | pb "gim/pkg/protocol/pb/logicpb" 10 | ) 11 | 12 | type GroupExtService struct { 13 | pb.UnsafeGroupExtServiceServer 14 | } 15 | 16 | // SendMessage 发送群组消息 17 | func (*GroupExtService) SendMessage(ctx context.Context, request *pb.SendGroupMessageRequest) (*pb.SendGroupMessageReply, error) { 18 | userId, deviceId, err := md.GetCtxData(ctx) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | messageId, err := App.SendMessage(ctx, deviceId, userId, request) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &pb.SendGroupMessageReply{MessageId: messageId}, nil 28 | } 29 | 30 | // Create 创建群组 31 | func (*GroupExtService) Create(ctx context.Context, request *pb.GroupCreateRequest) (*pb.GroupCreateReply, error) { 32 | userId, _, err := md.GetCtxData(ctx) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | groupId, err := App.CreateGroup(ctx, userId, request) 38 | return &pb.GroupCreateReply{GroupId: groupId}, err 39 | } 40 | 41 | // Update 更新群组 42 | func (*GroupExtService) Update(ctx context.Context, request *pb.GroupUpdateRequest) (*emptypb.Empty, error) { 43 | userId, _, err := md.GetCtxData(ctx) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | err = App.Update(ctx, userId, request) 49 | return &emptypb.Empty{}, err 50 | } 51 | 52 | // Get 获取群组信息 53 | func (*GroupExtService) Get(ctx context.Context, request *pb.GroupGetRequest) (*pb.GroupGetReply, error) { 54 | group, err := App.GetGroup(ctx, request.GroupId) 55 | return &pb.GroupGetReply{Group: group}, err 56 | } 57 | 58 | // List 获取用户加入的所有群组 59 | func (*GroupExtService) List(ctx context.Context, in *emptypb.Empty) (*pb.GroupListReply, error) { 60 | userId, _, err := md.GetCtxData(ctx) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | groups, err := App.GetUserGroups(ctx, userId) 66 | return &pb.GroupListReply{Groups: groups}, err 67 | } 68 | 69 | func (s *GroupExtService) AddMembers(ctx context.Context, in *pb.AddMembersRequest) (*pb.AddMembersReply, error) { 70 | userId, _, err := md.GetCtxData(ctx) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | err = App.AddMembers(ctx, userId, in.GroupId, in.UserIds) 76 | return &pb.AddMembersReply{}, err 77 | } 78 | 79 | // UpdateMember 更新群组成员信息 80 | func (*GroupExtService) UpdateMember(ctx context.Context, in *pb.UpdateMemberRequest) (*emptypb.Empty, error) { 81 | return &emptypb.Empty{}, App.UpdateMember(ctx, in) 82 | } 83 | 84 | // DeleteMember 添加群组成员 85 | func (*GroupExtService) DeleteMember(ctx context.Context, in *pb.DeleteMemberRequest) (*emptypb.Empty, error) { 86 | userId, _, err := md.GetCtxData(ctx) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | err = App.DeleteMember(ctx, in.GroupId, in.UserId, userId) 92 | return &emptypb.Empty{}, err 93 | } 94 | 95 | // GetMembers 获取群组成员信息 96 | func (s *GroupExtService) GetMembers(ctx context.Context, in *pb.GetMembersRequest) (*pb.GetMembersReply, error) { 97 | members, err := App.GetMembers(ctx, in.GroupId) 98 | return &pb.GetMembersReply{Members: members}, err 99 | } 100 | -------------------------------------------------------------------------------- /internal/logic/group/api_test.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/credentials/insecure" 9 | "google.golang.org/grpc/metadata" 10 | "google.golang.org/protobuf/types/known/emptypb" 11 | 12 | "gim/pkg/md" 13 | pb "gim/pkg/protocol/pb/logicpb" 14 | ) 15 | 16 | func getExtClient() pb.GroupExtServiceClient { 17 | conn, err := grpc.NewClient("127.0.0.1:8010", grpc.WithTransportCredentials(insecure.NewCredentials())) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return pb.NewGroupExtServiceClient(conn) 22 | } 23 | 24 | func TestGroupExtService_Create(t *testing.T) { 25 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 26 | md.CtxUserID: "1", 27 | md.CtxDeviceID: "1", 28 | })) 29 | 30 | reply, err := new(GroupExtService).Create(ctx, &pb.GroupCreateRequest{ 31 | Name: "群组A", 32 | AvatarUrl: "", 33 | Introduction: "群组A的介绍", 34 | Extra: "", 35 | MemberIds: []uint64{2}, 36 | }) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | t.Log(reply) 41 | } 42 | 43 | func TestGroupExtService_Update(t *testing.T) { 44 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 45 | md.CtxUserID: "1", 46 | md.CtxDeviceID: "1", 47 | })) 48 | 49 | reply, err := new(GroupExtService).Update(ctx, &pb.GroupUpdateRequest{ 50 | GroupId: 5, 51 | Name: "群组B", 52 | AvatarUrl: "", 53 | Introduction: "群组B的介绍", 54 | Extra: "", 55 | }) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | t.Log(reply) 60 | } 61 | 62 | func TestGroupExtService_Get(t *testing.T) { 63 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 64 | md.CtxUserID: "1", 65 | md.CtxDeviceID: "1", 66 | })) 67 | 68 | reply, err := new(GroupExtService).Get(ctx, &pb.GroupGetRequest{GroupId: 5}) 69 | if err != nil { 70 | t.Error(err) 71 | } 72 | t.Log(reply) 73 | } 74 | 75 | func TestGroupExtService_List(t *testing.T) { 76 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 77 | md.CtxUserID: "1", 78 | md.CtxDeviceID: "1", 79 | })) 80 | 81 | reply, err := new(GroupExtService).List(ctx, &emptypb.Empty{}) 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | for _, group := range reply.Groups { 86 | t.Log(group) 87 | } 88 | } 89 | 90 | func TestGroupExtService_AddMembers(t *testing.T) { 91 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 92 | md.CtxUserID: "1", 93 | md.CtxDeviceID: "1", 94 | })) 95 | 96 | _, err := new(GroupExtService).AddMembers(ctx, &pb.AddMembersRequest{ 97 | GroupId: 5, 98 | UserIds: []uint64{3}, 99 | }) 100 | if err != nil { 101 | t.Error(err) 102 | } 103 | } 104 | 105 | func TestGroupExtService_UpdateMember(t *testing.T) { 106 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 107 | md.CtxUserID: "1", 108 | md.CtxDeviceID: "1", 109 | })) 110 | 111 | _, err := new(GroupExtService).UpdateMember(ctx, &pb.UpdateMemberRequest{ 112 | GroupId: 5, 113 | UserId: 3, 114 | MemberType: 2, 115 | Remarks: "1", 116 | Extra: "1", 117 | }) 118 | if err != nil { 119 | t.Error(err) 120 | } 121 | } 122 | 123 | func TestGroupExtService_DeleteMember(t *testing.T) { 124 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 125 | md.CtxUserID: "1", 126 | md.CtxDeviceID: "1", 127 | })) 128 | 129 | _, err := new(GroupExtService).DeleteMember(ctx, &pb.DeleteMemberRequest{ 130 | GroupId: 5, 131 | UserId: 3, 132 | }) 133 | if err != nil { 134 | t.Error(err) 135 | } 136 | } 137 | 138 | func TestGroupExtService_GetMembers(t *testing.T) { 139 | ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ 140 | md.CtxUserID: "1", 141 | md.CtxDeviceID: "1", 142 | })) 143 | 144 | reply, err := new(GroupExtService).GetMembers(ctx, &pb.GetMembersRequest{GroupId: 5}) 145 | if err != nil { 146 | t.Error(err) 147 | } 148 | for _, member := range reply.Members { 149 | t.Log(member) 150 | } 151 | } 152 | 153 | func TestGroupExtService_SendMessage(t *testing.T) { 154 | ctx := metadata.NewOutgoingContext(context.TODO(), metadata.New(map[string]string{ 155 | md.CtxUserID: "1", 156 | md.CtxDeviceID: "1", 157 | md.CtxToken: "0", 158 | })) 159 | 160 | reply, err := getExtClient().SendMessage(ctx, &pb.SendGroupMessageRequest{ 161 | GroupId: 5, 162 | Content: []byte("group msg hello"), 163 | }) 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | t.Log(reply.MessageId) 168 | } 169 | -------------------------------------------------------------------------------- /internal/logic/group/app.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "context" 5 | 6 | "gim/internal/logic/group/domain" 7 | "gim/internal/logic/group/repo" 8 | pb "gim/pkg/protocol/pb/logicpb" 9 | ) 10 | 11 | type app struct{} 12 | 13 | var App = new(app) 14 | 15 | // CreateGroup 创建群组 16 | func (*app) CreateGroup(ctx context.Context, userId uint64, in *pb.GroupCreateRequest) (uint64, error) { 17 | group := domain.CreateGroup(userId, in) 18 | err := repo.GroupRepo.Save(group) 19 | if err != nil { 20 | return 0, err 21 | } 22 | 23 | memberIDs := append([]uint64{userId}, in.MemberIds...) 24 | members := make([]domain.GroupUser, 0, len(memberIDs)) 25 | for _, memberID := range memberIDs { 26 | memberType := pb.MemberType_GMT_MEMBER 27 | if memberID == userId { 28 | memberType = pb.MemberType_GMT_ADMIN 29 | } 30 | members = append(members, domain.GroupUser{ 31 | GroupID: group.ID, 32 | UserID: memberID, 33 | MemberType: memberType, 34 | }) 35 | } 36 | err = repo.GroupUserRepo.BatchCreate(members) 37 | return group.ID, err 38 | } 39 | 40 | // GetGroup 获取群组信息 41 | func (*app) GetGroup(ctx context.Context, groupId uint64) (*pb.Group, error) { 42 | group, err := repo.GroupRepo.Get(groupId) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return group.ToProto(), nil 48 | } 49 | 50 | // GetUserGroups 获取用户加入的群组列表 51 | func (*app) GetUserGroups(ctx context.Context, userId uint64) ([]*pb.Group, error) { 52 | groups, err := repo.GroupUserRepo.ListByUserId(userId) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | pbGroups := make([]*pb.Group, len(groups)) 58 | for i := range groups { 59 | pbGroups[i] = groups[i].ToProto() 60 | } 61 | return pbGroups, nil 62 | } 63 | 64 | // Update 更新群组 65 | func (*app) Update(ctx context.Context, userId uint64, request *pb.GroupUpdateRequest) error { 66 | group, err := repo.GroupRepo.Get(request.GroupId) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | group.Name = request.Name 72 | group.AvatarUrl = request.AvatarUrl 73 | group.Introduction = request.Introduction 74 | group.Extra = request.Extra 75 | 76 | err = repo.GroupRepo.Save(group) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return group.PushUpdate(ctx, userId) 82 | } 83 | 84 | // AddMembers 添加群组成员 85 | func (*app) AddMembers(ctx context.Context, userId, groupId uint64, userIds []uint64) error { 86 | group, err := repo.GroupRepo.Get(groupId) 87 | if err != nil { 88 | return err 89 | } 90 | members, err := group.AddMembers(userIds) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | err = repo.GroupRepo.Save(group) 96 | if err != nil { 97 | return err 98 | } 99 | err = repo.GroupUserRepo.BatchCreate(members) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | err = group.PushAddMember(ctx, userId, members) 105 | if err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | // UpdateMember 更新群组用户 112 | func (*app) UpdateMember(ctx context.Context, in *pb.UpdateMemberRequest) error { 113 | member, err := repo.GroupUserRepo.Get(in.GroupId, in.UserId) 114 | if err != nil { 115 | return err 116 | } 117 | member.MemberType = in.MemberType 118 | member.Remarks = in.Remarks 119 | member.Extra = in.Extra 120 | 121 | return repo.GroupUserRepo.Save(member) 122 | } 123 | 124 | // DeleteMember 删除群组成员 125 | func (*app) DeleteMember(ctx context.Context, groupId, userId, optId uint64) error { 126 | err := repo.GroupUserRepo.Delete(groupId, userId) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | group, err := repo.GroupRepo.Get(groupId) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | return group.PushDeleteMember(ctx, optId, userId) 137 | } 138 | 139 | // GetMembers 获取群组成员 140 | func (*app) GetMembers(ctx context.Context, groupId uint64) ([]*pb.GroupMember, error) { 141 | group, err := repo.GroupRepo.Get(groupId) 142 | if err != nil { 143 | return nil, err 144 | } 145 | return group.GetMembers(ctx) 146 | } 147 | 148 | // SendMessage 发送群组消息 149 | func (*app) SendMessage(ctx context.Context, fromDeviceID, fromUserID uint64, req *pb.SendGroupMessageRequest) (uint64, error) { 150 | group, err := repo.GroupRepo.Get(req.GroupId) 151 | if err != nil { 152 | return 0, err 153 | } 154 | 155 | return group.SendMessage(ctx, fromDeviceID, fromUserID, req) 156 | } 157 | -------------------------------------------------------------------------------- /internal/logic/group/domain/group_user.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | pb "gim/pkg/protocol/pb/logicpb" 7 | ) 8 | 9 | type GroupUser struct { 10 | GroupID uint64 // 群组id 11 | UserID uint64 // 用户id 12 | CreatedAt time.Time // 创建时间 13 | UpdatedAt time.Time // 更新时间 14 | MemberType pb.MemberType // 成员类型 15 | Remarks string // 备注 16 | Extra string // 附加属性 17 | Status int // 状态 18 | } 19 | -------------------------------------------------------------------------------- /internal/logic/group/repo/group_cache.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "gim/internal/logic/group/domain" 8 | "gim/pkg/db" 9 | ) 10 | 11 | const GroupKey = "group:" 12 | 13 | type groupCache struct{} 14 | 15 | var GroupCache = new(groupCache) 16 | 17 | // Get 获取群组缓存 18 | func (c *groupCache) Get(groupId uint64) (*domain.Group, error) { 19 | var user domain.Group 20 | err := db.RedisUtil.Get(GroupKey+strconv.FormatUint(groupId, 10), &user) 21 | return &user, err 22 | } 23 | 24 | // Set 设置群组缓存 25 | func (c *groupCache) Set(group *domain.Group) error { 26 | return db.RedisUtil.Set(GroupKey+strconv.FormatUint(group.ID, 10), group, 24*time.Hour) 27 | } 28 | 29 | // Delete 删除群组缓存 30 | func (c *groupCache) Delete(groupId uint64) error { 31 | _, err := db.RedisCli.Del(GroupKey + strconv.FormatUint(groupId, 10)).Result() 32 | return err 33 | } 34 | -------------------------------------------------------------------------------- /internal/logic/group/repo/group_dao.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | 8 | "gim/internal/logic/group/domain" 9 | "gim/pkg/db" 10 | "gim/pkg/gerrors" 11 | ) 12 | 13 | type groupDao struct{} 14 | 15 | var GroupDao = new(groupDao) 16 | 17 | // Get 获取群组信息 18 | func (*groupDao) Get(groupID uint64) (*domain.Group, error) { 19 | var group domain.Group 20 | err := db.DB.Preload("Members").First(&group, "id = ?", groupID).Error 21 | if errors.Is(err, gorm.ErrRecordNotFound) { 22 | return nil, gerrors.ErrGroupNotFound 23 | } 24 | return &group, err 25 | } 26 | 27 | // Save 插入一条群组 28 | func (*groupDao) Save(group *domain.Group) error { 29 | return db.DB.Save(&group).Error 30 | } 31 | -------------------------------------------------------------------------------- /internal/logic/group/repo/group_dao_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGroupDao_Get(t *testing.T) { 8 | group, err := GroupDao.Get(5) 9 | if err != nil { 10 | t.Fatal(err) 11 | } 12 | t.Log(group) 13 | 14 | for i := range group.Members { 15 | t.Log(group.Members[i]) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/logic/group/repo/group_repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "gim/internal/logic/group/domain" 5 | ) 6 | 7 | type groupRepo struct{} 8 | 9 | var GroupRepo = new(groupRepo) 10 | 11 | // Get 获取群组信息 12 | func (*groupRepo) Get(groupId uint64) (*domain.Group, error) { 13 | group, err := GroupCache.Get(groupId) 14 | if err == nil { 15 | return group, nil 16 | } 17 | 18 | group, err = GroupDao.Get(groupId) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | err = GroupCache.Set(group) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return group, nil 28 | } 29 | 30 | // Save 获取群组信息 31 | func (*groupRepo) Save(group *domain.Group) error { 32 | err := GroupDao.Save(group) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return GroupCache.Delete(group.ID) 38 | } 39 | -------------------------------------------------------------------------------- /internal/logic/group/repo/group_user_repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | 8 | "gim/internal/logic/group/domain" 9 | "gim/pkg/db" 10 | "gim/pkg/gerrors" 11 | ) 12 | 13 | type groupUserRepo struct{} 14 | 15 | var GroupUserRepo = new(groupUserRepo) 16 | 17 | // ListByUserId 获取用户加入的群组信息 18 | func (*groupUserRepo) ListByUserId(userID uint64) ([]domain.Group, error) { 19 | var groupUsers []domain.GroupUser 20 | err := db.DB.Find(&groupUsers, "user_id = ?", userID).Error 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | groupIds := make([]uint64, 0, len(groupUsers)) 26 | for _, groupUser := range groupUsers { 27 | groupIds = append(groupIds, groupUser.GroupID) 28 | } 29 | var groups []domain.Group 30 | err = db.DB.Find(&groups, "id in (?)", groupIds).Error 31 | return groups, err 32 | } 33 | 34 | // Get 获取群组成员信息 35 | func (*groupUserRepo) Get(groupId, userId uint64) (*domain.GroupUser, error) { 36 | var groupUser domain.GroupUser 37 | err := db.DB.First(&groupUser, "group_id = ? and user_id = ?", groupId, userId).Error 38 | if errors.Is(err, gorm.ErrRecordNotFound) { 39 | return nil, gerrors.ErrGroupUserNotFound 40 | } 41 | return &groupUser, err 42 | } 43 | 44 | func (*groupUserRepo) BatchCreate(groupUsers []domain.GroupUser) error { 45 | if len(groupUsers) == 0 { 46 | return nil 47 | } 48 | return db.DB.Create(&groupUsers).Error 49 | } 50 | 51 | // Save 将用户添加到群组 52 | func (*groupUserRepo) Save(groupUser *domain.GroupUser) error { 53 | return db.DB.Where("group_id = ? and user_id = ?", groupUser.GroupID, groupUser.UserID).Save(&groupUser).Error 54 | } 55 | 56 | // Delete 将用户从群组删除 57 | func (d *groupUserRepo) Delete(groupId, userId uint64) error { 58 | return db.DB.Delete(&domain.GroupUser{}, "group_id = ? and user_id = ?", groupId, userId).Error 59 | } 60 | -------------------------------------------------------------------------------- /internal/logic/group/repo/group_user_repo_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "gim/internal/logic/group/domain" 7 | pb "gim/pkg/protocol/pb/logicpb" 8 | ) 9 | 10 | func TestGroupUserDao_ListByUserId(t *testing.T) { 11 | groups, err := GroupUserRepo.ListByUserId(1) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | for _, group := range groups { 16 | t.Log(group) 17 | } 18 | } 19 | 20 | func TestGroupUserDao_Get(t *testing.T) { 21 | groupUser, err := GroupUserRepo.Get(1, 1) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | t.Log(groupUser) 26 | } 27 | 28 | func TestGroupUserDao_Delete(t *testing.T) { 29 | err := GroupUserRepo.Delete(1, 1) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | 35 | func Test_groupUserRepo_Save(t *testing.T) { 36 | err := GroupUserRepo.Save(&domain.GroupUser{ 37 | GroupID: 1, 38 | UserID: 1, 39 | MemberType: pb.MemberType_GMT_MEMBER, 40 | Remarks: "", 41 | Extra: "", 42 | Status: 0, 43 | }) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/logic/message/api.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/protobuf/types/known/emptypb" 7 | 8 | pb "gim/pkg/protocol/pb/logicpb" 9 | ) 10 | 11 | type MessageIntService struct { 12 | pb.UnsafeMessageIntServiceServer 13 | } 14 | 15 | // Sync 设备同步消息 16 | func (*MessageIntService) Sync(ctx context.Context, request *pb.SyncRequest) (*pb.SyncReply, error) { 17 | return App.Sync(ctx, request.UserId, request.Seq) 18 | } 19 | 20 | // MessageACK 设备收到消息ack 21 | func (*MessageIntService) MessageACK(ctx context.Context, request *pb.MessageACKRequest) (*emptypb.Empty, error) { 22 | return &emptypb.Empty{}, App.MessageAck(ctx, request.UserId, request.DeviceId, request.DeviceAck) 23 | } 24 | 25 | // Pushs 推送 26 | func (*MessageIntService) Pushs(ctx context.Context, request *pb.PushsRequest) (*pb.PushsReply, error) { 27 | messageID, err := App.PushToUserData(ctx, request.UserIds, request.Code, request.Content, request.IsPersist) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &pb.PushsReply{MessageId: messageID}, nil 32 | } 33 | 34 | // PushAll 全服推送 35 | func (s *MessageIntService) PushAll(ctx context.Context, request *pb.PushAllRequest) (*emptypb.Empty, error) { 36 | return &emptypb.Empty{}, App.PushAll(ctx, request) 37 | } 38 | -------------------------------------------------------------------------------- /internal/logic/message/api_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/credentials/insecure" 9 | 10 | pb "gim/pkg/protocol/pb/logicpb" 11 | ) 12 | 13 | func getClient() pb.MessageIntServiceClient { 14 | conn, err := grpc.NewClient("127.0.0.1:8010", grpc.WithTransportCredentials(insecure.NewCredentials())) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return pb.NewMessageIntServiceClient(conn) 19 | } 20 | 21 | func TestMessageIntService_Pushs(t *testing.T) { 22 | reply, err := getClient().Pushs(context.TODO(), &pb.PushsRequest{ 23 | UserIds: []uint64{1}, 24 | Code: 200, 25 | Content: []byte("hello gim3 hi"), 26 | IsPersist: true, 27 | }) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | t.Log(reply) 32 | } 33 | 34 | func TestMessageIntService_PushsLocal(t *testing.T) { 35 | reply, err := new(MessageIntService).Pushs(context.TODO(), &pb.PushsRequest{ 36 | UserIds: []uint64{1}, 37 | Code: 100, 38 | Content: []byte("hello gim3"), 39 | IsPersist: true, 40 | }) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | t.Log(reply) 45 | } 46 | -------------------------------------------------------------------------------- /internal/logic/message/app.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "time" 7 | 8 | "google.golang.org/protobuf/proto" 9 | 10 | "gim/internal/logic/device" 11 | "gim/internal/logic/message/domain" 12 | "gim/internal/logic/message/repo" 13 | "gim/pkg/grpclib/picker" 14 | "gim/pkg/md" 15 | "gim/pkg/mq" 16 | "gim/pkg/protocol/pb/connectpb" 17 | pb "gim/pkg/protocol/pb/logicpb" 18 | "gim/pkg/rpc" 19 | "gim/pkg/util" 20 | ) 21 | 22 | const pageSize = 50 // 最大消息同步数量 23 | 24 | var App = new(app) 25 | 26 | type app struct{} 27 | 28 | func (a *app) PushToUserData(ctx context.Context, toUserIDs []uint64, code pb.PushCode, bytes []byte, isPersist bool) (uint64, error) { 29 | message := pb.Message{ 30 | Code: code, 31 | Content: bytes, 32 | Seq: 0, 33 | CreatedAt: 0, 34 | Status: 0, 35 | } 36 | messageID, err := a.SendToUsers(ctx, toUserIDs, &message, isPersist) 37 | if err != nil { 38 | slog.Error("PushToUser", "error", err) 39 | return 0, err 40 | } 41 | return messageID, nil 42 | } 43 | 44 | func (a *app) PushToUser(ctx context.Context, toUserID []uint64, code pb.PushCode, msg proto.Message, isPersist bool) (uint64, error) { 45 | bytes, err := proto.Marshal(msg) 46 | if err != nil { 47 | slog.Error("PushToUser", "error", err) 48 | return 0, err 49 | } 50 | return a.PushToUserData(ctx, toUserID, code, bytes, isPersist) 51 | } 52 | 53 | type userMessageAndDevices struct { 54 | userMessage *domain.UserMessage 55 | devices []*pb.Device 56 | } 57 | 58 | // SendToUsers 发送消息给用户 59 | func (a *app) SendToUsers(ctx context.Context, toUserIDs []uint64, message *pb.Message, isPersist bool) (uint64, error) { 60 | slog.Debug("SendToUser", "request_id", md.GetCtxRequestId(ctx), "to_user_ids", toUserIDs) 61 | 62 | var messageID uint64 63 | if isPersist { 64 | message := domain.Message{ 65 | RequestID: md.GetCtxRequestId(ctx), 66 | Code: message.Code, 67 | Content: message.Content, 68 | } 69 | err := repo.MessageRepo.Save(&message) 70 | if err != nil { 71 | return 0, err 72 | } 73 | messageID = message.ID 74 | } 75 | 76 | var userMessages []domain.UserMessage 77 | for _, userID := range toUserIDs { 78 | var userSeq uint64 79 | if isPersist { 80 | var err error 81 | userSeq, err = repo.SeqRepo.Incr(repo.SeqObjectTypeUser, userID) 82 | if err != nil { 83 | return 0, err 84 | } 85 | } 86 | 87 | userMessage := domain.UserMessage{ 88 | UserID: userID, 89 | Seq: userSeq, 90 | MessageID: messageID, 91 | } 92 | userMessages = append(userMessages, userMessage) 93 | } 94 | 95 | err := repo.UserMessageRepo.Save(userMessages) 96 | if err != nil { 97 | return 0, err 98 | } 99 | 100 | devices, err := device.App.ListOnlineByUserId(ctx, toUserIDs) 101 | if err != nil { 102 | return 0, err 103 | } 104 | 105 | userMessageAndDevicesList := make(map[uint64]*userMessageAndDevices, len(userMessages)) 106 | for i := range userMessages { 107 | userMessageAndDevicesList[userMessages[i].UserID] = &userMessageAndDevices{ 108 | userMessage: &userMessages[i], 109 | devices: nil, 110 | } 111 | } 112 | 113 | for _, device := range devices { 114 | value, ok := userMessageAndDevicesList[device.UserId] 115 | if ok { 116 | value.devices = append(value.devices, device) 117 | } 118 | } 119 | 120 | var deviceAndMessageList = make([]deviceAndMessage, 0, len(devices)) 121 | for _, value := range userMessageAndDevicesList { 122 | for _, device := range value.devices { 123 | message.Seq = value.userMessage.Seq 124 | deviceAndMessageList = append(deviceAndMessageList, deviceAndMessage{ 125 | device: device, 126 | message: message, 127 | }) 128 | } 129 | } 130 | 131 | err = a.PushToDevices(ctx, deviceAndMessageList) 132 | return messageID, err 133 | } 134 | 135 | type deviceAndMessage struct { 136 | device *pb.Device 137 | message *pb.Message 138 | } 139 | 140 | // PushToDevices 将消息发送给设备 141 | func (*app) PushToDevices(ctx context.Context, dms []deviceAndMessage) error { 142 | connects := make(map[string][]deviceAndMessage) 143 | for _, dm := range dms { 144 | connects[dm.device.ConnAddr] = append(connects[dm.device.ConnAddr], dm) 145 | } 146 | 147 | for addr, dmlist := range connects { 148 | request := &connectpb.PushToDevicesRequest{ 149 | DeviceMessageList: make([]*connectpb.DeviceMessage, 0, len(dmlist)), 150 | } 151 | for _, dm := range dmlist { 152 | request.DeviceMessageList = append(request.DeviceMessageList, &connectpb.DeviceMessage{ 153 | DeviceId: dm.device.DeviceId, 154 | Message: dm.message, 155 | }) 156 | } 157 | 158 | _, err := rpc.GetConnectIntClient().PushToDevices(picker.ContextWithAddr(ctx, addr), request) 159 | if err != nil { 160 | slog.Error("SendToDevice error", "error", err) 161 | return err 162 | } 163 | } 164 | 165 | // todo 其他推送厂商 166 | return nil 167 | } 168 | 169 | // PushAll 全服推送 170 | func (*app) PushAll(ctx context.Context, req *pb.PushAllRequest) error { 171 | msg := connectpb.PushAllMsg{ 172 | Message: &pb.Message{ 173 | Code: req.Code, 174 | Content: req.Content, 175 | CreatedAt: util.UnixMilliTime(time.Now()), 176 | }, 177 | } 178 | bytes, err := proto.Marshal(&msg) 179 | if err != nil { 180 | return err 181 | } 182 | return mq.Publish(mq.PushAllTopic, bytes) 183 | } 184 | 185 | // Sync 消息同步 186 | func (a *app) Sync(ctx context.Context, userId, seq uint64) (*pb.SyncReply, error) { 187 | messages, hasMore, err := a.listByUserIdAndSeq(ctx, userId, seq) 188 | if err != nil { 189 | return nil, err 190 | } 191 | pbMessages := domain.MessagesToPB(messages) 192 | 193 | reply := &pb.SyncReply{Messages: pbMessages, HasMore: hasMore} 194 | return reply, nil 195 | } 196 | 197 | // listByUserIdAndSeq 查询消息 198 | func (a *app) listByUserIdAndSeq(ctx context.Context, userId, seq uint64) ([]domain.UserMessage, bool, error) { 199 | var err error 200 | if seq == 0 { 201 | seq, err = a.getMaxByUserId(ctx, userId) 202 | if err != nil { 203 | return nil, false, err 204 | } 205 | } 206 | return repo.UserMessageRepo.ListBySeq(userId, seq, pageSize) 207 | } 208 | 209 | // getMaxByUserId 根据用户id获取最大ack 210 | func (*app) getMaxByUserId(ctx context.Context, userId uint64) (uint64, error) { 211 | acks, err := repo.DeviceACKRepo.Get(userId) 212 | if err != nil { 213 | return 0, err 214 | } 215 | 216 | var max uint64 = 0 217 | for i := range acks { 218 | if acks[i] > max { 219 | max = acks[i] 220 | } 221 | } 222 | return max, nil 223 | } 224 | 225 | // MessageAck 收到消息回执 226 | func (*app) MessageAck(ctx context.Context, userId, deviceId, ack uint64) error { 227 | if ack <= 0 { 228 | return nil 229 | } 230 | return repo.DeviceACKRepo.Set(userId, deviceId, ack) 231 | } 232 | -------------------------------------------------------------------------------- /internal/logic/message/domain/message.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | pb "gim/pkg/protocol/pb/logicpb" 7 | ) 8 | 9 | type Message struct { 10 | ID uint64 // 自增主键 11 | CreatedAt time.Time // 创建时间 12 | UpdatedAt time.Time // 更新时间 13 | RequestID int64 // 请求id 14 | Code pb.PushCode // 推送码 15 | Content []byte // 消息内容 16 | Status int8 // 消息状态,0:未处理;1:消息撤回 17 | } 18 | 19 | func (m *Message) TableName() string { 20 | return "message" 21 | } 22 | -------------------------------------------------------------------------------- /internal/logic/message/domain/user_message.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | pb "gim/pkg/protocol/pb/logicpb" 7 | "gim/pkg/util" 8 | ) 9 | 10 | type UserMessage struct { 11 | UserID uint64 // 用户ID 12 | Seq uint64 // 序列号 13 | CreatedAt time.Time // 创建时间 14 | UpdatedAt time.Time // 更新时间 15 | MessageID uint64 // 消息ID 16 | Message Message `gorm:"->"` // 消息 17 | } 18 | 19 | func (m *UserMessage) MessageToPB() *pb.Message { 20 | return &pb.Message{ 21 | Code: m.Message.Code, 22 | Content: m.Message.Content, 23 | Seq: m.Seq, 24 | CreatedAt: util.UnixMilliTime(m.Message.CreatedAt), 25 | Status: pb.MessageStatus(m.Message.Status), 26 | } 27 | } 28 | 29 | func MessagesToPB(messages []UserMessage) []*pb.Message { 30 | pbMessages := make([]*pb.Message, 0, len(messages)) 31 | for i := range messages { 32 | pbMessage := messages[i].MessageToPB() 33 | if pbMessages != nil { 34 | pbMessages = append(pbMessages, pbMessage) 35 | } 36 | } 37 | return pbMessages 38 | } 39 | -------------------------------------------------------------------------------- /internal/logic/message/repo/device_ack_repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "gim/pkg/db" 8 | ) 9 | 10 | const DeviceACKKey = "device_ack:%d" 11 | 12 | type deviceACKRepo struct{} 13 | 14 | var DeviceACKRepo = new(deviceACKRepo) 15 | 16 | // Set 设置设备同步序列号 17 | func (c *deviceACKRepo) Set(userId, deviceId, ack uint64) error { 18 | key := fmt.Sprintf(DeviceACKKey, userId) 19 | _, err := db.RedisCli.HSet(key, strconv.FormatUint(deviceId, 10), strconv.FormatUint(ack, 10)).Result() 20 | return err 21 | } 22 | 23 | func (c *deviceACKRepo) Get(userId uint64) (map[uint64]uint64, error) { 24 | key := fmt.Sprintf(DeviceACKKey, userId) 25 | result, err := db.RedisCli.HGetAll(key).Result() 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | acks := make(map[uint64]uint64, len(result)) 31 | for k, v := range result { 32 | deviceId, _ := strconv.ParseUint(k, 10, 64) 33 | ack, _ := strconv.ParseUint(v, 10, 64) 34 | acks[deviceId] = ack 35 | } 36 | return acks, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/logic/message/repo/message_repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "gim/internal/logic/message/domain" 5 | "gim/pkg/db" 6 | ) 7 | 8 | var MessageRepo = new(messageRepo) 9 | 10 | type messageRepo struct{} 11 | 12 | func (*messageRepo) Save(message *domain.Message) error { 13 | return db.DB.Create(&message).Error 14 | } 15 | 16 | func (*messageRepo) GetByIDs(ids []int64) ([]domain.Message, error) { 17 | var messages []domain.Message 18 | err := db.DB.Find(&messages, "id in (?)", ids).Error 19 | return messages, err 20 | } 21 | -------------------------------------------------------------------------------- /internal/logic/message/repo/message_repo_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "gim/internal/logic/message/domain" 7 | ) 8 | 9 | func Test_messageRepo_Save(t *testing.T) { 10 | msg := domain.Message{ 11 | RequestID: 1, 12 | Code: 1, 13 | Content: []byte("hello world"), 14 | Status: 0, 15 | } 16 | err := MessageRepo.Save(&msg) 17 | t.Log(err) 18 | } 19 | 20 | func Test_messageRepo_GetByIDs(t *testing.T) { 21 | msgs, err := MessageRepo.GetByIDs([]int64{1}) 22 | t.Log(err) 23 | t.Log(msgs) 24 | } 25 | -------------------------------------------------------------------------------- /internal/logic/message/repo/seq_repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | 7 | "gim/pkg/db" 8 | ) 9 | 10 | const ( 11 | SeqObjectTypeUser = 1 // 用户 12 | SeqObjectTypeRoom = 2 // 房间 13 | ) 14 | 15 | type seqRepo struct{} 16 | 17 | var SeqRepo = new(seqRepo) 18 | 19 | // Incr 自增seq,并且获取自增后的值 20 | func (*seqRepo) Incr(objectType int, objectId uint64) (uint64, error) { 21 | tx := db.DB.Begin() 22 | defer tx.Rollback() 23 | 24 | var seq uint64 25 | err := tx.Raw("select seq from seq where object_type = ? and object_id = ? for update", objectType, objectId). 26 | Row().Scan(&seq) 27 | if err != nil && !errors.Is(err, sql.ErrNoRows) { 28 | return 0, err 29 | } 30 | if errors.Is(err, sql.ErrNoRows) { 31 | err = tx.Exec("insert into seq (object_type,object_id,seq) values (?,?,?)", objectType, objectId, seq+1).Error 32 | if err != nil { 33 | return 0, err 34 | } 35 | } else { 36 | err = tx.Exec("update seq set seq = seq + 1 where object_type = ? and object_id = ?", objectType, objectId).Error 37 | if err != nil { 38 | return 0, err 39 | } 40 | } 41 | 42 | tx.Commit() 43 | return seq + 1, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/logic/message/repo/seq_repo_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test_seqDao_Incr(t *testing.T) { 9 | fmt.Println(SeqRepo.Incr(1, 5)) 10 | } 11 | -------------------------------------------------------------------------------- /internal/logic/message/repo/user_message_repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "gim/internal/logic/message/domain" 5 | "gim/pkg/db" 6 | ) 7 | 8 | var UserMessageRepo = new(userMessageRepo) 9 | 10 | type userMessageRepo struct{} 11 | 12 | // Save 插入一条消息 13 | func (d *userMessageRepo) Save(message []domain.UserMessage) error { 14 | return db.DB.Create(&message).Error 15 | } 16 | 17 | // ListBySeq 根据类型和id查询大于序号大于seq的消息 18 | func (d *userMessageRepo) ListBySeq(userId, seq uint64, limit int64) ([]domain.UserMessage, bool, error) { 19 | DB := db.DB.Table("user_message"). 20 | Where("user_id = ? and seq > ?", userId, seq) 21 | 22 | var count int64 23 | err := DB.Count(&count).Error 24 | if err != nil { 25 | return nil, false, err 26 | } 27 | if count == 0 { 28 | return nil, false, nil 29 | } 30 | 31 | var messages []domain.UserMessage 32 | err = DB.Limit(int(limit)).Preload("Message").Find(&messages).Error 33 | if err != nil { 34 | return nil, false, err 35 | } 36 | return messages, count > limit, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/logic/message/repo/user_message_repo_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "gim/internal/logic/message/domain" 7 | ) 8 | 9 | func TestUserMessageDao_Add(t *testing.T) { 10 | messages := []domain.UserMessage{ 11 | { 12 | UserID: 1, 13 | Seq: 1, 14 | MessageID: 1, 15 | }, 16 | } 17 | t.Log(UserMessageRepo.Save(messages)) 18 | } 19 | 20 | func TestUserMessageDao_ListByUserIdAndUserSeq(t *testing.T) { 21 | messages, hasMore, err := UserMessageRepo.ListBySeq(1, 0, 100) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | t.Log(hasMore) 26 | for i := range messages { 27 | t.Logf("%+v\n", messages[i]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/logic/room/api.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/protobuf/types/known/emptypb" 7 | 8 | pb "gim/pkg/protocol/pb/logicpb" 9 | ) 10 | 11 | type RoomExtService struct { 12 | pb.UnsafeRoomExtServiceServer 13 | } 14 | 15 | // PushRoom 推送房间 16 | func (s *RoomExtService) PushRoom(ctx context.Context, request *pb.PushRoomRequest) (*emptypb.Empty, error) { 17 | return &emptypb.Empty{}, App.Push(ctx, request) 18 | } 19 | 20 | type RoomIntService struct { 21 | pb.UnsafeRoomIntServiceServer 22 | } 23 | 24 | // SubscribeRoom 订阅房间 25 | func (s *RoomIntService) SubscribeRoom(ctx context.Context, request *pb.SubscribeRoomRequest) (*emptypb.Empty, error) { 26 | return &emptypb.Empty{}, App.SubscribeRoom(ctx, request) 27 | } 28 | 29 | // PushRoom 推送房间 30 | func (s *RoomIntService) PushRoom(ctx context.Context, request *pb.PushRoomRequest) (*emptypb.Empty, error) { 31 | return &emptypb.Empty{}, App.Push(ctx, request) 32 | } 33 | -------------------------------------------------------------------------------- /internal/logic/room/api_test.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/credentials/insecure" 10 | "google.golang.org/grpc/metadata" 11 | 12 | "gim/pkg/md" 13 | pb "gim/pkg/protocol/pb/logicpb" 14 | ) 15 | 16 | func getExtClient() pb.RoomExtServiceClient { 17 | conn, err := grpc.NewClient("127.0.0.1:8010", grpc.WithTransportCredentials(insecure.NewCredentials())) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return pb.NewRoomExtServiceClient(conn) 22 | } 23 | 24 | func TestRoomExtService_PushRoom(t *testing.T) { 25 | ctx := metadata.NewOutgoingContext(context.TODO(), metadata.New(map[string]string{ 26 | md.CtxUserID: "1", 27 | md.CtxDeviceID: "1", 28 | md.CtxToken: "0", 29 | })) 30 | 31 | reply, err := getExtClient().PushRoom(ctx, &pb.PushRoomRequest{ 32 | RoomId: 1, 33 | Code: 1000, 34 | Content: []byte("room msg"), 35 | SendTime: time.Now().Unix(), 36 | IsPersist: false, 37 | IsPriority: false, 38 | }) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | t.Log(reply) 43 | } 44 | -------------------------------------------------------------------------------- /internal/logic/room/app.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "time" 7 | 8 | "google.golang.org/protobuf/proto" 9 | 10 | "gim/pkg/grpclib/picker" 11 | "gim/pkg/mq" 12 | "gim/pkg/protocol/pb/connectpb" 13 | pb "gim/pkg/protocol/pb/logicpb" 14 | "gim/pkg/rpc" 15 | "gim/pkg/util" 16 | ) 17 | 18 | type app struct{} 19 | 20 | var App = new(app) 21 | 22 | // Push 推送房间消息 23 | func (s *app) Push(ctx context.Context, req *pb.PushRoomRequest) error { 24 | seq, err := SeqRepo.GetNextSeq(req.RoomId) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | msg := &pb.Message{ 30 | Code: req.Code, 31 | Content: req.Content, 32 | Seq: seq, 33 | CreatedAt: util.UnixMilliTime(time.Now()), 34 | } 35 | if req.IsPersist { 36 | err = s.addMessage(req.RoomId, msg) 37 | if err != nil { 38 | return err 39 | } 40 | } 41 | 42 | pushRoomMsg := connectpb.PushRoomMsg{ 43 | RoomId: req.RoomId, 44 | Message: msg, 45 | } 46 | buf, err := proto.Marshal(&pushRoomMsg) 47 | if err != nil { 48 | return err 49 | } 50 | var topicName = mq.PushRoomTopic 51 | if req.IsPriority { 52 | topicName = mq.PushRoomPriorityTopic 53 | } 54 | return mq.Publish(topicName, buf) 55 | } 56 | 57 | // SubscribeRoom 订阅房间 58 | func (s *app) SubscribeRoom(ctx context.Context, req *pb.SubscribeRoomRequest) error { 59 | if req.Seq == 0 { 60 | return nil 61 | } 62 | 63 | messages, err := MessageRepo.List(req.RoomId, req.Seq) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | var request connectpb.PushToDevicesRequest 69 | for i := range messages { 70 | request.DeviceMessageList = append(request.DeviceMessageList, &connectpb.DeviceMessage{ 71 | DeviceId: req.DeviceId, 72 | Message: messages[i], 73 | }) 74 | } 75 | 76 | _, err = rpc.GetConnectIntClient().PushToDevices(picker.ContextWithAddr(ctx, req.ConnAddr), &request) 77 | if err != nil { 78 | slog.Error("deliverMessage", "error", err) 79 | } 80 | return nil 81 | } 82 | 83 | func (s *app) addMessage(roomId uint64, msg *pb.Message) error { 84 | err := MessageRepo.Add(roomId, msg) 85 | if err != nil { 86 | return err 87 | } 88 | return s.delExpireMessage(roomId) 89 | } 90 | 91 | // DelExpireMessage 删除过期消息 92 | func (s *app) delExpireMessage(roomId uint64) error { 93 | var ( 94 | index int64 = 0 95 | stop bool 96 | min uint64 97 | max uint64 98 | ) 99 | 100 | for { 101 | msgs, err := MessageRepo.ListByIndex(roomId, index, index+20) 102 | if err != nil { 103 | return err 104 | } 105 | if len(msgs) == 0 { 106 | break 107 | } 108 | 109 | for _, v := range msgs { 110 | if v.CreatedAt > util.UnixMilliTime(time.Now().Add(-MessageExpireTime)) { 111 | stop = true 112 | break 113 | } 114 | 115 | if min == 0 { 116 | min = v.Seq 117 | } 118 | max = v.Seq 119 | } 120 | if stop { 121 | break 122 | } 123 | } 124 | 125 | return MessageRepo.DelBySeq(roomId, min, max) 126 | } 127 | -------------------------------------------------------------------------------- /internal/logic/room/message_repo.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/go-redis/redis" 9 | "google.golang.org/protobuf/proto" 10 | 11 | "gim/pkg/db" 12 | pb "gim/pkg/protocol/pb/logicpb" 13 | "gim/pkg/util" 14 | ) 15 | 16 | const MessageKey = "room_message:%d" 17 | 18 | const MessageExpireTime = 2 * time.Minute 19 | 20 | type messageRepo struct{} 21 | 22 | var MessageRepo = new(messageRepo) 23 | 24 | // Add 将消息添加到队列 25 | func (*messageRepo) Add(roomId uint64, msg *pb.Message) error { 26 | key := fmt.Sprintf(MessageKey, roomId) 27 | buf, err := proto.Marshal(msg) 28 | if err != nil { 29 | return err 30 | } 31 | _, err = db.RedisCli.ZAdd(key, redis.Z{ 32 | Score: float64(msg.Seq), 33 | Member: buf, 34 | }).Result() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | _, err = db.RedisCli.Expire(key, MessageExpireTime).Result() 40 | return err 41 | } 42 | 43 | // List 获取指定房间序列号大于seq的消息 44 | func (*messageRepo) List(roomId, seq uint64) ([]*pb.Message, error) { 45 | key := fmt.Sprintf(MessageKey, roomId) 46 | result, err := db.RedisCli.ZRangeByScore(key, redis.ZRangeBy{ 47 | Min: strconv.FormatUint(seq, 10), 48 | Max: "+inf", 49 | }).Result() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | var msgs []*pb.Message 55 | for i := range result { 56 | buf := util.Str2bytes(result[i]) 57 | var msg pb.Message 58 | err = proto.Unmarshal(buf, &msg) 59 | if err != nil { 60 | return nil, err 61 | } 62 | msgs = append(msgs, &msg) 63 | } 64 | return msgs, nil 65 | } 66 | 67 | func (*messageRepo) ListByIndex(roomId uint64, start, stop int64) ([]*pb.Message, error) { 68 | key := fmt.Sprintf(MessageKey, roomId) 69 | result, err := db.RedisCli.ZRange(key, start, stop).Result() 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | var msgs []*pb.Message 75 | for i := range result { 76 | buf := util.Str2bytes(result[i]) 77 | var msg pb.Message 78 | err = proto.Unmarshal(buf, &msg) 79 | if err != nil { 80 | return nil, err 81 | } 82 | msgs = append(msgs, &msg) 83 | } 84 | return msgs, nil 85 | } 86 | 87 | func (*messageRepo) DelBySeq(roomId uint64, min, max uint64) error { 88 | if min == 0 && max == 0 { 89 | return nil 90 | } 91 | key := fmt.Sprintf(MessageKey, roomId) 92 | _, err := db.RedisCli.ZRemRangeByScore(key, strconv.FormatUint(min, 10), strconv.FormatUint(max, 10)).Result() 93 | return err 94 | } 95 | -------------------------------------------------------------------------------- /internal/logic/room/seq_repo.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gim/pkg/db" 7 | ) 8 | 9 | const SeqKey = "room_seq:%d" 10 | 11 | type seqRepo struct{} 12 | 13 | var SeqRepo = new(seqRepo) 14 | 15 | func (*seqRepo) GetNextSeq(roomId uint64) (uint64, error) { 16 | num, err := db.RedisCli.Incr(fmt.Sprintf(SeqKey, roomId)).Result() 17 | if err != nil { 18 | return 0, err 19 | } 20 | return uint64(num), err 21 | } 22 | -------------------------------------------------------------------------------- /internal/user/api/user_ext.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/protobuf/types/known/emptypb" 7 | 8 | "gim/internal/user/app" 9 | "gim/pkg/md" 10 | pb "gim/pkg/protocol/pb/userpb" 11 | ) 12 | 13 | type UserExtService struct { 14 | pb.UnsafeUserExtServiceServer 15 | } 16 | 17 | func (s *UserExtService) SignIn(ctx context.Context, req *pb.SignInRequest) (*pb.SignInReply, error) { 18 | isNew, userId, token, err := app.AuthApp.SignIn(ctx, req.PhoneNumber, req.Code, req.DeviceId) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return &pb.SignInReply{ 23 | IsNew: isNew, 24 | UserId: userId, 25 | Token: token, 26 | }, nil 27 | } 28 | 29 | func (s *UserExtService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserReply, error) { 30 | userId, _, err := md.GetCtxData(ctx) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | user, err := app.UserApp.Get(ctx, userId) 36 | return &pb.GetUserReply{User: user}, err 37 | } 38 | 39 | func (s *UserExtService) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*emptypb.Empty, error) { 40 | userId, _, err := md.GetCtxData(ctx) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return new(emptypb.Empty), app.UserApp.Update(ctx, userId, req) 46 | } 47 | 48 | func (s *UserExtService) SearchUser(ctx context.Context, req *pb.SearchUserRequest) (*pb.SearchUserReply, error) { 49 | users, err := app.UserApp.Search(ctx, req.Key) 50 | return &pb.SearchUserReply{Users: users}, err 51 | } 52 | -------------------------------------------------------------------------------- /internal/user/api/user_ext_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials/insecure" 12 | "google.golang.org/grpc/metadata" 13 | 14 | pb "gim/pkg/protocol/pb/userpb" 15 | ) 16 | 17 | func getUserExtServiceClient() pb.UserExtServiceClient { 18 | conn, err := grpc.NewClient("127.0.0.1:8020", grpc.WithTransportCredentials(insecure.NewCredentials())) 19 | if err != nil { 20 | panic(err) 21 | } 22 | return pb.NewUserExtServiceClient(conn) 23 | } 24 | 25 | func getCtx() context.Context { 26 | token := "0" 27 | return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs( 28 | "user_id", "1", 29 | "device_id", "1", 30 | "token", token, 31 | "request_id", strconv.FormatInt(time.Now().UnixNano(), 10))) 32 | } 33 | 34 | func TestUserExtServer_SignIn(t *testing.T) { 35 | reply, err := getUserExtServiceClient().SignIn(context.TODO(), &pb.SignInRequest{ 36 | PhoneNumber: "22222222222", 37 | Code: "0", 38 | DeviceId: 2, 39 | }) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | t.Log(reply) 44 | } 45 | 46 | func TestUserExtServer_GetUser(t *testing.T) { 47 | reply, err := getUserExtServiceClient().GetUser(getCtx(), &pb.GetUserRequest{UserId: 1}) 48 | if err != nil { 49 | fmt.Println(err) 50 | } 51 | t.Log(reply) 52 | } 53 | 54 | func TestUserExtService_SearchUser(t *testing.T) { 55 | reply, err := getUserExtServiceClient().SearchUser(getCtx(), &pb.SearchUserRequest{Key: "1"}) 56 | if err != nil { 57 | fmt.Println(err) 58 | } 59 | t.Log(reply) 60 | } 61 | -------------------------------------------------------------------------------- /internal/user/api/user_int.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/protobuf/types/known/emptypb" 7 | 8 | "gim/internal/user/app" 9 | pb "gim/pkg/protocol/pb/userpb" 10 | ) 11 | 12 | type UserIntService struct { 13 | pb.UnsafeUserIntServiceServer 14 | } 15 | 16 | func (*UserIntService) Auth(ctx context.Context, req *pb.AuthRequest) (*emptypb.Empty, error) { 17 | return &emptypb.Empty{}, app.AuthApp.Auth(ctx, req.UserId, req.DeviceId, req.Token) 18 | } 19 | 20 | func (*UserIntService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserReply, error) { 21 | user, err := app.UserApp.Get(ctx, req.UserId) 22 | return &pb.GetUserReply{User: user}, err 23 | } 24 | 25 | func (*UserIntService) GetUsers(ctx context.Context, req *pb.GetUsersRequest) (*pb.GetUsersReply, error) { 26 | var userIds = make([]uint64, 0, len(req.UserIds)) 27 | for k := range req.UserIds { 28 | userIds = append(userIds, k) 29 | } 30 | 31 | users, err := app.UserApp.GetByIds(ctx, userIds) 32 | return &pb.GetUsersReply{Users: users}, err 33 | } 34 | -------------------------------------------------------------------------------- /internal/user/api/user_int_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/credentials/insecure" 8 | 9 | pb "gim/pkg/protocol/pb/userpb" 10 | ) 11 | 12 | func getUserIntClient() pb.UserIntServiceClient { 13 | conn, err := grpc.NewClient("127.0.0.1:8020", grpc.WithTransportCredentials(insecure.NewCredentials())) 14 | if err != nil { 15 | panic(err) 16 | } 17 | return pb.NewUserIntServiceClient(conn) 18 | } 19 | 20 | func TestUserIntServer_Auth(t *testing.T) { 21 | _, err := getUserIntClient().Auth(getCtx(), &pb.AuthRequest{ 22 | UserId: 2, 23 | DeviceId: 1, 24 | Token: "0", 25 | }) 26 | t.Log(err) 27 | } 28 | 29 | func TestUserIntServer_GetUsers(t *testing.T) { 30 | reply, err := getUserIntClient().GetUsers(getCtx(), &pb.GetUsersRequest{ 31 | UserIds: map[uint64]int32{1: 0, 2: 0, 3: 0}, 32 | }) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | for k, v := range reply.Users { 38 | t.Log(k, v) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/user/app/auth_app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/go-redis/redis" 9 | 10 | "gim/internal/user/domain" 11 | "gim/internal/user/repo" 12 | "gim/pkg/gerrors" 13 | "gim/pkg/protocol/pb/logicpb" 14 | "gim/pkg/rpc" 15 | ) 16 | 17 | type authApp struct{} 18 | 19 | var AuthApp = new(authApp) 20 | 21 | // SignIn 长连接登录 22 | func (*authApp) SignIn(ctx context.Context, phoneNumber, code string, deviceId uint64) (bool, uint64, string, error) { 23 | if !verify(phoneNumber, code) { 24 | return false, 0, "", gerrors.ErrBadCode 25 | } 26 | 27 | user, err := repo.UserRepo.GetByPhoneNumber(phoneNumber) 28 | if err != nil && !errors.Is(err, gerrors.ErrUserNotFound) { 29 | return false, 0, "", err 30 | } 31 | 32 | var isNew = false 33 | if errors.Is(err, gerrors.ErrUserNotFound) { 34 | user = &domain.User{ 35 | PhoneNumber: phoneNumber, 36 | CreatedAt: time.Now(), 37 | UpdatedAt: time.Now(), 38 | } 39 | err := repo.UserRepo.Save(user) 40 | if err != nil { 41 | return false, 0, "", err 42 | } 43 | isNew = true 44 | } 45 | 46 | resp, err := rpc.GetDeviceIntClient().GetDevice(ctx, &logicpb.GetDeviceRequest{DeviceId: deviceId}) 47 | if err != nil { 48 | return false, 0, "", err 49 | } 50 | 51 | // 方便测试 52 | token := "0" 53 | //token := util.RandString(40) 54 | err = repo.AuthRepo.Set(user.Id, resp.Device.DeviceId, domain.Device{ 55 | Type: resp.Device.Type, 56 | Token: token, 57 | Expire: time.Now().AddDate(0, 3, 0).Unix(), 58 | }) 59 | if err != nil { 60 | return false, 0, "", err 61 | } 62 | 63 | return isNew, user.Id, token, nil 64 | } 65 | 66 | func verify(phoneNumber, code string) bool { 67 | // 假装他成功了 68 | return true 69 | } 70 | 71 | // Auth 验证用户是否登录 72 | func (*authApp) Auth(ctx context.Context, userId, deviceId uint64, token string) error { 73 | device, err := repo.AuthRepo.Get(userId, deviceId) 74 | if errors.Is(err, redis.Nil) { 75 | return gerrors.ErrUnauthorized 76 | } 77 | if err != nil { 78 | return err 79 | } 80 | 81 | if device.Expire < time.Now().Unix() { 82 | return gerrors.ErrUnauthorized 83 | } 84 | if device.Token != token { 85 | return gerrors.ErrUnauthorized 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/user/app/user_app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "gim/internal/user/repo" 8 | pb "gim/pkg/protocol/pb/userpb" 9 | ) 10 | 11 | type userApp struct{} 12 | 13 | var UserApp = new(userApp) 14 | 15 | func (*userApp) Get(ctx context.Context, userId uint64) (*pb.User, error) { 16 | user, err := repo.UserRepo.Get(userId) 17 | return user.ToProto(), err 18 | } 19 | 20 | func (*userApp) Update(ctx context.Context, userId uint64, req *pb.UpdateUserRequest) error { 21 | u, err := repo.UserRepo.Get(userId) 22 | if err != nil { 23 | return err 24 | } 25 | if u == nil { 26 | return nil 27 | } 28 | 29 | u.Nickname = req.Nickname 30 | u.Sex = req.Sex 31 | u.AvatarUrl = req.AvatarUrl 32 | u.Extra = req.Extra 33 | u.UpdatedAt = time.Now() 34 | 35 | return repo.UserRepo.Save(u) 36 | } 37 | 38 | func (*userApp) GetByIds(ctx context.Context, userIds []uint64) (map[uint64]*pb.User, error) { 39 | users, err := repo.UserRepo.GetByIds(userIds) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | pbUsers := make(map[uint64]*pb.User, len(users)) 45 | for i := range users { 46 | pbUsers[users[i].Id] = users[i].ToProto() 47 | } 48 | return pbUsers, nil 49 | } 50 | 51 | func (*userApp) Search(ctx context.Context, key string) ([]*pb.User, error) { 52 | users, err := repo.UserRepo.Search(key) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | pbUsers := make([]*pb.User, len(users)) 58 | for i, v := range users { 59 | pbUsers[i] = v.ToProto() 60 | } 61 | return pbUsers, nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/user/domain/device.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import pb "gim/pkg/protocol/pb/logicpb" 4 | 5 | type Device struct { 6 | Type pb.DeviceType // 设备类型 7 | Token string // token 8 | Expire int64 // 过期时间 9 | } 10 | -------------------------------------------------------------------------------- /internal/user/domain/user.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | pb "gim/pkg/protocol/pb/userpb" 7 | ) 8 | 9 | // User 账户 10 | type User struct { 11 | Id uint64 // 用户id 12 | CreatedAt time.Time // 创建时间 13 | UpdatedAt time.Time // 更新时间 14 | PhoneNumber string // 手机号 15 | Nickname string // 昵称 16 | Sex int32 // 性别,1:男;2:女 17 | AvatarUrl string // 用户头像 18 | Extra string // 附加属性 19 | } 20 | 21 | func (u *User) ToProto() *pb.User { 22 | if u == nil { 23 | return nil 24 | } 25 | 26 | return &pb.User{ 27 | UserId: u.Id, 28 | Nickname: u.Nickname, 29 | Sex: u.Sex, 30 | AvatarUrl: u.AvatarUrl, 31 | Extra: u.Extra, 32 | CreateTime: u.CreatedAt.Unix(), 33 | UpdateTime: u.UpdatedAt.Unix(), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/user/repo/auth_repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "gim/internal/user/domain" 9 | "gim/pkg/db" 10 | "gim/pkg/util" 11 | ) 12 | 13 | const AuthKey = "auth:%d" 14 | 15 | type authRepo struct{} 16 | 17 | var AuthRepo = new(authRepo) 18 | 19 | func (*authRepo) Get(userId, deviceId uint64) (*domain.Device, error) { 20 | key := fmt.Sprintf(AuthKey, userId) 21 | bytes, err := db.RedisCli.HGet(key, strconv.FormatUint(deviceId, 10)).Bytes() 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | var device domain.Device 27 | err = json.Unmarshal(bytes, &device) 28 | return &device, err 29 | } 30 | 31 | func (*authRepo) Set(userId, deviceId uint64, device domain.Device) error { 32 | bytes, err := json.Marshal(device) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | key := fmt.Sprintf(AuthKey, userId) 38 | _, err = db.RedisCli.HSet(key, strconv.FormatUint(deviceId, 10), bytes).Result() 39 | return err 40 | } 41 | 42 | func (*authRepo) GetAll(userId uint64) (map[uint64]domain.Device, error) { 43 | key := fmt.Sprintf(AuthKey, userId) 44 | result, err := db.RedisCli.HGetAll(key).Result() 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | var devices = make(map[uint64]domain.Device, len(result)) 50 | 51 | for k, v := range result { 52 | deviceId, err := strconv.ParseUint(k, 10, 64) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | var device domain.Device 58 | err = json.Unmarshal(util.Str2bytes(v), &device) 59 | if err != nil { 60 | return nil, err 61 | } 62 | devices[deviceId] = device 63 | } 64 | return devices, nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/user/repo/user_repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | 8 | "gim/internal/user/domain" 9 | "gim/pkg/db" 10 | "gim/pkg/gerrors" 11 | ) 12 | 13 | type userRepo struct{} 14 | 15 | var UserRepo = new(userRepo) 16 | 17 | // Get 获取单个用户 18 | func (*userRepo) Get(userId uint64) (*domain.User, error) { 19 | var user = domain.User{Id: userId} 20 | err := db.DB.First(&user).Error 21 | if errors.Is(err, gorm.ErrRecordNotFound) { 22 | return nil, gerrors.ErrUserNotFound 23 | } 24 | return &user, err 25 | } 26 | 27 | func (*userRepo) GetByPhoneNumber(phoneNumber string) (*domain.User, error) { 28 | var user domain.User 29 | err := db.DB.First(&user, "phone_number = ?", phoneNumber).Error 30 | if errors.Is(err, gorm.ErrRecordNotFound) { 31 | return nil, gerrors.ErrUserNotFound 32 | } 33 | return &user, err 34 | } 35 | 36 | // GetByIds 获取多个用户 37 | func (*userRepo) GetByIds(userIds []uint64) ([]domain.User, error) { 38 | var users []domain.User 39 | err := db.DB.Find(&users, "id in (?)", userIds).Error 40 | return users, err 41 | } 42 | 43 | // Search 搜索用户 44 | func (*userRepo) Search(key string) ([]domain.User, error) { 45 | var users []domain.User 46 | key = "%" + key + "%" 47 | err := db.DB.Where("phone_number like ? or nickname like ?", key, key).Find(&users).Error 48 | if err != nil { 49 | return nil, err 50 | } 51 | return users, nil 52 | } 53 | 54 | // Save 保存用户 55 | func (*userRepo) Save(user *domain.User) error { 56 | return db.DB.Save(user).Error 57 | } 58 | -------------------------------------------------------------------------------- /pkg/codec/uvarint.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | ) 8 | 9 | func GetUvarintLen(x uint64) int { 10 | i := 0 11 | for x >= 0x80 { 12 | x >>= 7 13 | i++ 14 | } 15 | return i + 1 16 | } 17 | 18 | func Encode(in []byte) []byte { 19 | length := GetUvarintLen(uint64(len(in))) 20 | buf := make([]byte, length+len(in)) 21 | 22 | binary.PutUvarint(buf, uint64(len(in))) 23 | copy(buf[length:], in) 24 | return buf 25 | } 26 | 27 | func Decode(reader *bufio.Reader) ([]byte, error) { 28 | length, err := binary.ReadUvarint(reader) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | buf := make([]byte, length) 34 | n, err := reader.Read(buf) 35 | if err != nil { 36 | return nil, err 37 | } 38 | if n != int(length) { 39 | return nil, errors.New("decode invalid length") 40 | } 41 | return buf, nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/go-redis/redis" 7 | "gorm.io/driver/mysql" 8 | "gorm.io/gorm" 9 | "gorm.io/gorm/schema" 10 | 11 | "gim/config" 12 | "gim/pkg/util" 13 | ) 14 | 15 | var ( 16 | DB *gorm.DB 17 | RedisCli *redis.Client 18 | RedisUtil *util.RedisUtil 19 | ) 20 | 21 | func init() { 22 | InitMysql(config.Config.MySQL) 23 | InitRedis(config.Config.RedisHost, config.Config.RedisPassword) 24 | } 25 | 26 | // InitMysql 初始化MySQL 27 | func InitMysql(dsn string) { 28 | db, err := gorm.Open( 29 | mysql.Open(dsn), 30 | &gorm.Config{ 31 | NamingStrategy: schema.NamingStrategy{ 32 | SingularTable: true, 33 | }, 34 | }) 35 | if err != nil { 36 | slog.Error("open db error", "error", err, slog.String("dsn", dsn)) 37 | panic(err) 38 | } 39 | 40 | if config.ENV == config.EnvLocal { 41 | db = db.Debug() 42 | } 43 | DB = db 44 | } 45 | 46 | // InitRedis 初始化Redis 47 | func InitRedis(addr, password string) { 48 | RedisCli = redis.NewClient(&redis.Options{ 49 | Addr: addr, 50 | DB: 0, 51 | Password: password, 52 | }) 53 | 54 | _, err := RedisCli.Ping().Result() 55 | if err != nil { 56 | slog.Error("redis ping error", "error", err) 57 | panic(err) 58 | } 59 | 60 | RedisUtil = util.NewRedisUtil(RedisCli) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/gerrors/connect.go: -------------------------------------------------------------------------------- 1 | package gerrors 2 | 3 | var ( 4 | ErrConnNotFound = newError(10101, "连接找不到") 5 | ErrConnDeviceIdNotEqual = newError(10102, "连接设备ID不相等") 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/gerrors/define.go: -------------------------------------------------------------------------------- 1 | package gerrors 2 | 3 | import ( 4 | "google.golang.org/grpc/codes" 5 | "google.golang.org/grpc/status" 6 | ) 7 | 8 | var ( 9 | ErrUnknown = status.New(codes.Unknown, "服务器异常").Err() // 服务器未知错误 10 | ErrUnauthorized = newError(10000, "请重新登录") 11 | ErrBadRequest = newError(10001, "请求参数错误") 12 | ) 13 | 14 | func newError(code int, message string) error { 15 | return status.New(codes.Code(code), message).Err() 16 | } 17 | -------------------------------------------------------------------------------- /pkg/gerrors/error.go: -------------------------------------------------------------------------------- 1 | package gerrors 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/status" 9 | 10 | "gim/pkg/util" 11 | ) 12 | 13 | const TypeUrlStack = "type_url_stack" 14 | 15 | func GetErrorStack(s *status.Status) string { 16 | pbs := s.Proto() 17 | for i := range pbs.Details { 18 | if pbs.Details[i].TypeUrl == TypeUrlStack { 19 | return util.Bytes2str(pbs.Details[i].Value) 20 | } 21 | } 22 | return "" 23 | } 24 | 25 | func LogPanic(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, err *error) { 26 | p := recover() 27 | if p != nil { 28 | slog.Error("panic", "info", info, "ctx", ctx, "req", req, "panic", p, 29 | "stack", util.GetStackInfo()) 30 | *err = ErrUnknown 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/gerrors/logic.go: -------------------------------------------------------------------------------- 1 | package gerrors 2 | 3 | var ( 4 | ErrDeviceNotFound = newError(10200, "设备不存在") 5 | ErrFriendNotFound = newError(10210, "好友关系不存在") 6 | ErrAlreadyIsFriend = newError(10211, "对方已经是好友了") 7 | ErrGroupNotFound = newError(10220, "群组不存在") 8 | ErrNotInGroup = newError(10201, "用户没有在群组中") 9 | ErrGroupUserNotFound = newError(10202, "群组成员不存在") 10 | ErrUserAlreadyInGroup = newError(10203, "用户已经在群组中") 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/gerrors/user.go: -------------------------------------------------------------------------------- 1 | package gerrors 2 | 3 | var ( 4 | ErrBadCode = newError(10300, "验证码错误") 5 | ErrUserNotFound = newError(10301, "用户找不到") 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/grpclib/picker/addr_picker.go: -------------------------------------------------------------------------------- 1 | package picker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log/slog" 7 | 8 | "google.golang.org/grpc/balancer" 9 | "google.golang.org/grpc/balancer/base" 10 | ) 11 | 12 | // AddrPickerName 实现指定地址调用的RPC调用 13 | const AddrPickerName = "addr" 14 | 15 | type addrKey struct{} 16 | 17 | var ErrNoSubConnSelect = errors.New("no sub conn select") 18 | 19 | func init() { 20 | balancer.Register(newBuilder()) 21 | } 22 | 23 | func ContextWithAddr(ctx context.Context, addr string) context.Context { 24 | return context.WithValue(ctx, addrKey{}, addr) 25 | } 26 | 27 | type addrPickerBuilder struct{} 28 | 29 | func newBuilder() balancer.Builder { 30 | return base.NewBalancerBuilder(AddrPickerName, &addrPickerBuilder{}, base.Config{HealthCheck: true}) 31 | } 32 | 33 | func (*addrPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker { 34 | if len(info.ReadySCs) == 0 { 35 | return base.NewErrPicker(balancer.ErrNoSubConnAvailable) 36 | } 37 | 38 | subConns := make(map[string]balancer.SubConn, len(info.ReadySCs)) 39 | for k, sc := range info.ReadySCs { 40 | subConns[sc.Address.Addr] = k 41 | } 42 | return &addrPicker{ 43 | subConnes: subConns, 44 | } 45 | } 46 | 47 | type addrPicker struct { 48 | subConnes map[string]balancer.SubConn 49 | } 50 | 51 | func (p *addrPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { 52 | pr := balancer.PickResult{} 53 | 54 | address := info.Ctx.Value(addrKey{}).(string) 55 | sc, ok := p.subConnes[address] 56 | if !ok { 57 | slog.Error("Pick error", "address", address, "subConnes", p.subConnes) 58 | return pr, ErrNoSubConnSelect 59 | } 60 | pr.SubConn = sc 61 | return pr, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/grpclib/resolver/addrs/addrs_resolver.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | import ( 4 | "strings" 5 | 6 | "google.golang.org/grpc/resolver" 7 | ) 8 | 9 | // 实现多个IP地址解析,比如,addrs:///127.0.0.1:50000,127.0.0.1:50001 10 | func init() { 11 | RegisterResolver() 12 | } 13 | 14 | func RegisterResolver() { 15 | resolver.Register(NewAddrsBuilder()) 16 | } 17 | 18 | type addrsBuilder struct{} 19 | 20 | func NewAddrsBuilder() resolver.Builder { 21 | return &addrsBuilder{} 22 | } 23 | 24 | func (b *addrsBuilder) Build(target resolver.Target, clientConn resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { 25 | ips := strings.Split(target.Endpoint(), ",") 26 | 27 | state := resolver.State{ 28 | Addresses: getAddrs(ips), 29 | } 30 | _ = clientConn.UpdateState(state) 31 | return &addrsResolver{ 32 | addrs: ips, 33 | clientConn: clientConn, 34 | }, nil 35 | } 36 | 37 | func (b *addrsBuilder) Scheme() string { 38 | return "addrs" 39 | } 40 | 41 | type addrsResolver struct { 42 | addrs []string 43 | clientConn resolver.ClientConn 44 | } 45 | 46 | func (r *addrsResolver) ResolveNow(opt resolver.ResolveNowOptions) { 47 | state := resolver.State{ 48 | Addresses: getAddrs(r.addrs), 49 | } 50 | _ = r.clientConn.UpdateState(state) 51 | } 52 | 53 | func (r *addrsResolver) Close() {} 54 | 55 | func getAddrs(ips []string) []resolver.Address { 56 | addresses := make([]resolver.Address, len(ips)) 57 | for i := range ips { 58 | addresses[i].Addr = ips[i] 59 | } 60 | return addresses 61 | } 62 | -------------------------------------------------------------------------------- /pkg/grpclib/resolver/k8s/k8s_resolver.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "sort" 9 | "strings" 10 | "time" 11 | 12 | "google.golang.org/grpc/resolver" 13 | v1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/client-go/kubernetes" 16 | corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 17 | "k8s.io/client-go/rest" 18 | ) 19 | 20 | var k8sClientSet *kubernetes.Clientset 21 | 22 | func GetK8sClient() (*kubernetes.Clientset, error) { 23 | if k8sClientSet == nil { 24 | config, err := rest.InClusterConfig() 25 | if err != nil { 26 | return nil, err 27 | } 28 | k8sClientSet, err = kubernetes.NewForConfig(config) 29 | if err != nil { 30 | return nil, err 31 | } 32 | } 33 | return k8sClientSet, nil 34 | } 35 | 36 | // 实现k8s地址解析,根据k8s的service的endpoints解析 比如,k8s:///namespace.server:port 37 | func init() { 38 | resolver.Register(&k8sBuilder{}) 39 | } 40 | 41 | func GetK8STarget(namespace, server, port string) string { 42 | return fmt.Sprintf("k8s:///%s.%s:%s", namespace, server, port) 43 | } 44 | 45 | type k8sBuilder struct{} 46 | 47 | func (b *k8sBuilder) Build(target resolver.Target, clientConn resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { 48 | return newK8sResolver(target, clientConn) 49 | } 50 | 51 | func (b *k8sBuilder) Scheme() string { 52 | return "k8s" 53 | } 54 | 55 | // k8sResolver k8s地址解析器 56 | type k8sResolver struct { 57 | log *slog.Logger 58 | clientConn resolver.ClientConn 59 | endpointsClient corev1.EndpointsInterface 60 | service string 61 | 62 | cancel context.CancelFunc 63 | 64 | ips []string 65 | port string 66 | } 67 | 68 | func newK8sResolver(target resolver.Target, clientConn resolver.ClientConn) (*k8sResolver, error) { 69 | log := slog.With("target", target.Endpoint()) 70 | log.Info("k8s resolver build") 71 | namespace, service, port, err := parseTarget(target) 72 | if err != nil { 73 | log.Error("k8s resolver error", "error", err) 74 | return nil, err 75 | } 76 | 77 | k8sClient, err := GetK8sClient() 78 | if err != nil { 79 | log.Error("k8s resolver error", "error", err) 80 | return nil, err 81 | } 82 | 83 | ctx, cancel := context.WithCancel(context.Background()) 84 | client := k8sClient.CoreV1().Endpoints(namespace) 85 | k8sResolver := &k8sResolver{ 86 | log: log, 87 | clientConn: clientConn, 88 | endpointsClient: client, 89 | service: service, 90 | cancel: cancel, 91 | port: port, 92 | } 93 | err = k8sResolver.updateState(true) 94 | if err != nil { 95 | log.Error("k8s resolver error", "error", err) 96 | return nil, err 97 | } 98 | 99 | ticker := time.NewTicker(time.Second) 100 | // 监听变化 101 | go func() { 102 | for { 103 | select { 104 | case <-ctx.Done(): 105 | return 106 | case <-ticker.C: 107 | _ = k8sResolver.updateState(false) 108 | } 109 | } 110 | }() 111 | return k8sResolver, nil 112 | } 113 | 114 | // ResolveNow grpc感知到连接异常,会做通知,观察日志得知 115 | func (r *k8sResolver) ResolveNow(opt resolver.ResolveNowOptions) { 116 | r.log.Info("k8s resolver resolveNow") 117 | } 118 | 119 | func (r *k8sResolver) Close() { 120 | r.log.Info("k8s resolver close") 121 | r.cancel() 122 | } 123 | 124 | // updateState 更新地址列表 125 | func (r *k8sResolver) updateState(isFromNew bool) error { 126 | endpoints, err := r.endpointsClient.Get(context.TODO(), r.service, metav1.GetOptions{}) 127 | if err != nil { 128 | r.log.Error("k8s resolver error", "error", err) 129 | return err 130 | } 131 | newIPs := getIPs(endpoints) 132 | if len(newIPs) == 0 { 133 | return nil 134 | } 135 | if !isFromNew && isEqualIPs(r.ips, newIPs) { 136 | return nil 137 | } 138 | r.ips = newIPs 139 | 140 | addresses := make([]resolver.Address, 0, len(r.ips)) 141 | for _, ip := range r.ips { 142 | addresses = append(addresses, resolver.Address{ 143 | Addr: ip + ":" + r.port, 144 | }) 145 | } 146 | state := resolver.State{ 147 | Addresses: addresses, 148 | } 149 | r.log.Info("k8s resolver updateState", "is_from_new", isFromNew, "service", r.service, "addresses", addresses) 150 | // 这里地址数量不能为0,为0会返回错误 151 | err = r.clientConn.UpdateState(state) 152 | if err != nil { 153 | r.log.Error("k8s resolver error", "error", err) 154 | return err 155 | } 156 | return nil 157 | } 158 | 159 | // parseTarget 对grpc的Endpoint进行解析,格式必须是:k8s:///namespace.server:port 160 | func parseTarget(target resolver.Target) (namespace string, service string, port string, err error) { 161 | namespaceAndServerPort := strings.Split(target.Endpoint(), ".") 162 | if len(namespaceAndServerPort) != 2 { 163 | err = errors.New("endpoint must is namespace.server:port") 164 | return 165 | } 166 | namespace = namespaceAndServerPort[0] 167 | serverAndPort := strings.Split(namespaceAndServerPort[1], ":") 168 | if len(serverAndPort) != 2 { 169 | err = errors.New("endpoint must is namespace.server:port") 170 | return 171 | } 172 | service = serverAndPort[0] 173 | port = serverAndPort[1] 174 | return 175 | } 176 | 177 | // isEqualIPs 判断两个地址列表是否相等 178 | func isEqualIPs(s1, s2 []string) bool { 179 | if len(s1) != len(s2) { 180 | return false 181 | } 182 | 183 | sort.Strings(s1) 184 | sort.Strings(s2) 185 | for i := range s1 { 186 | if s1[i] != s2[i] { 187 | return false 188 | } 189 | } 190 | return true 191 | } 192 | 193 | // getIPs 获取EndpointSlice里面的IP列表 194 | func getIPs(endpoints *v1.Endpoints) []string { 195 | ips := make([]string, 0, 10) 196 | if len(endpoints.Subsets) <= 0 { 197 | return ips 198 | } 199 | 200 | for _, address := range endpoints.Subsets[0].Addresses { 201 | ips = append(ips, address.IP) 202 | } 203 | return ips 204 | } 205 | -------------------------------------------------------------------------------- /pkg/grpclib/resolver/k8s/k8s_resolver_test.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "testing" 5 | 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/credentials/insecure" 8 | ) 9 | 10 | func TestClient(t *testing.T) { 11 | _, err := grpc.NewClient("172.18.0.2:8000", grpc.WithTransportCredentials(insecure.NewCredentials())) 12 | if err != nil { 13 | panic(err) 14 | } 15 | } 16 | 17 | func Test_isEqualIPs(t *testing.T) { 18 | type args struct { 19 | s1 []string 20 | s2 []string 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | want bool 26 | }{ 27 | { 28 | name: "", 29 | args: args{s1: []string{"1", "2"}, s2: []string{"2", "1"}}, 30 | want: true, 31 | }, 32 | { 33 | name: "", 34 | args: args{s1: []string{"1", "2"}, s2: []string{"1", "2"}}, 35 | want: true, 36 | }, 37 | { 38 | name: "", 39 | args: args{s1: []string{"1", "2"}, s2: []string{"1"}}, 40 | want: false, 41 | }, 42 | } 43 | 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | if got := isEqualIPs(tt.args.s1, tt.args.s2); got != tt.want { 47 | t.Errorf("isEqual() = %v, want %v", got, tt.want) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/interceptor/interceptor.go: -------------------------------------------------------------------------------- 1 | package interceptor 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "strings" 7 | 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/metadata" 10 | "google.golang.org/grpc/status" 11 | 12 | "gim/pkg/gerrors" 13 | "gim/pkg/md" 14 | "gim/pkg/protocol/pb/userpb" 15 | "gim/pkg/rpc" 16 | ) 17 | 18 | // NewInterceptor 生成GRPC过滤器 19 | func NewInterceptor(urlWhitelist map[string]struct{}) grpc.UnaryServerInterceptor { 20 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (reply interface{}, err error) { 21 | defer gerrors.LogPanic(ctx, req, info, &err) 22 | md, _ := metadata.FromIncomingContext(ctx) 23 | logger := slog.With("method", info.FullMethod, "md", md, "request", req, "reply", reply) 24 | 25 | reply, err = handleWithAuth(ctx, req, info, handler, urlWhitelist) 26 | 27 | s, _ := status.FromError(err) 28 | if s.Code() != 0 && s.Code() < 10000 { 29 | logger.Error("interceptor", "error", err, "stack", gerrors.GetErrorStack(s)) 30 | } 31 | logger.Debug("interceptor", "error", err) 32 | return 33 | } 34 | } 35 | 36 | // handleWithAuth 处理鉴权逻辑 37 | func handleWithAuth(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, urlWhitelist map[string]struct{}) (interface{}, error) { 38 | serverName := strings.Split(info.FullMethod, "/")[1] 39 | if !strings.HasSuffix(serverName, "IntService") { 40 | if _, ok := urlWhitelist[info.FullMethod]; !ok { 41 | userId, deviceId, err := md.GetCtxData(ctx) 42 | if err != nil { 43 | return nil, err 44 | } 45 | token := md.GetCtxToken(ctx) 46 | 47 | _, err = rpc.GetUserIntClient().Auth(ctx, &userpb.AuthRequest{ 48 | UserId: userId, 49 | DeviceId: deviceId, 50 | Token: token, 51 | }) 52 | 53 | if err != nil { 54 | return nil, err 55 | } 56 | } 57 | } 58 | return handler(ctx, req) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/k8sutil/k8sutil.go: -------------------------------------------------------------------------------- 1 | package k8sutil 2 | 3 | import ( 4 | "k8s.io/client-go/kubernetes" 5 | "k8s.io/client-go/rest" 6 | ) 7 | 8 | func GetK8sClient() (*kubernetes.Clientset, error) { 9 | config, err := rest.InClusterConfig() 10 | if err != nil { 11 | return nil, err 12 | } 13 | k8sClient, err := kubernetes.NewForConfig(config) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return k8sClient, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/local/local.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "google.golang.org/grpc" 5 | "google.golang.org/grpc/credentials/insecure" 6 | 7 | "gim/config" 8 | "gim/pkg/protocol/pb/connectpb" 9 | "gim/pkg/protocol/pb/logicpb" 10 | "gim/pkg/protocol/pb/userpb" 11 | ) 12 | 13 | func Init() { 14 | config.Config.ConnectIntClientBuilder = func() connectpb.ConnectIntServiceClient { 15 | conn, err := grpc.NewClient("127.0.0.1:8000", grpc.WithTransportCredentials(insecure.NewCredentials())) 16 | if err != nil { 17 | panic(err) 18 | } 19 | return connectpb.NewConnectIntServiceClient(conn) 20 | } 21 | 22 | logicConn, err := grpc.NewClient("127.0.0.1:8010", grpc.WithTransportCredentials(insecure.NewCredentials())) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | config.Config.DeviceIntClientBuilder = func() logicpb.DeviceIntServiceClient { 28 | return logicpb.NewDeviceIntServiceClient(logicConn) 29 | } 30 | 31 | config.Config.MessageIntClientBuilder = func() logicpb.MessageIntServiceClient { 32 | return logicpb.NewMessageIntServiceClient(logicConn) 33 | } 34 | 35 | config.Config.RoomIntClientBuilder = func() logicpb.RoomIntServiceClient { 36 | return logicpb.NewRoomIntServiceClient(logicConn) 37 | } 38 | 39 | config.Config.UserIntClientBuilder = func() userpb.UserIntServiceClient { 40 | conn, err := grpc.NewClient("127.0.0.1:8020", grpc.WithTransportCredentials(insecure.NewCredentials())) 41 | if err != nil { 42 | panic(err) 43 | } 44 | return userpb.NewUserIntServiceClient(conn) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log/slog" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "gopkg.in/natefinch/lumberjack.v2" 12 | 13 | "gim/config" 14 | ) 15 | 16 | func Init(directory string) { 17 | var writer io.Writer 18 | 19 | logFile := config.Config.LogFile(directory) 20 | if logFile == "" { 21 | writer = os.Stdout 22 | } else { 23 | writer = &lumberjack.Logger{ 24 | Filename: fmt.Sprintf("/data/log/%s/log.log", directory), 25 | MaxSize: 100, // 单个文件大小megabytes 26 | MaxBackups: 30, // 最大备份数量 27 | MaxAge: 30, // 保存天数 28 | LocalTime: true, 29 | } 30 | } 31 | 32 | options := &slog.HandlerOptions{ 33 | AddSource: true, 34 | Level: config.Config.LogLevel, 35 | ReplaceAttr: replaceAttr, 36 | } 37 | slog.SetDefault(slog.New(slog.NewJSONHandler(writer, options))) 38 | slog.Info("slog init") 39 | } 40 | 41 | func replaceAttr(groups []string, a slog.Attr) slog.Attr { 42 | switch a.Key { 43 | case "time": 44 | a.Key = "ts" 45 | a.Value = slog.StringValue(a.Value.Time().Format("2006-01-02 15:04:05.000")) 46 | case "level": 47 | a.Value = slog.StringValue(strings.ToLower(a.Value.String())) 48 | case "source": 49 | source := a.Value.Any().(*slog.Source) 50 | a.Value = slog.StringValue(getShortSource(source)) 51 | } 52 | return a 53 | } 54 | 55 | func getShortSource(source *slog.Source) string { 56 | index := strings.LastIndex(source.File, "/") 57 | if index != -1 { 58 | index = strings.LastIndex(source.File[0:index], "/") 59 | } 60 | return strings.ReplaceAll(source.File[index+1:], ".go", "") + ":" + strconv.Itoa(source.Line) 61 | } 62 | 63 | func Error(err error) slog.Attr { 64 | if err == nil { 65 | return slog.Attr{} 66 | } 67 | return slog.Attr{ 68 | Key: "error", 69 | Value: slog.StringValue(err.Error()), 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/md/context.go: -------------------------------------------------------------------------------- 1 | package md 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | "google.golang.org/grpc/metadata" 8 | 9 | "gim/pkg/gerrors" 10 | ) 11 | 12 | const ( 13 | CtxUserID = "user_id" 14 | CtxDeviceID = "device_id" 15 | CtxToken = "token" 16 | CtxRequestID = "request_id" 17 | ) 18 | 19 | func ContextWithRequestId(ctx context.Context, requestId int64) context.Context { 20 | return metadata.NewOutgoingContext(ctx, metadata.Pairs(CtxRequestID, strconv.FormatInt(requestId, 10))) 21 | } 22 | 23 | func Get(ctx context.Context, key string) string { 24 | md, ok := metadata.FromIncomingContext(ctx) 25 | if !ok { 26 | return "" 27 | } 28 | 29 | values, ok := md[key] 30 | if !ok || len(values) == 0 { 31 | return "" 32 | } 33 | return values[0] 34 | } 35 | 36 | // GetCtxRequestId 获取ctx的app_id 37 | func GetCtxRequestId(ctx context.Context) int64 { 38 | requestIdStr := Get(ctx, CtxRequestID) 39 | requestId, err := strconv.ParseInt(requestIdStr, 10, 64) 40 | if err != nil { 41 | return 0 42 | } 43 | return requestId 44 | } 45 | 46 | // GetCtxData 获取ctx的用户数据,依次返回user_id,device_id 47 | func GetCtxData(ctx context.Context) (uint64, uint64, error) { 48 | var ( 49 | userId uint64 50 | deviceId uint64 51 | err error 52 | ) 53 | 54 | userIdStr := Get(ctx, CtxUserID) 55 | userId, err = strconv.ParseUint(userIdStr, 10, 64) 56 | if err != nil { 57 | return 0, 0, gerrors.ErrUnauthorized 58 | } 59 | 60 | deviceIdStr := Get(ctx, CtxDeviceID) 61 | deviceId, err = strconv.ParseUint(deviceIdStr, 10, 64) 62 | if err != nil { 63 | return 0, 0, gerrors.ErrUnauthorized 64 | } 65 | return userId, deviceId, nil 66 | } 67 | 68 | // GetCtxToken 获取ctx的token 69 | func GetCtxToken(ctx context.Context) string { 70 | return Get(ctx, CtxToken) 71 | } 72 | 73 | // NewAndCopyRequestId 创建一个context,并且复制RequestId 74 | func NewAndCopyRequestId(ctx context.Context) context.Context { 75 | newCtx := context.TODO() 76 | md, ok := metadata.FromIncomingContext(ctx) 77 | if !ok { 78 | return newCtx 79 | } 80 | 81 | requestIds, ok := md[CtxRequestID] 82 | if !ok && len(requestIds) == 0 { 83 | return newCtx 84 | } 85 | return metadata.NewOutgoingContext(newCtx, metadata.Pairs(CtxRequestID, requestIds[0])) 86 | } 87 | -------------------------------------------------------------------------------- /pkg/mq/mq.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "gim/pkg/db" 5 | ) 6 | 7 | const ( 8 | PushRoomTopic = "push_room_topic" // 房间消息队列 9 | PushRoomPriorityTopic = "push_room_priority_topic" // 房间优先级消息队列 10 | PushAllTopic = "push_all_topic" // 全服消息队列 11 | ) 12 | 13 | func Publish(topic string, bytes []byte) error { 14 | _, err := db.RedisCli.Publish(topic, bytes).Result() 15 | return err 16 | } 17 | -------------------------------------------------------------------------------- /pkg/protocol/pb/connectpb/connect.int_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.5.1 4 | // - protoc v5.29.3 5 | // source: connect/connect.int.proto 6 | 7 | package connectpb 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | emptypb "google.golang.org/protobuf/types/known/emptypb" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.64.0 or later. 20 | const _ = grpc.SupportPackageIsVersion9 21 | 22 | const ( 23 | ConnectIntService_PushToDevices_FullMethodName = "/connect.ConnectIntService/PushToDevices" 24 | ) 25 | 26 | // ConnectIntServiceClient is the client API for ConnectIntService service. 27 | // 28 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 29 | type ConnectIntServiceClient interface { 30 | // 消息投递 31 | PushToDevices(ctx context.Context, in *PushToDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) 32 | } 33 | 34 | type connectIntServiceClient struct { 35 | cc grpc.ClientConnInterface 36 | } 37 | 38 | func NewConnectIntServiceClient(cc grpc.ClientConnInterface) ConnectIntServiceClient { 39 | return &connectIntServiceClient{cc} 40 | } 41 | 42 | func (c *connectIntServiceClient) PushToDevices(ctx context.Context, in *PushToDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 43 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 44 | out := new(emptypb.Empty) 45 | err := c.cc.Invoke(ctx, ConnectIntService_PushToDevices_FullMethodName, in, out, cOpts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return out, nil 50 | } 51 | 52 | // ConnectIntServiceServer is the server API for ConnectIntService service. 53 | // All implementations must embed UnimplementedConnectIntServiceServer 54 | // for forward compatibility. 55 | type ConnectIntServiceServer interface { 56 | // 消息投递 57 | PushToDevices(context.Context, *PushToDevicesRequest) (*emptypb.Empty, error) 58 | mustEmbedUnimplementedConnectIntServiceServer() 59 | } 60 | 61 | // UnimplementedConnectIntServiceServer must be embedded to have 62 | // forward compatible implementations. 63 | // 64 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 65 | // pointer dereference when methods are called. 66 | type UnimplementedConnectIntServiceServer struct{} 67 | 68 | func (UnimplementedConnectIntServiceServer) PushToDevices(context.Context, *PushToDevicesRequest) (*emptypb.Empty, error) { 69 | return nil, status.Errorf(codes.Unimplemented, "method PushToDevices not implemented") 70 | } 71 | func (UnimplementedConnectIntServiceServer) mustEmbedUnimplementedConnectIntServiceServer() {} 72 | func (UnimplementedConnectIntServiceServer) testEmbeddedByValue() {} 73 | 74 | // UnsafeConnectIntServiceServer may be embedded to opt out of forward compatibility for this service. 75 | // Use of this interface is not recommended, as added methods to ConnectIntServiceServer will 76 | // result in compilation errors. 77 | type UnsafeConnectIntServiceServer interface { 78 | mustEmbedUnimplementedConnectIntServiceServer() 79 | } 80 | 81 | func RegisterConnectIntServiceServer(s grpc.ServiceRegistrar, srv ConnectIntServiceServer) { 82 | // If the following call pancis, it indicates UnimplementedConnectIntServiceServer was 83 | // embedded by pointer and is nil. This will cause panics if an 84 | // unimplemented method is ever invoked, so we test this at initialization 85 | // time to prevent it from happening at runtime later due to I/O. 86 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 87 | t.testEmbeddedByValue() 88 | } 89 | s.RegisterService(&ConnectIntService_ServiceDesc, srv) 90 | } 91 | 92 | func _ConnectIntService_PushToDevices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 93 | in := new(PushToDevicesRequest) 94 | if err := dec(in); err != nil { 95 | return nil, err 96 | } 97 | if interceptor == nil { 98 | return srv.(ConnectIntServiceServer).PushToDevices(ctx, in) 99 | } 100 | info := &grpc.UnaryServerInfo{ 101 | Server: srv, 102 | FullMethod: ConnectIntService_PushToDevices_FullMethodName, 103 | } 104 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 105 | return srv.(ConnectIntServiceServer).PushToDevices(ctx, req.(*PushToDevicesRequest)) 106 | } 107 | return interceptor(ctx, in, info, handler) 108 | } 109 | 110 | // ConnectIntService_ServiceDesc is the grpc.ServiceDesc for ConnectIntService service. 111 | // It's only intended for direct use with grpc.RegisterService, 112 | // and not to be introspected or modified (even as a copy) 113 | var ConnectIntService_ServiceDesc = grpc.ServiceDesc{ 114 | ServiceName: "connect.ConnectIntService", 115 | HandlerType: (*ConnectIntServiceServer)(nil), 116 | Methods: []grpc.MethodDesc{ 117 | { 118 | MethodName: "PushToDevices", 119 | Handler: _ConnectIntService_PushToDevices_Handler, 120 | }, 121 | }, 122 | Streams: []grpc.StreamDesc{}, 123 | Metadata: "connect/connect.int.proto", 124 | } 125 | -------------------------------------------------------------------------------- /pkg/protocol/pb/logicpb/device.ext_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.5.1 4 | // - protoc v5.29.3 5 | // source: logic/device.ext.proto 6 | 7 | package logicpb 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.64.0 or later. 19 | const _ = grpc.SupportPackageIsVersion9 20 | 21 | const ( 22 | DeviceExtService_RegisterDevice_FullMethodName = "/logic.DeviceExtService/RegisterDevice" 23 | ) 24 | 25 | // DeviceExtServiceClient is the client API for DeviceExtService service. 26 | // 27 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 28 | type DeviceExtServiceClient interface { 29 | // 注册设备 30 | RegisterDevice(ctx context.Context, in *RegisterDeviceRequest, opts ...grpc.CallOption) (*RegisterDeviceReply, error) 31 | } 32 | 33 | type deviceExtServiceClient struct { 34 | cc grpc.ClientConnInterface 35 | } 36 | 37 | func NewDeviceExtServiceClient(cc grpc.ClientConnInterface) DeviceExtServiceClient { 38 | return &deviceExtServiceClient{cc} 39 | } 40 | 41 | func (c *deviceExtServiceClient) RegisterDevice(ctx context.Context, in *RegisterDeviceRequest, opts ...grpc.CallOption) (*RegisterDeviceReply, error) { 42 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 43 | out := new(RegisterDeviceReply) 44 | err := c.cc.Invoke(ctx, DeviceExtService_RegisterDevice_FullMethodName, in, out, cOpts...) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return out, nil 49 | } 50 | 51 | // DeviceExtServiceServer is the server API for DeviceExtService service. 52 | // All implementations must embed UnimplementedDeviceExtServiceServer 53 | // for forward compatibility. 54 | type DeviceExtServiceServer interface { 55 | // 注册设备 56 | RegisterDevice(context.Context, *RegisterDeviceRequest) (*RegisterDeviceReply, error) 57 | mustEmbedUnimplementedDeviceExtServiceServer() 58 | } 59 | 60 | // UnimplementedDeviceExtServiceServer must be embedded to have 61 | // forward compatible implementations. 62 | // 63 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 64 | // pointer dereference when methods are called. 65 | type UnimplementedDeviceExtServiceServer struct{} 66 | 67 | func (UnimplementedDeviceExtServiceServer) RegisterDevice(context.Context, *RegisterDeviceRequest) (*RegisterDeviceReply, error) { 68 | return nil, status.Errorf(codes.Unimplemented, "method RegisterDevice not implemented") 69 | } 70 | func (UnimplementedDeviceExtServiceServer) mustEmbedUnimplementedDeviceExtServiceServer() {} 71 | func (UnimplementedDeviceExtServiceServer) testEmbeddedByValue() {} 72 | 73 | // UnsafeDeviceExtServiceServer may be embedded to opt out of forward compatibility for this service. 74 | // Use of this interface is not recommended, as added methods to DeviceExtServiceServer will 75 | // result in compilation errors. 76 | type UnsafeDeviceExtServiceServer interface { 77 | mustEmbedUnimplementedDeviceExtServiceServer() 78 | } 79 | 80 | func RegisterDeviceExtServiceServer(s grpc.ServiceRegistrar, srv DeviceExtServiceServer) { 81 | // If the following call pancis, it indicates UnimplementedDeviceExtServiceServer was 82 | // embedded by pointer and is nil. This will cause panics if an 83 | // unimplemented method is ever invoked, so we test this at initialization 84 | // time to prevent it from happening at runtime later due to I/O. 85 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 86 | t.testEmbeddedByValue() 87 | } 88 | s.RegisterService(&DeviceExtService_ServiceDesc, srv) 89 | } 90 | 91 | func _DeviceExtService_RegisterDevice_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 92 | in := new(RegisterDeviceRequest) 93 | if err := dec(in); err != nil { 94 | return nil, err 95 | } 96 | if interceptor == nil { 97 | return srv.(DeviceExtServiceServer).RegisterDevice(ctx, in) 98 | } 99 | info := &grpc.UnaryServerInfo{ 100 | Server: srv, 101 | FullMethod: DeviceExtService_RegisterDevice_FullMethodName, 102 | } 103 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 104 | return srv.(DeviceExtServiceServer).RegisterDevice(ctx, req.(*RegisterDeviceRequest)) 105 | } 106 | return interceptor(ctx, in, info, handler) 107 | } 108 | 109 | // DeviceExtService_ServiceDesc is the grpc.ServiceDesc for DeviceExtService service. 110 | // It's only intended for direct use with grpc.RegisterService, 111 | // and not to be introspected or modified (even as a copy) 112 | var DeviceExtService_ServiceDesc = grpc.ServiceDesc{ 113 | ServiceName: "logic.DeviceExtService", 114 | HandlerType: (*DeviceExtServiceServer)(nil), 115 | Methods: []grpc.MethodDesc{ 116 | { 117 | MethodName: "RegisterDevice", 118 | Handler: _DeviceExtService_RegisterDevice_Handler, 119 | }, 120 | }, 121 | Streams: []grpc.StreamDesc{}, 122 | Metadata: "logic/device.ext.proto", 123 | } 124 | -------------------------------------------------------------------------------- /pkg/protocol/pb/logicpb/room.ext_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.5.1 4 | // - protoc v5.29.3 5 | // source: logic/room.ext.proto 6 | 7 | package logicpb 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | emptypb "google.golang.org/protobuf/types/known/emptypb" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.64.0 or later. 20 | const _ = grpc.SupportPackageIsVersion9 21 | 22 | const ( 23 | RoomExtService_PushRoom_FullMethodName = "/logic.RoomExtService/PushRoom" 24 | ) 25 | 26 | // RoomExtServiceClient is the client API for RoomExtService service. 27 | // 28 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 29 | type RoomExtServiceClient interface { 30 | // 推送消息到房间 31 | PushRoom(ctx context.Context, in *PushRoomRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) 32 | } 33 | 34 | type roomExtServiceClient struct { 35 | cc grpc.ClientConnInterface 36 | } 37 | 38 | func NewRoomExtServiceClient(cc grpc.ClientConnInterface) RoomExtServiceClient { 39 | return &roomExtServiceClient{cc} 40 | } 41 | 42 | func (c *roomExtServiceClient) PushRoom(ctx context.Context, in *PushRoomRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 43 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 44 | out := new(emptypb.Empty) 45 | err := c.cc.Invoke(ctx, RoomExtService_PushRoom_FullMethodName, in, out, cOpts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return out, nil 50 | } 51 | 52 | // RoomExtServiceServer is the server API for RoomExtService service. 53 | // All implementations must embed UnimplementedRoomExtServiceServer 54 | // for forward compatibility. 55 | type RoomExtServiceServer interface { 56 | // 推送消息到房间 57 | PushRoom(context.Context, *PushRoomRequest) (*emptypb.Empty, error) 58 | mustEmbedUnimplementedRoomExtServiceServer() 59 | } 60 | 61 | // UnimplementedRoomExtServiceServer must be embedded to have 62 | // forward compatible implementations. 63 | // 64 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 65 | // pointer dereference when methods are called. 66 | type UnimplementedRoomExtServiceServer struct{} 67 | 68 | func (UnimplementedRoomExtServiceServer) PushRoom(context.Context, *PushRoomRequest) (*emptypb.Empty, error) { 69 | return nil, status.Errorf(codes.Unimplemented, "method PushRoom not implemented") 70 | } 71 | func (UnimplementedRoomExtServiceServer) mustEmbedUnimplementedRoomExtServiceServer() {} 72 | func (UnimplementedRoomExtServiceServer) testEmbeddedByValue() {} 73 | 74 | // UnsafeRoomExtServiceServer may be embedded to opt out of forward compatibility for this service. 75 | // Use of this interface is not recommended, as added methods to RoomExtServiceServer will 76 | // result in compilation errors. 77 | type UnsafeRoomExtServiceServer interface { 78 | mustEmbedUnimplementedRoomExtServiceServer() 79 | } 80 | 81 | func RegisterRoomExtServiceServer(s grpc.ServiceRegistrar, srv RoomExtServiceServer) { 82 | // If the following call pancis, it indicates UnimplementedRoomExtServiceServer was 83 | // embedded by pointer and is nil. This will cause panics if an 84 | // unimplemented method is ever invoked, so we test this at initialization 85 | // time to prevent it from happening at runtime later due to I/O. 86 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 87 | t.testEmbeddedByValue() 88 | } 89 | s.RegisterService(&RoomExtService_ServiceDesc, srv) 90 | } 91 | 92 | func _RoomExtService_PushRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 93 | in := new(PushRoomRequest) 94 | if err := dec(in); err != nil { 95 | return nil, err 96 | } 97 | if interceptor == nil { 98 | return srv.(RoomExtServiceServer).PushRoom(ctx, in) 99 | } 100 | info := &grpc.UnaryServerInfo{ 101 | Server: srv, 102 | FullMethod: RoomExtService_PushRoom_FullMethodName, 103 | } 104 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 105 | return srv.(RoomExtServiceServer).PushRoom(ctx, req.(*PushRoomRequest)) 106 | } 107 | return interceptor(ctx, in, info, handler) 108 | } 109 | 110 | // RoomExtService_ServiceDesc is the grpc.ServiceDesc for RoomExtService service. 111 | // It's only intended for direct use with grpc.RegisterService, 112 | // and not to be introspected or modified (even as a copy) 113 | var RoomExtService_ServiceDesc = grpc.ServiceDesc{ 114 | ServiceName: "logic.RoomExtService", 115 | HandlerType: (*RoomExtServiceServer)(nil), 116 | Methods: []grpc.MethodDesc{ 117 | { 118 | MethodName: "PushRoom", 119 | Handler: _RoomExtService_PushRoom_Handler, 120 | }, 121 | }, 122 | Streams: []grpc.StreamDesc{}, 123 | Metadata: "logic/room.ext.proto", 124 | } 125 | -------------------------------------------------------------------------------- /pkg/protocol/pb/logicpb/room.int_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.5.1 4 | // - protoc v5.29.3 5 | // source: logic/room.int.proto 6 | 7 | package logicpb 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | emptypb "google.golang.org/protobuf/types/known/emptypb" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.64.0 or later. 20 | const _ = grpc.SupportPackageIsVersion9 21 | 22 | const ( 23 | RoomIntService_PushRoom_FullMethodName = "/logic.RoomIntService/PushRoom" 24 | RoomIntService_SubscribeRoom_FullMethodName = "/logic.RoomIntService/SubscribeRoom" 25 | ) 26 | 27 | // RoomIntServiceClient is the client API for RoomIntService service. 28 | // 29 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 30 | type RoomIntServiceClient interface { 31 | // 推送消息到房间 32 | PushRoom(ctx context.Context, in *PushRoomRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) 33 | // 订阅房间 34 | SubscribeRoom(ctx context.Context, in *SubscribeRoomRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) 35 | } 36 | 37 | type roomIntServiceClient struct { 38 | cc grpc.ClientConnInterface 39 | } 40 | 41 | func NewRoomIntServiceClient(cc grpc.ClientConnInterface) RoomIntServiceClient { 42 | return &roomIntServiceClient{cc} 43 | } 44 | 45 | func (c *roomIntServiceClient) PushRoom(ctx context.Context, in *PushRoomRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 46 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 47 | out := new(emptypb.Empty) 48 | err := c.cc.Invoke(ctx, RoomIntService_PushRoom_FullMethodName, in, out, cOpts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | 55 | func (c *roomIntServiceClient) SubscribeRoom(ctx context.Context, in *SubscribeRoomRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 56 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 57 | out := new(emptypb.Empty) 58 | err := c.cc.Invoke(ctx, RoomIntService_SubscribeRoom_FullMethodName, in, out, cOpts...) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return out, nil 63 | } 64 | 65 | // RoomIntServiceServer is the server API for RoomIntService service. 66 | // All implementations must embed UnimplementedRoomIntServiceServer 67 | // for forward compatibility. 68 | type RoomIntServiceServer interface { 69 | // 推送消息到房间 70 | PushRoom(context.Context, *PushRoomRequest) (*emptypb.Empty, error) 71 | // 订阅房间 72 | SubscribeRoom(context.Context, *SubscribeRoomRequest) (*emptypb.Empty, error) 73 | mustEmbedUnimplementedRoomIntServiceServer() 74 | } 75 | 76 | // UnimplementedRoomIntServiceServer must be embedded to have 77 | // forward compatible implementations. 78 | // 79 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 80 | // pointer dereference when methods are called. 81 | type UnimplementedRoomIntServiceServer struct{} 82 | 83 | func (UnimplementedRoomIntServiceServer) PushRoom(context.Context, *PushRoomRequest) (*emptypb.Empty, error) { 84 | return nil, status.Errorf(codes.Unimplemented, "method PushRoom not implemented") 85 | } 86 | func (UnimplementedRoomIntServiceServer) SubscribeRoom(context.Context, *SubscribeRoomRequest) (*emptypb.Empty, error) { 87 | return nil, status.Errorf(codes.Unimplemented, "method SubscribeRoom not implemented") 88 | } 89 | func (UnimplementedRoomIntServiceServer) mustEmbedUnimplementedRoomIntServiceServer() {} 90 | func (UnimplementedRoomIntServiceServer) testEmbeddedByValue() {} 91 | 92 | // UnsafeRoomIntServiceServer may be embedded to opt out of forward compatibility for this service. 93 | // Use of this interface is not recommended, as added methods to RoomIntServiceServer will 94 | // result in compilation errors. 95 | type UnsafeRoomIntServiceServer interface { 96 | mustEmbedUnimplementedRoomIntServiceServer() 97 | } 98 | 99 | func RegisterRoomIntServiceServer(s grpc.ServiceRegistrar, srv RoomIntServiceServer) { 100 | // If the following call pancis, it indicates UnimplementedRoomIntServiceServer was 101 | // embedded by pointer and is nil. This will cause panics if an 102 | // unimplemented method is ever invoked, so we test this at initialization 103 | // time to prevent it from happening at runtime later due to I/O. 104 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 105 | t.testEmbeddedByValue() 106 | } 107 | s.RegisterService(&RoomIntService_ServiceDesc, srv) 108 | } 109 | 110 | func _RoomIntService_PushRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 111 | in := new(PushRoomRequest) 112 | if err := dec(in); err != nil { 113 | return nil, err 114 | } 115 | if interceptor == nil { 116 | return srv.(RoomIntServiceServer).PushRoom(ctx, in) 117 | } 118 | info := &grpc.UnaryServerInfo{ 119 | Server: srv, 120 | FullMethod: RoomIntService_PushRoom_FullMethodName, 121 | } 122 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 123 | return srv.(RoomIntServiceServer).PushRoom(ctx, req.(*PushRoomRequest)) 124 | } 125 | return interceptor(ctx, in, info, handler) 126 | } 127 | 128 | func _RoomIntService_SubscribeRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 129 | in := new(SubscribeRoomRequest) 130 | if err := dec(in); err != nil { 131 | return nil, err 132 | } 133 | if interceptor == nil { 134 | return srv.(RoomIntServiceServer).SubscribeRoom(ctx, in) 135 | } 136 | info := &grpc.UnaryServerInfo{ 137 | Server: srv, 138 | FullMethod: RoomIntService_SubscribeRoom_FullMethodName, 139 | } 140 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 141 | return srv.(RoomIntServiceServer).SubscribeRoom(ctx, req.(*SubscribeRoomRequest)) 142 | } 143 | return interceptor(ctx, in, info, handler) 144 | } 145 | 146 | // RoomIntService_ServiceDesc is the grpc.ServiceDesc for RoomIntService service. 147 | // It's only intended for direct use with grpc.RegisterService, 148 | // and not to be introspected or modified (even as a copy) 149 | var RoomIntService_ServiceDesc = grpc.ServiceDesc{ 150 | ServiceName: "logic.RoomIntService", 151 | HandlerType: (*RoomIntServiceServer)(nil), 152 | Methods: []grpc.MethodDesc{ 153 | { 154 | MethodName: "PushRoom", 155 | Handler: _RoomIntService_PushRoom_Handler, 156 | }, 157 | { 158 | MethodName: "SubscribeRoom", 159 | Handler: _RoomIntService_SubscribeRoom_Handler, 160 | }, 161 | }, 162 | Streams: []grpc.StreamDesc{}, 163 | Metadata: "logic/room.int.proto", 164 | } 165 | -------------------------------------------------------------------------------- /pkg/protocol/proto/connect/connect.ext.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package connect; 3 | option go_package = "gim/pkg/protocol/pb/connectpb"; 4 | 5 | import "logic/message.ext.proto"; 6 | 7 | enum Command { 8 | UNKNOWN = 0; // 未知 9 | SIGN_IN = 1; // 设备登录请求 10 | SYNC = 2; // 消息同步触发 11 | HEARTBEAT = 3; // 心跳 12 | MESSAGE = 4; // 消息投递 13 | SUBSCRIBE_ROOM = 5; // 订阅房间 14 | } 15 | 16 | // 包 17 | message Packet { 18 | Command command = 1; // 指令 19 | int64 request_id = 2; // 请求id 20 | int32 code = 3; // 错误码 21 | string message = 4; // 错误信息 22 | bytes data = 5; // 数据 23 | } 24 | 25 | // 设备登录,package_type:1 26 | message SignInInput { 27 | uint64 device_id = 1; // 设备id 28 | uint64 user_id = 2; // 用户id 29 | string token = 3; // 秘钥 30 | } 31 | 32 | // 消息同步请求,package_type:2 33 | message SyncInput { 34 | uint64 seq = 1; // 客户端已经同步的序列号 35 | } 36 | // 消息同步响应,package_type:2 37 | message SyncOutput { 38 | repeated logic.Message messages = 1; // 消息列表 39 | bool has_more = 2; // 是否有更多数据 40 | } 41 | 42 | // 订阅房间请求 43 | message SubscribeRoomInput { 44 | uint64 room_id = 1; // 房间ID,如果为0,取消房间订阅 45 | uint64 seq = 2; // 消息消息序列号, 46 | } 47 | 48 | // 消息投递,package_type:4 49 | // message.ext.proto文件下 Message 50 | 51 | // 投递消息回执,package_type:4 52 | message MessageACK { 53 | uint64 device_ack = 2; // 设备收到消息的确认号 54 | int64 receive_time = 3; // 消息接收时间戳,精确到毫秒 55 | } 56 | -------------------------------------------------------------------------------- /pkg/protocol/proto/connect/connect.int.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package connect; 3 | option go_package = "gim/pkg/protocol/pb/connectpb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | import "logic/message.ext.proto"; 8 | 9 | service ConnectIntService { 10 | // 消息投递 11 | rpc PushToDevices (PushToDevicesRequest) returns (google.protobuf.Empty); 12 | } 13 | 14 | message PushToDevicesRequest { 15 | repeated DeviceMessage DeviceMessageList = 1; 16 | } 17 | 18 | message DeviceMessage{ 19 | uint64 device_id = 1; // 设备ID 20 | logic.Message message = 2; // 消息 21 | } 22 | 23 | // 房间推送 24 | message PushRoomMsg{ 25 | uint64 room_id = 1; // 设备id 26 | logic.Message message = 2; // 数据 27 | } 28 | 29 | // 房间推送 30 | message PushAllMsg{ 31 | logic.Message message = 2; // 数据 32 | } -------------------------------------------------------------------------------- /pkg/protocol/proto/logic/device.ext.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package logic; 3 | option go_package = "gim/pkg/protocol/pb/logicpb"; 4 | 5 | service DeviceExtService { 6 | // 注册设备 7 | rpc RegisterDevice (RegisterDeviceRequest) returns (RegisterDeviceReply); 8 | } 9 | 10 | enum DeviceType{ 11 | DT_DEFAULT = 0; 12 | DT_ANDROID = 1; 13 | DT_IOS = 2; 14 | DT_WINDOWS = 3; 15 | DT_MACOS = 4; 16 | DT_WEB = 5; 17 | } 18 | 19 | message RegisterDeviceRequest { 20 | DeviceType type = 1; // 设备类型 21 | string brand = 2; // 厂商 22 | string model = 3; // 机型 23 | string system_version = 4; // 系统版本 24 | string sdk_version = 5; // sdk版本号 25 | } 26 | message RegisterDeviceReply { 27 | uint64 device_id = 1; // 设备id 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /pkg/protocol/proto/logic/device.int.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package logic; 3 | option go_package = "gim/pkg/protocol/pb/logicpb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | import "logic/device.ext.proto"; 8 | 9 | service DeviceIntService { 10 | // 登录 11 | rpc ConnSignIn (ConnSignInRequest) returns (google.protobuf.Empty); 12 | // 设备离线 13 | rpc Offline (OfflineRequest) returns (google.protobuf.Empty); 14 | // 获取设备信息 15 | rpc GetDevice (GetDeviceRequest) returns (GetDeviceReply); 16 | // 服务停止 17 | rpc ServerStop (ServerStopRequest) returns (google.protobuf.Empty); 18 | } 19 | 20 | message ConnSignInRequest { 21 | uint64 device_id = 1; // 设备id 22 | uint64 user_id = 2; // 用户id 23 | string token = 3; // 秘钥 24 | string conn_addr = 4; // 服务器地址 25 | string client_addr = 5; // 客户端地址 26 | } 27 | 28 | message OfflineRequest { 29 | uint64 user_id = 1; // 用户id 30 | uint64 device_id = 2; // 设备id 31 | string client_addr = 3; // 客户端地址 32 | } 33 | 34 | message GetDeviceRequest { 35 | uint64 device_id = 1; 36 | } 37 | message GetDeviceReply { 38 | Device device = 1; 39 | } 40 | 41 | message Device { 42 | uint64 device_id = 1; // 设备id 43 | uint64 user_id = 2; // 用户id 44 | DeviceType type = 3; // 设备类型 45 | string brand = 4; // 手机厂商 46 | string model = 5; // 机型 47 | string system_version = 6; // 系统版本 48 | string sdk_version = 7; // SDK版本 49 | int32 status = 8; // 在线状态,0:不在线;1:在线 50 | string conn_addr = 9; // 服务端连接地址 51 | string client_addr = 10; // 客户端地址 52 | int64 create_time = 11; // 创建时间 53 | int64 update_time = 12; // 更新时间 54 | } 55 | 56 | message ServerStopRequest { 57 | string conn_addr = 1; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /pkg/protocol/proto/logic/friend.ext.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package logic; 3 | option go_package = "gim/pkg/protocol/pb/logicpb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service FriendExtService { 8 | // 发送好友消息 9 | rpc SendMessage (SendFriendMessageRequest) returns (SendFriendMessageReply); 10 | // 添加好友 11 | rpc Add (FriendAddRequest) returns (google.protobuf.Empty); 12 | // 同意添加好友 13 | rpc Agree (FriendAgreeRequest) returns (google.protobuf.Empty); 14 | // 设置好友信息 15 | rpc Set (FriendSetRequest) returns (FriendSetReply); 16 | // 获取好友列表 17 | rpc GetFriends (google.protobuf.Empty) returns (GetFriendsReply); 18 | } 19 | 20 | message SendFriendMessageRequest { 21 | uint64 user_id = 1; // 接收者ID,用户ID/群组ID 22 | bytes content = 2; // 推动内容 23 | } 24 | message SendFriendMessageReply { 25 | uint64 message_id = 1; // 消息序列号 26 | } 27 | 28 | message FriendAddRequest { 29 | uint64 friend_id = 1; // 用户id 30 | string remarks = 2; // 备注 31 | string description = 3; // 描述 32 | } 33 | 34 | message FriendAgreeRequest { 35 | uint64 user_id = 1; // 用户id 36 | string remarks = 2; // 备注 37 | } 38 | 39 | message FriendSetRequest { 40 | uint64 friend_id = 1; // 好友id 41 | string remarks = 2; // 备注 42 | string extra = 8; // 附加字段 43 | } 44 | message FriendSetReply { 45 | uint64 friend_id = 1; // 好友id 46 | string remarks = 2; // 备注 47 | string extra = 8; // 附加字段 48 | } 49 | 50 | message Friend { 51 | uint64 user_id = 1; // 用户id 52 | string phone_number = 2; // 电话号码 53 | string nickname = 3; // 昵称 54 | int32 sex = 4; // 性别 55 | string avatar_url = 5; // 头像地址 56 | string user_extra = 6; // 用户附加字段 57 | string remarks = 7; // 备注 58 | string extra = 8; // 附加字段 59 | } 60 | message GetFriendsReply { 61 | repeated Friend friends = 1; 62 | } -------------------------------------------------------------------------------- /pkg/protocol/proto/logic/group.ext.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package logic; 3 | option go_package = "gim/pkg/protocol/pb/logicpb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service GroupExtService { 8 | // 发送群组消息 9 | rpc SendMessage (SendGroupMessageRequest) returns (SendGroupMessageReply); 10 | // 创建群组 11 | rpc Create (GroupCreateRequest) returns (GroupCreateReply); 12 | // 更新群组 13 | rpc Update (GroupUpdateRequest) returns (google.protobuf.Empty); 14 | // 获取群组信息 15 | rpc Get (GroupGetRequest) returns (GroupGetReply); 16 | // 获取用户加入的所有群组 17 | rpc List (google.protobuf.Empty) returns (GroupListReply); 18 | 19 | // 添加群组成员 20 | rpc AddMembers (AddMembersRequest) returns (AddMembersReply); 21 | // 更新群组成员信息 22 | rpc UpdateMember (UpdateMemberRequest) returns (google.protobuf.Empty); 23 | // 添加群组成员 24 | rpc DeleteMember (DeleteMemberRequest) returns (google.protobuf.Empty); 25 | // 获取群组成员 26 | rpc GetMembers (GetMembersRequest) returns (GetMembersReply); 27 | } 28 | 29 | message SendGroupMessageRequest { 30 | uint64 group_id = 1; // 群组ID 31 | bytes content = 2; // 推动内容 32 | } 33 | message SendGroupMessageReply { 34 | uint64 message_id = 1; // 消息序列号 35 | } 36 | 37 | message GroupCreateRequest { 38 | string name = 1; // 名称 39 | string avatar_url = 2; // 头像 40 | string introduction = 3; // 简介 41 | string extra = 4; // 附加字段 42 | repeated uint64 member_ids = 5; // 群组成员ID列表 43 | } 44 | message GroupCreateReply { 45 | uint64 group_id = 1; // 群组id 46 | } 47 | 48 | message GroupUpdateRequest { 49 | uint64 group_id = 1; // 群组id 50 | string avatar_url = 2; // 头像 51 | string name = 3; // 名称 52 | string introduction = 4; // 简介 53 | string extra = 5; // 附加字段 54 | } 55 | 56 | message GroupGetRequest { 57 | uint64 group_id = 1; 58 | } 59 | message GroupGetReply { 60 | Group group = 1; 61 | } 62 | 63 | message Group { 64 | uint64 group_id = 1; // 群组id 65 | string name = 2; // 名称 66 | string avatar_url = 3; // 头像 67 | string introduction = 4; // 简介 68 | int32 user_mum = 5; // 用户数 69 | string extra = 6; // 附加字段 70 | int64 create_time = 7; // 创建时间 71 | int64 update_time = 8; // 更新时间 72 | } 73 | 74 | message GroupListReply { 75 | repeated Group groups = 1; 76 | } 77 | 78 | message AddMembersRequest { 79 | uint64 group_id = 1; // 群组id 80 | repeated uint64 user_ids = 2; // 用户id列表 81 | } 82 | message AddMembersReply {} 83 | 84 | enum MemberType { 85 | GMT_UNKNOWN = 0; // 未知 86 | GMT_ADMIN = 1; // 管理员 87 | GMT_MEMBER = 2; // 成员 88 | } 89 | 90 | message UpdateMemberRequest { 91 | uint64 group_id = 1; // 群组id 92 | uint64 user_id = 2; // 用户id 93 | MemberType member_type = 3; // 成员类型 94 | string remarks = 4; // 备注 95 | string extra = 5; // 附加字段 96 | } 97 | 98 | message DeleteMemberRequest { 99 | uint64 group_id = 1; // 群组id 100 | uint64 user_id = 2; // 用户id 101 | } 102 | 103 | message GetMembersRequest { 104 | uint64 group_id = 1; 105 | } 106 | message GetMembersReply { 107 | repeated GroupMember members = 1; 108 | } 109 | message GroupMember { 110 | uint64 user_id = 1; 111 | string nickname = 2; // 昵称 112 | int32 sex = 3; // 性别 113 | string avatar_url = 4; // 头像地址 114 | string user_extra = 5; // 用户附加字段 115 | MemberType member_type = 6; // 成员类型 116 | string remarks = 7; // 备注 117 | string extra = 8; // 群组成员附加字段 118 | } -------------------------------------------------------------------------------- /pkg/protocol/proto/logic/message.ext.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package logic; 3 | option go_package = "gim/pkg/protocol/pb/logicpb"; 4 | 5 | import "logic/group.ext.proto"; 6 | 7 | // 单条消息投递内容 8 | message Message { 9 | PushCode code = 1; // 推送码 10 | bytes content = 2; // 推送内容 11 | uint64 seq = 3; // 用户消息发送序列号 12 | int64 created_at = 4; // 消息发送时间戳,精确到毫秒 13 | MessageStatus status = 5; // 消息状态 14 | } 15 | 16 | enum MessageStatus { 17 | MS_NORMAL = 0; // 正常的 18 | MS_RECALL = 1; // 撤回 19 | } 20 | 21 | enum PushCode { 22 | PC_ADD_DEFAULT = 0; 23 | 24 | PC_USER_MESSAGE = 100; // 用户消息 25 | PC_GROUP_MESSAGE = 101; // 群组消息 26 | 27 | PC_ADD_FRIEND = 110; // 添加好友请求 28 | PC_AGREE_ADD_FRIEND = 111; // 同意添加好友 29 | 30 | PC_UPDATE_GROUP = 120; // 更新群组 31 | PC_ADD_GROUP_MEMBERS = 121; // 添加群组成员 32 | PC_REMOVE_GROUP_MEMBER = 122; // 移除群组成员 33 | } 34 | 35 | message Sender { 36 | uint64 user_id = 2; // 发送者id 37 | uint64 device_id = 3; // 发送者设备id 38 | string avatar_url = 4; // 昵称 39 | string nickname = 5; // 头像 40 | string extra = 6; // 扩展字段 41 | } 42 | 43 | // 用户消息 PC_USER_MESSAGE = 100 44 | message UserMessagePush{ 45 | Sender sender = 1; // 发送者信息 46 | bytes content = 2; // 用户发送的消息内容 47 | } 48 | 49 | // 群组消息 PC_GROUP_MESSAGE = 101 50 | message GroupMessagePush{ 51 | Sender sender = 1; // 发送者信息 52 | uint64 group_id = 2; // 群组ID 53 | bytes content = 3; // 用户发送的消息内容 54 | } 55 | 56 | // 添加好友 PC_ADD_FRIEND = 110 57 | message AddFriendPush { 58 | uint64 friend_id = 1; // 好友id 59 | string nickname = 2; // 昵称 60 | string avatar_url = 3; // 头像 61 | string description = 4; // 描述 62 | } 63 | 64 | // 同意 添加好友 PC_AGREE_ADD_FRIEND = 111 65 | message AgreeAddFriendPush { 66 | uint64 friend_id = 1; // 好友id 67 | string nickname = 2; // 昵称 68 | string avatar_url = 3; // 头像 69 | } 70 | 71 | // 更新群组 PC_UPDATE_GROUP = 120 72 | message UpdateGroupPush { 73 | uint64 opt_id = 1; // 操作人用户id 74 | string opt_name = 2; // 操作人昵称 75 | string name = 3; // 群组名称 76 | string avatar_url = 4; // 群组头像 77 | string introduction = 5; // 群组简介 78 | string extra = 6; // 附加字段 79 | } 80 | 81 | // 添加群组成员 PC_AGREE_ADD_GROUPS = 121 82 | message AddGroupMembersPush { 83 | uint64 opt_id = 1; // 操作人用户id 84 | string opt_name = 2; // 操作人昵称 85 | repeated GroupMember members = 3; // 群组成员 86 | } 87 | 88 | // 删除群组成员 PC_REMOVE_GROUP_MEMBER = 122 89 | message RemoveGroupMemberPush { 90 | uint64 opt_id = 1; // 操作人用户id 91 | string opt_name = 2; // 操作人昵称 92 | uint64 deleted_user_id = 3; // 被删除的成员id 93 | } 94 | 95 | -------------------------------------------------------------------------------- /pkg/protocol/proto/logic/message.int.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package logic; 3 | option go_package = "gim/pkg/protocol/pb/logicpb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | import "logic/message.ext.proto"; 8 | 9 | service MessageIntService { 10 | // 消息同步 11 | rpc Sync (SyncRequest) returns (SyncReply); 12 | // 设备收到消息回执 13 | rpc MessageACK (MessageACKRequest) returns (google.protobuf.Empty); 14 | // 推送 15 | rpc Pushs (PushsRequest) returns (PushsReply); 16 | // 全服推送 17 | rpc PushAll(PushAllRequest)returns(google.protobuf.Empty); 18 | } 19 | 20 | message SyncRequest { 21 | uint64 user_id = 1; // 用户id 22 | uint64 device_id = 2; // 设备id 23 | uint64 seq = 3; // 客户端已经同步的序列号 24 | } 25 | message SyncReply { 26 | repeated Message messages = 1; // 消息列表 27 | bool has_more = 2; // 是否有更多数据 28 | } 29 | 30 | message MessageACKRequest { 31 | uint64 user_id = 1; // 用户id 32 | uint64 device_id = 2; // 设备id 33 | uint64 device_ack = 3; // 设备收到消息的确认号 34 | int64 receive_time = 4; // 消息接收时间戳,精确到毫秒 35 | } 36 | 37 | message PushsRequest{ 38 | repeated uint64 user_ids = 1; // 用户ID 39 | PushCode code = 2; // 推送码 40 | bytes content = 3; // 推送内容 41 | bool is_persist = 4; // 是否持久化 42 | } 43 | message PushsReply{ 44 | uint64 message_id = 1; 45 | } 46 | 47 | message PushAllRequest{ 48 | PushCode code = 1; // 推送码 49 | bytes content = 2; // 推送内容 50 | } -------------------------------------------------------------------------------- /pkg/protocol/proto/logic/room.ext.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package logic; 3 | option go_package = "gim/pkg/protocol/pb/logicpb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "logic/message.ext.proto"; 7 | 8 | service RoomExtService { 9 | // 推送消息到房间 10 | rpc PushRoom(PushRoomRequest)returns(google.protobuf.Empty); 11 | } 12 | 13 | message PushRoomRequest{ 14 | uint64 room_id = 1; // 房间id 15 | PushCode code = 2; // 消息类型 16 | bytes content = 3; // 消息内容 17 | int64 send_time = 4; // 消息发送时间戳,精确到毫秒 18 | bool is_persist = 5; // 是否将消息持久化 19 | bool is_priority = 6; // 是否优先推送 20 | } -------------------------------------------------------------------------------- /pkg/protocol/proto/logic/room.int.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package logic; 3 | option go_package = "gim/pkg/protocol/pb/logicpb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | import "logic/room.ext.proto"; 8 | 9 | service RoomIntService { 10 | // 推送消息到房间 11 | rpc PushRoom(PushRoomRequest)returns(google.protobuf.Empty); 12 | // 订阅房间 13 | rpc SubscribeRoom(SubscribeRoomRequest)returns(google.protobuf.Empty); 14 | } 15 | 16 | message SubscribeRoomRequest{ 17 | uint64 user_id = 1; // 用户id 18 | uint64 device_id = 2; // 设备id 19 | uint64 room_id = 3; // 房间id 20 | uint64 seq = 4; // 消息序列号 21 | string conn_addr = 5; // 服务器地址 22 | } -------------------------------------------------------------------------------- /pkg/protocol/proto/user/user.ext.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package user; 3 | option go_package = "gim/pkg/protocol/pb/userpb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service UserExtService { 8 | // 登录 9 | rpc SignIn (SignInRequest) returns (SignInReply); 10 | // 获取用户信息 11 | rpc GetUser (GetUserRequest) returns (GetUserReply); 12 | // 更新用户信息 13 | rpc UpdateUser (UpdateUserRequest) returns (google.protobuf.Empty); 14 | // 搜索用户(这里简单数据库实现,生产环境建议使用ES) 15 | rpc SearchUser (SearchUserRequest) returns (SearchUserReply); 16 | } 17 | 18 | message SignInRequest { 19 | string phone_number = 1; // 手机号 20 | string code = 2; // 验证码 21 | uint64 device_id = 3; // 设备id 22 | } 23 | message SignInReply { 24 | bool is_new = 1; // 是否是新用户 25 | uint64 user_id = 2; // 用户id 26 | string token = 3; // token 27 | } 28 | 29 | message User { 30 | uint64 user_id = 1; // 用户id 31 | string nickname = 2; // 昵称 32 | int32 sex = 3; // 性别 33 | string avatar_url = 4; // 头像地址 34 | string extra = 5; // 附加字段 35 | int64 create_time = 6; // 创建时间 36 | int64 update_time = 7; // 更新时间 37 | } 38 | 39 | message GetUserRequest { 40 | uint64 user_id = 1; // 用户id 41 | } 42 | message GetUserReply { 43 | User user = 1; // 用户信息 44 | } 45 | 46 | message UpdateUserRequest { 47 | string nickname = 1; // 昵称 48 | int32 sex = 2; // 性别 49 | string avatar_url = 3; // 头像地址 50 | string extra = 4; // 附加字段 51 | } 52 | 53 | message SearchUserRequest{ 54 | string key = 1; 55 | } 56 | message SearchUserReply{ 57 | repeated User users = 1; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /pkg/protocol/proto/user/user.int.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package user; 3 | option go_package = "gim/pkg/protocol/pb/userpb"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "user/user.ext.proto"; 7 | 8 | service UserIntService { 9 | // 权限校验 10 | rpc Auth (AuthRequest) returns (google.protobuf.Empty); 11 | // 批量获取用户信息 12 | rpc GetUser (GetUserRequest) returns (GetUserReply); 13 | // 批量获取用户信息 14 | rpc GetUsers (GetUsersRequest) returns (GetUsersReply); 15 | } 16 | 17 | message AuthRequest { 18 | uint64 user_id = 1; 19 | uint64 device_id = 2; 20 | string token = 3; 21 | } 22 | 23 | message GetUsersRequest { 24 | map user_ids = 1; // 用户id 25 | } 26 | message GetUsersReply { 27 | map users = 1; // 用户信息 28 | } 29 | -------------------------------------------------------------------------------- /pkg/rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | 6 | "gim/config" 7 | "gim/pkg/protocol/pb/connectpb" 8 | "gim/pkg/protocol/pb/logicpb" 9 | "gim/pkg/protocol/pb/userpb" 10 | ) 11 | 12 | var ( 13 | connectIntClient connectpb.ConnectIntServiceClient 14 | deviceIntClient logicpb.DeviceIntServiceClient 15 | messageIntClient logicpb.MessageIntServiceClient 16 | roomIntClient logicpb.RoomIntServiceClient 17 | userIntClient userpb.UserIntServiceClient 18 | ) 19 | 20 | func GetConnectIntClient() connectpb.ConnectIntServiceClient { 21 | if connectIntClient == nil { 22 | connectIntClient = config.Config.ConnectIntClientBuilder() 23 | } 24 | return connectIntClient 25 | } 26 | 27 | func GetDeviceIntClient() logicpb.DeviceIntServiceClient { 28 | if deviceIntClient == nil { 29 | deviceIntClient = config.Config.DeviceIntClientBuilder() 30 | } 31 | return deviceIntClient 32 | } 33 | 34 | func GetMessageIntClient() logicpb.MessageIntServiceClient { 35 | if messageIntClient == nil { 36 | messageIntClient = config.Config.MessageIntClientBuilder() 37 | } 38 | return messageIntClient 39 | } 40 | 41 | func GetRoomIntClient() logicpb.RoomIntServiceClient { 42 | if roomIntClient == nil { 43 | roomIntClient = config.Config.RoomIntClientBuilder() 44 | } 45 | return roomIntClient 46 | } 47 | 48 | func GetUserIntClient() userpb.UserIntServiceClient { 49 | if userIntClient == nil { 50 | userIntClient = config.Config.UserIntClientBuilder() 51 | } 52 | return userIntClient 53 | } 54 | 55 | func GetSender(deviceID, userID uint64) (*logicpb.Sender, error) { 56 | user, err := GetUserIntClient().GetUser(context.TODO(), &userpb.GetUserRequest{UserId: userID}) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return &logicpb.Sender{ 61 | UserId: userID, 62 | DeviceId: deviceID, 63 | AvatarUrl: user.User.AvatarUrl, 64 | Nickname: user.User.Nickname, 65 | Extra: user.User.Extra, 66 | }, nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | const beginStatus = 1 8 | 9 | // SessionFactory 会话工厂 10 | type SessionFactory struct { 11 | *sql.DB 12 | } 13 | 14 | // Session 会话 15 | type Session struct { 16 | DB *sql.DB // 原生db 17 | tx *sql.Tx // 原生事务 18 | commitSign int8 // 提交标记,控制是否提交事务 19 | rollbackSign bool // 回滚标记,控制是否回滚事务 20 | } 21 | 22 | // NewSessionFactory 创建一个会话工厂 23 | func NewSessionFactory(driverName, dataSourseName string) (*SessionFactory, error) { 24 | db, err := sql.Open(driverName, dataSourseName) 25 | if err != nil { 26 | panic(err) 27 | } 28 | factory := new(SessionFactory) 29 | factory.DB = db 30 | return factory, nil 31 | } 32 | 33 | // GetSession 获取一个Session 34 | func (sf *SessionFactory) GetSession() *Session { 35 | session := new(Session) 36 | session.DB = sf.DB 37 | return session 38 | } 39 | 40 | // Begin 开启事务 41 | func (s *Session) Begin() error { 42 | s.rollbackSign = true 43 | if s.tx == nil { 44 | tx, err := s.DB.Begin() 45 | if err != nil { 46 | return err 47 | } 48 | s.tx = tx 49 | s.commitSign = beginStatus 50 | return nil 51 | } 52 | s.commitSign++ 53 | return nil 54 | } 55 | 56 | // Rollback 回滚事务 57 | func (s *Session) Rollback() error { 58 | if s.tx != nil && s.rollbackSign { 59 | err := s.tx.Rollback() 60 | if err != nil { 61 | return err 62 | } 63 | s.tx = nil 64 | return nil 65 | } 66 | return nil 67 | } 68 | 69 | // Commit 提交事务 70 | func (s *Session) Commit() error { 71 | s.rollbackSign = false 72 | if s.tx != nil { 73 | if s.commitSign == beginStatus { 74 | err := s.tx.Commit() 75 | if err != nil { 76 | return err 77 | } 78 | s.tx = nil 79 | return nil 80 | } else { 81 | s.commitSign-- 82 | } 83 | return nil 84 | } 85 | return nil 86 | } 87 | 88 | // Exec 执行sql语句,如果已经开启事务,就以事务方式执行,如果没有开启事务,就以非事务方式执行 89 | func (s *Session) Exec(query string, args ...interface{}) (sql.Result, error) { 90 | if s.tx != nil { 91 | return s.tx.Exec(query, args...) 92 | } 93 | return s.DB.Exec(query, args...) 94 | } 95 | 96 | // QueryRow 如果已经开启事务,就以事务方式执行,如果没有开启事务,就以非事务方式执行 97 | func (s *Session) QueryRow(query string, args ...interface{}) *sql.Row { 98 | if s.tx != nil { 99 | return s.tx.QueryRow(query, args...) 100 | } 101 | return s.DB.QueryRow(query, args...) 102 | } 103 | 104 | // Query 查询数据,如果已经开启事务,就以事务方式执行,如果没有开启事务,就以非事务方式执行 105 | func (s *Session) Query(query string, args ...interface{}) (*sql.Rows, error) { 106 | if s.tx != nil { 107 | return s.tx.Query(query, args...) 108 | } 109 | return s.DB.Query(query, args...) 110 | } 111 | 112 | // Prepare 预执行,如果已经开启事务,就以事务方式执行,如果没有开启事务,就以非事务方式执行 113 | func (s *Session) Prepare(query string) (*sql.Stmt, error) { 114 | if s.tx != nil { 115 | return s.tx.Prepare(query) 116 | } 117 | return s.DB.Prepare(query) 118 | } 119 | -------------------------------------------------------------------------------- /pkg/urlwhitelist/urlwhitelist.go: -------------------------------------------------------------------------------- 1 | package urlwhitelist 2 | 3 | var User = map[string]struct{}{ 4 | "/user.UserExtService/SignIn": {}, 5 | } 6 | 7 | var Logic = map[string]struct{}{ 8 | "/logic.DeviceExtService/RegisterDevice": {}, 9 | } 10 | -------------------------------------------------------------------------------- /pkg/util/json.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func FormatMessage(code int32, content []byte) string { 8 | return fmt.Sprintf("code:%d,content:%s", code, string(content)) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/util/message.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | 6 | "google.golang.org/protobuf/proto" 7 | 8 | pb "gim/pkg/protocol/pb/logicpb" 9 | ) 10 | 11 | var MessagePushes = map[pb.PushCode]proto.Message{ 12 | pb.PushCode_PC_USER_MESSAGE: &pb.UserMessagePush{}, 13 | pb.PushCode_PC_GROUP_MESSAGE: &pb.GroupMessagePush{}, 14 | pb.PushCode_PC_ADD_FRIEND: &pb.AddFriendPush{}, 15 | pb.PushCode_PC_AGREE_ADD_FRIEND: &pb.AgreeAddFriendPush{}, 16 | pb.PushCode_PC_UPDATE_GROUP: &pb.UpdateGroupPush{}, 17 | pb.PushCode_PC_ADD_GROUP_MEMBERS: &pb.AddGroupMembersPush{}, 18 | pb.PushCode_PC_REMOVE_GROUP_MEMBER: &pb.RemoveGroupMemberPush{}, 19 | } 20 | 21 | func MessageToString(msg *pb.Message) string { 22 | push, ok := MessagePushes[msg.Code] 23 | if !ok { 24 | return fmt.Sprintf("%-5d %-5d %s %s", msg.Code, msg.Seq, "unknown", string(msg.Content)) 25 | } 26 | 27 | _ = proto.Unmarshal(msg.Content, push) 28 | //return fmt.Sprintf("%-5d %-5d %s", msg.Code, msg.Seq, push) 29 | 30 | switch msg.Code { 31 | case pb.PushCode_PC_USER_MESSAGE: 32 | p := push.(*pb.UserMessagePush) 33 | return fmt.Sprintf("%-5d %-5d %v %s", msg.Code, msg.Seq, push, string(p.Content)) 34 | case pb.PushCode_PC_GROUP_MESSAGE: 35 | p := push.(*pb.GroupMessagePush) 36 | return fmt.Sprintf("%-5d %-5d %v %s", msg.Code, msg.Seq, push, string(p.Content)) 37 | default: 38 | return fmt.Sprintf("%-5d %-5d %s", msg.Code, msg.Seq, push) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/util/panic.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "log/slog" 5 | "runtime" 6 | ) 7 | 8 | // RecoverPanic 恢复panic 9 | func RecoverPanic() { 10 | err := recover() 11 | if err != nil { 12 | slog.Error("panic", "panic", err, "stack", GetStackInfo()) 13 | } 14 | } 15 | 16 | // GetStackInfo 获取Panic堆栈信息 17 | func GetStackInfo() string { 18 | buf := make([]byte, 4096) 19 | n := runtime.Stack(buf, false) 20 | return string(buf[:n]) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/util/redis.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-redis/redis" 7 | jsoniter "github.com/json-iterator/go" 8 | ) 9 | 10 | type RedisUtil struct { 11 | client *redis.Client 12 | } 13 | 14 | func NewRedisUtil(client *redis.Client) *RedisUtil { 15 | return &RedisUtil{client: client} 16 | } 17 | 18 | // Set 将指定值设置到redis中,使用json的序列化方式 19 | func (u *RedisUtil) Set(key string, value interface{}, duration time.Duration) error { 20 | bytes, err := jsoniter.Marshal(value) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return u.client.Set(key, bytes, duration).Err() 26 | } 27 | 28 | // Get 从redis中读取指定值,使用json的反序列化方式 29 | func (u *RedisUtil) Get(key string, value interface{}) error { 30 | bytes, err := u.client.Get(key).Bytes() 31 | if err != nil { 32 | return err 33 | } 34 | return jsoniter.Unmarshal(bytes, value) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/util/string.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | "unsafe" 7 | ) 8 | 9 | var r *rand.Rand 10 | 11 | func init() { 12 | r = rand.New(rand.NewSource(time.Now().Unix())) 13 | } 14 | 15 | func Str2bytes(s string) []byte { 16 | x := (*[2]uintptr)(unsafe.Pointer(&s)) 17 | h := [3]uintptr{x[0], x[1], x[1]} 18 | return *(*[]byte)(unsafe.Pointer(&h)) 19 | } 20 | 21 | func Bytes2str(b []byte) string { 22 | return *(*string)(unsafe.Pointer(&b)) 23 | } 24 | 25 | // RandString 生成随机字符串 26 | func RandString(len int) string { 27 | bytes := make([]byte, len) 28 | for i := 0; i < len; i++ { 29 | b := r.Intn(26) + 65 30 | bytes[i] = byte(b) 31 | } 32 | return string(bytes) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/util/time.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "time" 4 | 5 | // FormatTime 格式化时间 6 | func FormatTime(time time.Time) string { 7 | return time.Format("2006-01-02 15:04:05") 8 | } 9 | 10 | // ParseTime 将时间字符串转为Time 11 | func ParseTime(str string) (time.Time, error) { 12 | return time.Parse("2006-01-02 15:04:05", str) 13 | } 14 | 15 | // UnixMilliTime 将时间转化为毫秒数 16 | func UnixMilliTime(t time.Time) int64 { 17 | return t.UnixNano() / 1000000 18 | } 19 | 20 | // UnunixMilliTime 将毫秒数转为为时间 21 | func UnunixMilliTime(unix int64) time.Time { 22 | return time.Unix(0, unix*1000000) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/util/uid/uid.go: -------------------------------------------------------------------------------- 1 | package uid 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "log" 7 | "time" 8 | ) 9 | 10 | type logger interface { 11 | Error(error) 12 | } 13 | 14 | // Logger Log接口,如果设置了Logger,就使用Logger打印日志,如果没有设置,就使用内置库log打印日志 15 | var Logger logger 16 | 17 | // ErrTimeOut 获取uid超时错误 18 | var ErrTimeOut = errors.New("get uid timeout") 19 | 20 | type Uid struct { 21 | db *sql.DB // 数据库连接 22 | businessId string // 业务id 23 | ch chan int64 // id缓冲池 24 | min, max int64 // id段最小值,最大值 25 | } 26 | 27 | // NewUid 创建一个Uid;len:缓冲池大小() 28 | // db:数据库连接 29 | // businessId:业务id 30 | // len:缓冲池大小(长度可控制缓存中剩下多少id时,去DB中加载) 31 | func NewUid(db *sql.DB, businessId string, len int) (*Uid, error) { 32 | lid := Uid{ 33 | db: db, 34 | businessId: businessId, 35 | ch: make(chan int64, len), 36 | } 37 | go lid.productId() 38 | return &lid, nil 39 | } 40 | 41 | // Get 获取自增id,当发生超时,返回错误,避免大量请求阻塞,服务器崩溃 42 | func (u *Uid) Get() (int64, error) { 43 | select { 44 | case <-time.After(1 * time.Second): 45 | return 0, ErrTimeOut 46 | case uid := <-u.ch: 47 | return uid, nil 48 | } 49 | } 50 | 51 | // productId 生产id,当ch达到最大容量时,这个方法会阻塞,直到ch中的id被消费 52 | func (u *Uid) productId() { 53 | _ = u.reLoad() 54 | 55 | for { 56 | if u.min >= u.max { 57 | _ = u.reLoad() 58 | } 59 | 60 | u.min++ 61 | u.ch <- u.min 62 | } 63 | } 64 | 65 | // reLoad 在数据库获取id段,如果失败,会每隔一秒尝试一次 66 | func (u *Uid) reLoad() error { 67 | var err error 68 | for { 69 | err = u.getFromDB() 70 | if err == nil { 71 | return nil 72 | } 73 | 74 | // 数据库发生异常,等待一秒之后再次进行尝试 75 | if Logger != nil { 76 | Logger.Error(err) 77 | } else { 78 | log.Println(err) 79 | } 80 | time.Sleep(time.Second) 81 | } 82 | } 83 | 84 | // getFromDB 从数据库获取id段 85 | func (u *Uid) getFromDB() error { 86 | var ( 87 | maxId int64 88 | step int64 89 | ) 90 | 91 | tx, err := u.db.Begin() 92 | if err != nil { 93 | return err 94 | } 95 | defer func() { _ = tx.Rollback() }() 96 | 97 | row := tx.QueryRow("SELECT max_id,step FROM uid WHERE business_id = ? FOR UPDATE", u.businessId) 98 | err = row.Scan(&maxId, &step) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | _, err = tx.Exec("UPDATE uid SET max_id = ? WHERE business_id = ?", maxId+step, u.businessId) 104 | if err != nil { 105 | return err 106 | } 107 | err = tx.Commit() 108 | if err != nil { 109 | return err 110 | } 111 | 112 | u.min = maxId 113 | u.max = maxId + step 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/util/uid/uid_test.go: -------------------------------------------------------------------------------- 1 | package uid 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "testing" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | func TestLid(t *testing.T) { 12 | db, err := sql.Open("mysql", "root:Liu123456@tcp(localhost:3306)/im?charset=utf8") 13 | if err != nil { 14 | fmt.Println(err) 15 | panic(err) 16 | } 17 | 18 | lid, err := NewUid(db, "test", 100) 19 | if err != nil { 20 | fmt.Println(err) 21 | return 22 | } 23 | i := 0 24 | for i < 100 { 25 | id, _ := lid.Get() 26 | fmt.Println(id) 27 | i++ 28 | } 29 | } 30 | 31 | func TestLid_Get(t *testing.T) { 32 | go getLid("one") 33 | go getLid("two") 34 | go getLid("three") 35 | select {} 36 | } 37 | 38 | func getLid(index string) { 39 | db, err := sql.Open("mysql", "root:Liu123456@tcp(localhost:3306)/im?charset=utf8") 40 | if err != nil { 41 | fmt.Println(err) 42 | panic(err) 43 | } 44 | 45 | lid, err := NewUid(db, "test", 1000) 46 | if err != nil { 47 | fmt.Println(err) 48 | return 49 | } 50 | i := 0 51 | for i < 100 { 52 | id, _ := lid.Get() 53 | fmt.Println(index, id) 54 | i++ 55 | } 56 | } 57 | 58 | func BenchmarkLeafKey(b *testing.B) { 59 | db, err := sql.Open("mysql", "root:Liu123456@tcp(localhost:3306)/im?charset=utf8") 60 | if err != nil { 61 | fmt.Println(err) 62 | panic(err) 63 | } 64 | 65 | lid, err := NewUid(db, "test", 1000) 66 | if err != nil { 67 | fmt.Println(err) 68 | return 69 | } 70 | 71 | for i := 0; i < b.N; i++ { 72 | _, _ = lid.Get() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | version=$(date +"%Y%m%d.%H%M%S") 6 | 7 | ./build.sh $1 $version 8 | 9 | cd deploy/compose 10 | 11 | image_tag=$1:$version 12 | sed -i '' "s/$1:[0-9]\{8\}\.[0-9]\{6\}/$image_tag/g" compose.yaml 13 | 14 | docker compose up -d 15 | 16 | --------------------------------------------------------------------------------