Go項目推薦之即時通訊服務(wù)器 IM
點擊上方藍(lán)色“Go語言中文網(wǎng)”關(guān)注我們,領(lǐng)全套Go資料,每天學(xué)習(xí)?Go?語言
簡要介紹
im是一個即時通訊服務(wù)器,代碼全部使用golang完成。主要功能
1.支持tcp,websocket接入
2.離線消息同步
3.單用戶多設(shè)備同時在線
4.單聊,群聊,以及超大群聊天場景
5.支持服務(wù)水平擴(kuò)展
gim和im有什么區(qū)別?gim可以作為一個im中臺提供給業(yè)務(wù)方使用,而im可以作為以業(yè)務(wù)服務(wù)器的一個組件, 為業(yè)務(wù)服務(wù)器提供im的能力,業(yè)務(wù)服務(wù)器的user服務(wù)只需要實現(xiàn)user.int.proto協(xié)議中定義的GRPC接口,為im服務(wù) 提供基本的用戶功能即可,其實以我目前的認(rèn)知,我更推薦這種方式,這種模式相比于gim,我認(rèn)為最大好處在于 以下兩點:
1.im不需要考慮多個app的場景,相比gim,業(yè)務(wù)復(fù)雜度降低了一個維度
2.各個業(yè)務(wù)服務(wù)可以互不影響,可以做到風(fēng)險隔離
使用技術(shù):
數(shù)據(jù)庫:MySQL+Redis
通訊框架:GRPC
長連接通訊協(xié)議:Protocol Buffers
日志框架:Zap
ORM框架:GORM
安裝部署
1.首先安裝MySQL,Redis
2.創(chuàng)建數(shù)據(jù)庫im,執(zhí)行sql/create_table.sql,完成初始化表的創(chuàng)建(數(shù)據(jù)庫包含提供測試的一些初始數(shù)據(jù))
3.修改config下配置文件,使之和你本地配置一致
4.分別切換到cmd的tcp_conn,ws_conn,logic,user目錄下,執(zhí)行g(shù)o run main.go,啟動TCP連接層服務(wù)器, WebSocket連接層服務(wù)器,邏輯層服務(wù)器,用戶服務(wù)器
(注意:tcp_conn只能在linux下啟動,如果想在其他平臺下啟動,請安裝docker,執(zhí)行run.sh)
項目目錄簡介
項目結(jié)構(gòu)遵循?https://github.com/golang-standards/project-layout
cmd: 服務(wù)啟動入口
config: 服務(wù)配置
internal: 每個服務(wù)私有代碼
pkg: 服務(wù)共有代碼
sql: 項目sql文件
test: 長連接測試腳本服務(wù)簡介
1.tcp_conn
維持與客戶端的TCP長連接,心跳,以及TCP拆包粘包,消息編解碼
2.ws_conn
維持與客戶端的WebSocket長連接,心跳,消息編解碼
3.logic
設(shè)備信息,好友信息,群組信息管理,消息轉(zhuǎn)發(fā)邏輯
4.user
一個簡單的用戶服務(wù),可以根據(jù)自己的業(yè)務(wù)需求,進(jìn)行擴(kuò)展
網(wǎng)絡(luò)模型
TCP的網(wǎng)絡(luò)層使用linux的epoll實現(xiàn),相比golang原生,能減少goroutine使用,從而節(jié)省系統(tǒng)資源占用
單用戶多設(shè)備支持,離線消息同步
每個用戶都會維護(hù)一個自增的序列號,當(dāng)用戶A給用戶B發(fā)送消息時,首先會獲取A的最大序列號,設(shè)置為這條消息的seq,持久化到用戶A的消息列表, 再通過長連接下發(fā)到用戶A賬號登錄的所有設(shè)備,再獲取用戶B的最大序列號,設(shè)置為這條消息的seq,持久化到用戶B的消息列表,再通過長連接下發(fā) 到用戶B賬號登錄的所有設(shè)備。
假如用戶的某個設(shè)備不在線,在設(shè)備長連接登錄時,用本地收到消息的最大序列號,到服務(wù)器做消息同步,這樣就可以保證離線消息不丟失。
讀擴(kuò)散和寫擴(kuò)散
首先解釋一下,什么是讀擴(kuò)散,什么是寫擴(kuò)散
讀擴(kuò)散
簡介:群組成員發(fā)送消息時,先建立一個會話,都將這個消息寫入這個會話中,同步離線消息時,需要同步這個會話的未同步消息
優(yōu)點:每個消息只需要寫入數(shù)據(jù)庫一次就行,減少數(shù)據(jù)庫訪問次數(shù),節(jié)省數(shù)據(jù)庫空間
缺點:一個用戶有n個群組,客戶端每次同步消息時,要上傳n個序列號,服務(wù)器要對這n個群組分別做消息同步
寫擴(kuò)散
簡介:在群組中,每個用戶維持一個自己的消息列表,當(dāng)群組中有人發(fā)送消息時,給群組的每個用戶的消息列表插入一條消息即可
優(yōu)點:每個用戶只需要維護(hù)一個序列號和消息列表
缺點:一個群組有多少人,就要插入多少條消息,當(dāng)群組成員很多時,DB的壓力會增大
消息轉(zhuǎn)發(fā)邏輯選型以及特點
普通群組:
采用寫擴(kuò)散,群組成員信息持久化到數(shù)據(jù)庫保存。支持消息離線同步。
超大群組:
采用讀擴(kuò)散,群組成員信息保存到redis,不支持離線消息同步。
核心流程時序圖
長連接登錄

離線消息同步

心跳

