<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go項目推薦之即時通訊服務(wù)器 IM

          共 5877字,需瀏覽 12分鐘

           ·

          2020-08-09 05:41


          點擊上方藍(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.Builder for 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.LogicIntClient
          func 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/



          推薦閱讀


          學(xué)習(xí)交流 Go 語言,掃碼回復(fù)「進(jìn)群」即可


          站長 polarisxu

          自己的原創(chuàng)文章

          不限于 Go 技術(shù)

          職場和創(chuàng)業(yè)經(jīng)驗


          Go語言中文網(wǎng)

          每天為你

          分享 Go 知識

          Go愛好者值得關(guān)注


          瀏覽 38
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人人肏人人摸人人操 | 91丨国产丨白浆秘 在线 | 久久国产精品波多野结衣AV | 中文有码在线 | 69AV电影 |