go java websocket_golang写的IM服务器(tcp,websocket,grpc)

本文转载: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' `

6c367f8122b22eda83318ee2996b43b7.png离线消息同步4 o2 ?# o! R# x: Z

, j+ d& Y# e% Z, s' s$ f. C

ee843d6d19e3fb460b478b5a2d35024c.png心跳. L! v  ]! K' C. S

- X" z* s% Q, x9 y0 v# q8 C

e3a05e6976520d3c2f00d9bc32e7d6fd.png消息单发

/ 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

64bf0636dcf221e6963edaf15674e42c.png小群消息群发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

697e4a371504fc907847033f18a570fe.png大群消息群发

) W. X; ^9 ^* s) N; u

( e( M8 i8 d( v+ P& h8 ^

335ced04797b01aeb49c05b1c90963a5.png错误处理,链路追踪,日志打印. z/ L; M) r- {7 v4 `

* v+ D* n; `4 v- O# |  ~$ h5 b系统中的错误一般可以归类为两种,一种是业务定义的错误,一种就是未知的错误,在业务正式上线的时候,业务定义的错误的属于正常业务逻辑,不需要打印出来,2 H* z2 P9 K1 e

但是未知的错误,我们就需要打印出来,我们不仅要知道是什么错误,还要知道错误的调用堆栈,所以这里我对GRPC的错误进行了一些封装,使之包含调用堆栈。

要在 Java 中实现 WebSocket,可以使用 Java WebSocket API。以下是一个简单的示例: ```java import javax.websocket.*; import java.net.URI; @ClientEndpoint public class MyWebSocketClient { @OnOpen public void onOpen(Session session) { System.out.println("Connected to endpoint: " + session.getBasicRemote()); } @OnMessage public void onMessage(String message) { System.out.println("Received message: " + message); } public static void main(String[] args) { WebSocketContainer container = ContainerProvider.getWebSocketContainer(); String uri = "ws://localhost:8080/myendpoint"; try { Session session = container.connectToServer(MyWebSocketClient.class, URI.create(uri)); session.getBasicRemote().sendText("Hello, World!"); } catch (Exception e) { e.printStackTrace(); } } } ``` 在上面的示例中,我们定义了一个 `MyWebSocketClient` 类,并使用 `@ClientEndpoint` 注解将其标记为 WebSocket 客户端端点。我们还定义了两个注解方法,`onOpen()` 和 `onMessage()`,分别在连接建立和接收到消息时被调用。在 `main()` 方法中,我们使用 `WebSocketContainer` 来连接到 WebSocket 终端点,并向其发送一条消息。 在使用上述代码之前,需要确保已经引入 Java WebSocket API 的依赖项。可以使用以下 Maven 依赖项: ```xml <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.1</version> </dependency> ``` 注意,以上代码仅适用于通过端口直接连接的 WebSocket。如果需要使用 SSL/TLS 连接,或者需要进行身份验证等操作,需要进行额外的配置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值