消息單發(fā)
c1.d1和c1.d2分別表示c1用戶的兩個設(shè)備d1和d2,c2.d3和c2.d4同理

小群消息群發(fā)
c1,c2.c3表示一個群組中的三個用戶

大群消息群發(fā)

錯誤處理,鏈路追蹤,日志打印
系統(tǒng)中的錯誤一般可以歸類為兩種,一種是業(yè)務(wù)定義的錯誤,一種就是未知的錯誤,在業(yè)務(wù)正式上線的時候,業(yè)務(wù)定義的錯誤的屬于正常業(yè)務(wù)邏輯,不需要打印出來, 但是未知的錯誤,我們就需要打印出來,我們不僅要知道是什么錯誤,還要知道錯誤的調(diào)用堆棧,所以這里我對GRPC的錯誤進(jìn)行了一些封裝,使之包含調(diào)用堆棧。
func WrapError(err error) error {if err == nil {return nil}s := &spb.Status{Code: int32(codes.Unknown),Message: err.Error(),Details: []*any.Any{{TypeUrl: TypeUrlStack,Value: util.Str2bytes(stack()),},},}return status.FromProto(s).Err()}// Stack 獲取堆棧信息func stack() string {var pc = make([]uintptr, 20)n := runtime.Callers(3, pc)var build strings.Builderfor i := 0; i < n; i++ {f := runtime.FuncForPC(pc[i] - 1)file, line := f.FileLine(pc[i] - 1)n := strings.Index(file, name)if n != -1 {s := fmt.Sprintf(" %s:%d \n", file[n:], line)build.WriteString(s)}}return build.String()}
這樣,不僅可以拿到錯誤的堆棧,錯誤的堆棧也可以跨RPC傳輸,但是,但是這樣你只能拿到當(dāng)前服務(wù)的堆棧,卻不能拿到調(diào)用方的堆棧,就比如說,A服務(wù)調(diào)用 B服務(wù),當(dāng)B服務(wù)發(fā)生錯誤時,在A服務(wù)通過日志打印錯誤的時候,我們只打印了B服務(wù)的調(diào)用堆棧,怎樣可以把A服務(wù)的堆棧打印出來。我們在A服務(wù)調(diào)用的地方也獲取 一次堆棧。
func WrapRPCError(err error) error {if err == nil {return nil}e, _ := status.FromError(err)s := &spb.Status{Code: int32(e.Code()),Message: e.Message(),Details: []*any.Any{{TypeUrl: TypeUrlStack,Value: util.Str2bytes(GetErrorStack(e) + " --grpc-- \n" + stack()),},},}return status.FromProto(s).Err()}func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {err := invoker(ctx, method, req, reply, cc, opts...)return gerrors.WrapRPCError(err)}var LogicIntClient pb.LogicIntClientfunc InitLogicIntClient(addr string) {conn, err := grpc.DialContext(context.TODO(), addr, grpc.WithInsecure(), grpc.WithUnaryInterceptor(interceptor))if err != nil {logger.Sugar.Error(err)panic(err)}LogicIntClient = pb.NewLogicIntClient(conn)}
像這樣,就可以獲取完整一次調(diào)用堆棧。錯誤打印也沒有必要在函數(shù)返回錯誤的時候,每次都去打印。因為錯誤已經(jīng)包含了堆棧信息
// 錯誤的方式if err != nil {logger.Sugar.Error(err)return err}// 正確的方式if err != nil {return err}
然后,我們在上層統(tǒng)一打印就可以
func startServer() {extListen, err := net.Listen("tcp", conf.LogicConf.ClientRPCExtListenAddr)if err != nil {panic(err)}extServer := grpc.NewServer(grpc.UnaryInterceptor(LogicClientExtInterceptor))pb.RegisterLogicClientExtServer(extServer, &LogicClientExtServer{})err = extServer.Serve(extListen)if err != nil {panic(err)}}func LogicClientExtInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {defer func() {logPanic("logic_client_ext_interceptor", ctx, req, info, &err)}()resp, err = handler(ctx, req)logger.Logger.Debug("logic_client_ext_interceptor", zap.Any("info", info), zap.Any("ctx", ctx), zap.Any("req", req),zap.Any("resp", resp), zap.Error(err))s, _ := status.FromError(err)if s.Code() != 0 && s.Code() < 1000 {md, _ := metadata.FromIncomingContext(ctx)logger.Logger.Error("logic_client_ext_interceptor", zap.String("method", info.FullMethod), zap.Any("md", md), zap.Any("req", req),zap.Any("resp", resp), zap.Error(err), zap.String("stack", gerrors.GetErrorStack(s)))}return}
這樣做的前提就是,在業(yè)務(wù)代碼中透傳context,golang不像其他語言,可以在線程本地保存變量,像Java的ThreadLocal,所以只能通過函數(shù)參數(shù)的形式進(jìn)行傳遞,im中,service層函數(shù)的第一個參數(shù) 都是context,但是dao層和cache層就不需要了,不然,顯得代碼臃腫。
最后可以在客戶端的每次請求添加一個隨機(jī)的request_id,這樣客戶端到服務(wù)的每次請求都可以串起來了。
func getCtx() context.Context {token, _ := util.GetToken(1, 2, 3, time.Now().Add(1*time.Hour).Unix(), util.PublicKey)return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs("app_id", "1","user_id", "2","device_id", "3","token", token,"request_id", strconv.FormatInt(time.Now().UnixNano(), 10)))}
github
https://github.com/alberliu/im
來源:
https://www.toutiao.com/a6853605017463554572/
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場和創(chuàng)業(yè)經(jīng)驗
Go語言中文網(wǎng)
每天為你
分享 Go 知識
Go愛好者值得關(guān)注
