本文转载:https://github.com/alberliu/gim( i- y6 e/ t/ L; w, s简要介绍
% H$ O" L- M: ]4 C( Q$ g0 o4 o( M! n. y, |
gim是一个即时通讯服务器,代码全部使用golang完成。主要功能6 B. [2 _3 a" o9 \* C* u+ L* O% Z
1.支持tcp,websocket接入
0 q+ }; ~! l' Z2.离线消息同步& p$ c' s# Z$ K$ R. M
3.多业务接入
7 q1 V1 k9 t9 f% |, C4.单用户多设备同时在线
' Z8 u- x: B4 b6 \5.单聊,群聊,以及超大群聊天场景1 W$ Z9 K+ T5 y& s B- L; M8 O; z
6.支持服务水平扩展m. d# y& ]3 V ?9 _2 J# A5 j
使用技术:+ T4 N2 H- Y6 a" e1 Q# I! {3 T
: c, i: ?2 C# V( @: y数据库:Mysql+Redis5 y0 m5 t. G4 G
通讯框架:Grpc( f% Y! L1 e! D L# y8 ~0 V! ~% C
长连接通讯协议:Protocol Buffers2 X% q3 N; `! _, ~8 ^ \- j
日志框架:ZapK4 y+ Q+ o8 {' g ?& }
安装部署% D+ c! }5 `1 b, V7 C) ^
9 a. k5 P8 }; M4 E9 `/ m1.首先安装MySQL,Redis# i6 _9 T+ ~& F
2.创建数据库gim,执行sql/create_table.sql,完成初始化表的创建(数据库包含提供测试的一些初始数据)
5 y, H1 g; y; n; Y; {. f9 \3.修改config下配置文件,使之和你本地配置一致
* r4 @+ v5 t2 Q8 f0 m4.分别切换到cmd的tcp_conn,ws_conn,logic目录下,执行go run main.go,启动TCP连接层服务器,WebSocket连接层服务器,逻辑层服务器
& f- O8 R, \ S9 L9 J3 N迅速跑通本地测试* X% K0 j4 f# ~: ^8 z
2 g% f1 S+ v! n: q1.在test目录下,tcp_conn或者ws_conn目录下,执行go run main,启动测试脚本
- u1 Y& m3 m; R: ?4 u2.根据提示,依次填入app_id,user_id,device_id,sync_sequence(中间空格空开),进行长连接登录;数据库device表中已经初始化了一些设备信息,用作测试
, E' L7 W7 D2 n$ N6 c3.执行api/logic/logic_client_ext_test.go下的TestLogicExtServer_SendMessage函数,发送消息
: x6 m2 S& m( v$ B. h; k业务服务器如何接入
! L- x1 n: E! J$ R2 T; ~: j$ s! V9 d* U
1.首先生成私钥和公钥3 c/ a% v; U) {4 [
2.在app表里根据你的私钥添加一条app记录" T) f7 P3 [4 P2 ?0 l
3.将app_id和公钥保存到业务服务器
) |3 Q- h0 [- x+ `! L% U: h4.将用户通过LogicClientExtServer.AddUser接口添加到IM服务器; b: ~7 D- Z8 R X
5.通过LogicClientExtServer.RegisterDevice接口注册设备,获取设备id(device_id)- P1 p* Y, }7 C- o8 b2 w/ E0 e, i( F
6.将app_id,user_id,device_id用公钥通过公钥加密,生成token,相应库的代码在pkg/util/aes.go4 n& v7 O, C4 U+ q# w1 K
7.接下来使用这个token,app就可以和IM服务器交互5 B! ^) H% x; N$ r0 t$ O7 A
rpc接口简介; ^% n/ x% h1 ]9 Q0 [5 e
% ]3 `; q* t5 |. M' v3 Y- t, ?, Q
项目所有的proto协议在gim/public/proto/目录下3 i" N/ y8 d% L+ W; B
1.tcp.proto% Y, W- K( s+ W$ C$ s2 X
长连接通讯协议$ C' i. ~# R" s
2.logic_client.ext.proto
: Q1 s* O% b9 D' \3 M对客户端(Android设备,IOS设备)提供的rpc协议& x! I4 y5 C9 F% x# K
3.logic_server.ext.proto
' d7 V' O* t. _6 L9 f对业务服务器提供的rpc协议# k3 z N/ d% J, H' N5 m* _
4.logic.int.proto
5 u/ i b* r& n8 Z& }3 u: I对conn服务层提供的rpc协议- `; `3 W3 s% }) e' P
5.conn.int.proto2 [1 k; _. f5 f8 m+ C) O. T5 u9 r8 `7 h
对logic服务层提供的rpc协议
* l4 t+ q3 x& ]4 u6 n- ?( b2 P项目目录简介
* [1 T+ y3 L: k6 [) H* L8 Q7 @. i0 {# k
项目结构遵循 https://github.com/golang-standards/project-layout
) W+ f0 N- q4 ^- O& }; a2 hapi: 服务对外提供的grpc接口cmd: 服务启动入口config: 服务配置internal: 每个服务私有代码pkg: 服务共有代码sql: 项目sql文件test: 长连接测试脚本服务简介2 Q8 L8 J5 R0 h6 {+ a1 S- y5 y
8 D# @1 ?. N0 E: y$ P1.tcp_conn, z2 E: g) X. W% W& U- h
维持与客户端的TCP长连接,心跳,以及TCP拆包粘包,消息编解码
5 R: p) p& r% o! ?2 m1 q4 O2.ws_conn( G4 k. b3 f& X* J! [/ T
维持与客户端的WebSocket长连接,心跳,消息编解码
: [, i% E' ]1 ~- H* K8 Y6 X3.logic: y+ U4 O9 [4 V7 z5 \9 k! D* P
设备信息,用户信息,群组信息管理,消息转发逻辑% y8 m9 A6 K( [9 V4 w N4 b1 t
TCP拆包粘包! [/ ?" z4 g; Z( y" G+ G9 q
% @4 i6 x# ~5 V A! e
遵循LV的协议格式,一个消息包分为两部分,消息字节长度以及消息内容。2 I" p$ w }- [' k0 B; G. t
这里为了减少内存分配,拆出来的包的内存复用读缓存区内存。
5 T1 ^! _& N( [) |2 S4 m& `# _拆包流程: # b8 D+ j4 q7 i6 O B3 p
1.首先从系统缓存区读取字节流到buffer
9 Y# F( u2 `; W3 }2.根据包头的length字段,检查报的value字段的长度是否大于等于length
+ p6 z% U+ j, J& c5 B2 T1 l3.如果大于,返回一个完整包(此包内存复用),重复步骤2
F; N+ U1 b! p5 v4.如果小于,将buffer的有效字节前移,重复步骤1" \/ l) A2 k4 a5 ~! C
单用户多设备支持,离线消息同步$ [! ?. D2 q& E# B. u$ ~; I6 r
. P* R2 p% R& z* w+ h2 x; `每个用户都会维护一个自增的序列号,当用户A给用户B发送消息是,首先会获取A的最大序列号,设置为这条消息的seq,持久化到用户A的消息列表,9 W0 O0 j, f: P0 \' q& s r
再通过长连接下发到用户A账号登录的所有设备,再获取用户B的最大序列号,设置为这条消息的seq,持久化到用户B的消息列表,再通过长连接下发( t7 B3 e# Z1 Y9 G/ G C4 v
到用户B账号登录的所有设备。% j! B- @8 c- U' m
假如用户的某个设备不在线,在设备长连接登录时,用本地收到消息的最大序列号,到服务器做消息同步,这样就可以保证离线消息不丢失。
) f K* w7 y( }, A4 q- S) v, U度扩散和写扩散1 q" s& x8 g6 r; A7 X+ c
/ @" M+ k+ ^( C5 t. s# M! i B首先解释一下,什么是读扩散,什么是写扩散& |5 \, ]1 @4 ~1 L6 `
读扩散
7 [' U3 @9 A7 `5 B
" i }; y: M( B4 X简介:群组成员发送消息时,先建立一个会话,都将这个消息写入这个会话中,同步离线消息时,需要同步这个会话的未同步消息s/ C+ o3 T4 l3 Y$ N$ N
优点:每个消息只需要写入数据库一次就行,减少数据库访问次数,节省数据库空间" F" s3 S2 J1 K
缺点:一个用户有n个群组,客户端每次同步消息时,要上传n个序列号,服务器要对这n个群组分别做消息同步
0 y+ |1 Q) S R3 c$ q5 s( A4 n写扩散/ S' v9 Z+ a9 j' F3 ^! q- |9 ?- {
. o5 Q/ ?, H, g" M% W
简介:在群组中,每个用户维持一个自己的消息列表,当群组中有人发送消息时,给群组的每个用户的消息列表插入一条消息即可4 D+ p6 E& f2 A2 v8 x" G
优点:每个用户只需要维护一个序列号和消息列表
) G; H1 ]! }! {2 w3 Q缺点:一个群组有多少人,就要插入多少条消息,当群组成员很多时,DB的压力会增大1 d1 H3 p* C) m
消息转发逻辑选型以及特点7 m- ?6 W" E6 E% V7 B8 K0 H0 q
' f ~& `$ D* t* |% K9 k! P) G普通群组:
1 j$ D" [3 B1 G6 L& G% c% g n3 U7 H
采用写扩散,群组成员信息持久化到数据库保存。支持消息离线同步。
$ }' ?, D7 p) K0 N& h2 B1 ?$ Y- `超大群组:0 L' [3 ~3 X: _1 G0 v% ~
; `2 u* h9 E8 O9 i& \4 ~采用读扩散,群组成员信息保存到redis,不支持离线消息同步。3 s% e6 C* I- Q! V! g7 V
核心流程时序图/ E- w6 @5 W1 Q
" h* @0 s5 T( L6 B长连接登录
/ ^; i) l b* ?* w- e% O% M Y9 t' `
离线消息同步4 o2 ?# o! R# x: Z
, j+ d& Y# e% Z, s' s$ f. C
心跳. L! v ]! K' C. S
- X" z* s% Q, x9 y0 v# q8 C
消息单发
/ M6 ]; K) c+ ~0 @9 h2 ?
x) W5 p( g! S2 }0 s3 m6 Zc1.d1和c1.d2分别表示c1用户的两个设备d1和d2,c2.d3和c2.d4同理* M ?5 |' D ?' g% Z( G
% P8 C/ _/ ^% `% w, u
小群消息群发6 w9 J3 w+ z" D! F2 V/ m
q0 K; W! {( g
c1,c2.c3表示一个群组中的三个用户, c4 T, q* }; D9 T( J# |
0 ~" ]9 o4 y* Y5 g: P
大群消息群发
) W. X; ^9 ^* s) N; u
( e( M8 i8 d( v+ P& h8 ^
错误处理,链路追踪,日志打印. z/ L; M) r- {7 v4 `
* v+ D* n; `4 v- O# | ~$ h5 b系统中的错误一般可以归类为两种,一种是业务定义的错误,一种就是未知的错误,在业务正式上线的时候,业务定义的错误的属于正常业务逻辑,不需要打印出来,2 H* z2 P9 K1 e
但是未知的错误,我们就需要打印出来,我们不仅要知道是什么错误,还要知道错误的调用堆栈,所以这里我对GRPC的错误进行了一些封装,使之包含调用堆栈。