简要介绍

im是一个即时通讯服务器,代码全部使用golang完成。主要功能

1.支持tcp,websocket接入

2.离线消息同步

3.单用户多设备同时在线

4.单聊,群聊,以及超大群聊天场景

5.支持服务水平扩展

gim和im有什么区别?gim可以作为一个im中台提供给业务方使用,而im可以作为以业务服务器的一个组件, 为业务服务器提供im的能力,业务服务器的user服务只需要实现user.int.proto协议中定义的GRPC接口,为im服务 提供基本的用户功能即可,其实以我目前的认知,我更推荐这种方式,这种模式相比于gim,我认为最大好处在于 以下两点:

1.im不需要考虑多个app的场景,相比gim,业务复杂度降低了一个维度

2.各个业务服务可以互不影响,可以做到风险隔离

使用技术:

数据库:MySQL+Redis

通讯框架:GRPC

长连接通讯协议:Protocol Buffers

日志框架:Zap

ORM框架:GORM

安装部署

1.首先安装MySQL,Redis

2.创建数据库im,执行sql/create_table.sql,完成初始化表的创建(数据库包含提供测试的一些初始数据)

3.修改config下配置文件,使之和你本地配置一致

4.分别切换到cmd的tcp_conn,ws_conn,logic,user目录下,执行go run main.go,启动TCP连接层服务器, WebSocket连接层服务器,逻辑层服务器,用户服务器

(注意:tcp_conn只能在linux下启动,如果想在其他平台下启动,请安装docker,执行run.sh)

项目目录简介

项目结构遵循 https://github.com/golang-standards/project-layoutcmd: 服务启动入口config: 服务配置internal: 每个服务私有代码pkg: 服务共有代码sql: 项目sql文件test: 长连接测试脚本

服务简介

1.tcp_conn

维持与客户端的TCP长连接,心跳,以及TCP拆包粘包,消息编解码

2.ws_conn

维持与客户端的WebSocket长连接,心跳,消息编解码

3.logic

设备信息,好友信息,群组信息管理,消息转发逻辑

4.user

一个简单的用户服务,可以根据自己的业务需求,进行扩展

网络模型

TCP的网络层使用linux的epoll实现,相比golang原生,能减少goroutine使用,从而节省系统资源占用

单用户多设备支持,离线消息同步

每个用户都会维护一个自增的序列号,当用户A给用户B发送消息时,首先会获取A的最大序列号,设置为这条消息的seq,持久化到用户A的消息列表, 再通过长连接下发到用户A账号登录的所有设备,再获取用户B的最大序列号,设置为这条消息的seq,持久化到用户B的消息列表,再通过长连接下发 到用户B账号登录的所有设备。

假如用户的某个设备不在线,在设备长连接登录时,用本地收到消息的最大序列号,到服务器做消息同步,这样就可以保证离线消息不丢失。

读扩散和写扩散

首先解释一下,什么是读扩散,什么是写扩散

读扩散

简介:群组成员发送消息时,先建立一个会话,都将这个消息写入这个会话中,同步离线消息时,需要同步这个会话的未同步消息

优点:每个消息只需要写入数据库一次就行,减少数据库访问次数,节省数据库空间

缺点:一个用户有n个群组,客户端每次同步消息时,要上传n个序列号,服务器要对这n个群组分别做消息同步

写扩散

简介:在群组中,每个用户维持一个自己的消息列表,当群组中有人发送消息时,给群组的每个用户的消息列表插入一条消息即可

优点:每个用户只需要维护一个序列号和消息列表

缺点:一个群组有多少人,就要插入多少条消息,当群组成员很多时,DB的压力会增大

消息转发逻辑选型以及特点

普通群组:

采用写扩散,群组成员信息持久化到数据库保存。支持消息离线同步。

超大群组:

采用读扩散,群组成员信息保存到redis,不支持离线消息同步。

核心流程时序图

长连接登录

离线消息同步

心跳

消息单发

c1.d1和c1.d2分别表示c1用户的两个设备d1和d2,c2.d3和c2.d4同理

小群消息群发

c1,c2.c3表示一个群组中的三个用户

大群消息群发

错误处理,链路追踪,日志打印

系统中的错误一般可以归类为两种,一种是业务定义的错误,一种就是未知的错误,在业务正式上线的时候,业务定义的错误的属于正常业务逻辑,不需要打印出来, 但是未知的错误,我们就需要打印出来,我们不仅要知道是什么错误,还要知道错误的调用堆栈,所以这里我对GRPC的错误进行了一些封装,使之包含调用堆栈。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传输,但是,但是这样你只能拿到当前服务的堆栈,却不能拿到调用方的堆栈,就比如说,A服务调用 B服务,当B服务发生错误时,在A服务通过日志打印错误的时候,我们只打印了B服务的调用堆栈,怎样可以把A服务的堆栈打印出来。我们在A服务调用的地方也获取 一次堆栈。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)}

像这样,就可以获取完整一次调用堆栈。错误打印也没有必要在函数返回错误的时候,每次都去打印。因为错误已经包含了堆栈信息// 错误的方式if err != nil {logger.Sugar.Error(err)return err}// 正确的方式if err != nil {return err}

然后,我们在上层统一打印就可以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}

这样做的前提就是,在业务代码中透传context,golang不像其他语言,可以在线程本地保存变量,像Java的ThreadLocal,所以只能通过函数参数的形式进行传递,im中,service层函数的第一个参数 都是context,但是dao层和cache层就不需要了,不然,显得代码臃肿。

最后可以在客户端的每次请求添加一个随机的request_id,这样客户端到服务的每次请求都可以串起来了。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/“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com

来都来了,走啥走,留个言呗~

IT大咖说  |关于版权

由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!

感谢您对IT大咖说的热心支持!

微信作为一种提供即时通讯服务器,Golang 写的即时通讯服务器 im(服务组件形式)...相关推荐

  1. fcm 服务器接入 golang_Golang 写的即时通讯服务器 im(服务组件形式)

    简要介绍 im是一个即时通讯服务器,代码全部使用golang完成.主要功能 1.支持tcp,websocket接入 2.离线消息同步 3.单用户多设备同时在线 4.单聊,群聊,以及超大群聊天场景 5. ...

  2. goim Java_GitHub - alberliu/gim: golang写的IM服务器(服务组件形式)

    简要介绍 im是一个即时通讯服务器,代码全部使用golang完成.主要功能 1.支持tcp,websocket接入 2.离线消息同步 3.单用户多设备同时在线 4.单聊,群聊,以及超大群聊天场景 5. ...

  3. 批量抓取微信朋友圈 Java_微信朋友圈转疯了(golang写小爬虫抓取朋友圈文章)...

    很多人在朋友圈里转发一些文章,标题都是什么转疯啦之类,虽然大多都也是广告啦,我觉得还蛮无聊的,但是的确是有一些文章是非常值得收藏的,比如老婆经常就会收藏一些养生和美容的文章在微信里看. 今天就突发奇想 ...

  4. 微信开发 没有认证过的服务号怎么办?微信提供了测试号(开通了认证过的服务号的功能)

    还在为微信开发,没有认证过的服务号烦恼?微信已经给开发者提供了测试号(开通了认证过的服务号的高级接口,支付等) 使用微信提供的测试号(开通了服务号的功能),微信提供给开发者开发测试使用. http:/ ...

  5. 微信、支付宝否认提供「锁定 35 万人」数据;苹果押宝中国 5G 市场;TiDB 4.0.1 发布 | 极客头条...

    整理 | 屠敏 头图 | CSDN 下载自东方 IC 快来收听极客头条音频版吧,智能播报由出门问问「魔音工坊」提供技术支持. 「极客头条」-- 技术人员的新闻圈! CSDN 的读者朋友们早上好哇,「极 ...

  6. java监听微信_一种基于java后台应用监控微信小程序的用户访问量的方法与流程...

    本发明涉及java应用与微信小程序应用开发技术领域,具体涉及一种基于java后台应用监控微信小程序的用户访问量的方法. 背景技术: 微信小程序,简称小程序,是一种不需要下载安装即可使用的应用,它实现了 ...

  7. oa 服务器 微信,微信OA:一种新的办公方式

    原标题:微信OA:一种新的办公方式 拥有6亿用户的腾讯微信计划推出新的公众账号体系"企业号",从而涉足企业办公市场,为企业提供针对内部员工的管理.沟通与服务.业界认为,微信企业号有 ...

  8. 主流的四种IM(IM:instant messaging,即时消息)协议

    转载:http://hi.baidu.com/zhaojinwei1986/blog/item/fe3b9f12d5784e24dd5401db.html XMPP(Extensible Messag ...

  9. 苹果手微信显示无法连接服务器,iPhone手机无法运行微信的4种解决技巧

    微信是大家常用的一种社交工具,因此,当微信突然无法在iPhone上面正常运行时,就会给大家带来一些困扰,造成许多不便.那么,该如何解决这个问题?接下来就告诉您答案. 当您发现您的微信突然无法打开或者正 ...

最新文章

  1. Java多线程干货系列—(一)Java多线程基础
  2. 进军“手机照相馆”:京东要和3C厂商干什么?
  3. 数字证书应用综合揭秘(包括证书生成、加密、解密、签名、验签)
  4. 文献记录(part49)--极大熵聚类算法的收敛性定理证明
  5. 通过引入switch表达式来增强Java switch语句
  6. java中this_夯实Java基础系列7:一文读懂Java 代码块和执行顺序
  7. HTTP相关知识的总结
  8. Android things:你应该了解的Android Things知识
  9. 20的阶乘c语言怎么编程,C语言:编写程序,求20的阶乘.
  10. 小米路由 php 服务器地址,小米路由器ip地址能改吗 小米路由器ip地址修改-192路由网...
  11. Java开发关键技术
  12. 数字化转型时代,我们为什么越来越需要数据库云管平台?
  13. a16z:私钥屡被攻破?Web3安全还得从钱包说起
  14. SIMOUS-卡布奇诺的由来
  15. 用python 画一个美国队长盾牌
  16. 十三个大数据学习网站
  17. 蓝桥杯真题python B试题 C: 纸张尺寸
  18. ElementUI折叠组件bug问题及解决
  19. オフショア開発を成功させる工夫10点
  20. 《电信3.0——第二章》

热门文章

  1. 【易创课堂·广州站】运营+融资+大数据,创业与技术坑点一步迈过
  2. ICML 2018大奖出炉:伯克利、MIT获最佳论文,复旦大学榜上有名
  3. Android组件之BroadCast简单实践
  4. 整理就知识,你会在里面发现新的东西
  5. eBay再度问路中国
  6. 自制爬虫例--抓取网站图像与简介
  7. 答TOGAF企业架构的一些问题
  8. 惊了!Chrome浏览器竟然自带的一款恐龙游戏【免下载】
  9. Windows 中进程的相关操作
  10. win11 wsl centos7换源aliyun阿里云命令记录