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

简要介绍

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-layout

cmd:          服务启动入口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,不支持离线消息同步。

核心流程时序图

长连接登录

54f10824b6e13043969b35d0feca5874.png

离线消息同步

b3798b50e347a1a9ba12213b151ed5b9.png

心跳

f8b329f23749571294c78d1bab73793c.png

消息单发

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

2106f69d13ff331d530bb4d84318e8d4.png

小群消息群发

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

5547a15f6b41a516dfa417cf56b1298d.png

大群消息群发

b7f1daa1edec915d260fd838f1f5363a.png

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

系统中的错误一般可以归类为两种,一种是业务定义的错误,一种就是未知的错误,在业务正式上线的时候,业务定义的错误的属于正常业务逻辑,不需要打印出来, 但是未知的错误,我们就需要打印出来,我们不仅要知道是什么错误,还要知道错误的调用堆栈,所以这里我对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 ", 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-- " + 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/530070.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

html 手机分辨率,移动端各种分辨率手机屏幕----适配方法集锦

1.使用viewport&#xff0c;html文件中在内添加meta&#xff0c;简单粗暴&#xff1a;2.加判断复杂点的viewport&#xff0c;可以封装成一个js&#xff0c;每次新建html时调用.js即可&#xff1a;var phoneWidth parseInt(window.screen.width);var phoneScale phoneWidth/75…

橡胶支座抗压弹性模量计算公式_隔震支座的这些力学特性您都掌握了吗?

隔震支座叠层橡胶隔震支座&#xff0c;是隔震设计中应用最为广泛的隔震装置。了解隔震支座的力学特性&#xff0c;在软件中设置正确的参数&#xff0c;是做好隔震设计最基本的前提。那么&#xff0c;隔震支座的这些特性您都掌握了吗&#xff1f;叠层橡胶隔震支座叠层橡胶隔震支…

asp.net button创建控件时出错_Tkinter Radiobutton控件

单选按钮是一种可在多个预先定义的选项中选择出一项的 Tkinter 控件 。单选按钮可显示文字或图片&#xff0c;显示文字时只能使用预设字体&#xff0c;该控件可以绑定一个 Python 函数或方法&#xff0c;当单选按钮被选择时&#xff0c;该函数或方法将被调用。单选按钮&#xf…

计算机师范类算师范教育类吗,师范教育类专业和计算机类专业,两者相比,哪个更适合自考生报读...

原标题&#xff1a;师范教育类专业和计算机类专业&#xff0c;两者相比&#xff0c;哪个更适合自考生报读一、师范教育类专业大部分毕业生的就业方向都是在教育行业&#xff0c;互联网的发展&#xff0c;教学教育有了网络课&#xff0c;让人们随时随地都可以学习&#xff0c;也…

android 绘画笔迹回放_随时记录分享书写笔迹,EverPEN高级版套装体验

看到笔记本就想起小时候&#xff0c;老师为了提高学生们的写作能力&#xff0c;而要求大家每周写3篇日记&#xff0c;几年下来慢慢的让自己养成了每周会抽出点时间&#xff0c;手写些生活中的点滴趣事。但随着笔记本保持时间有限&#xff0c;而且有时查找起来也不方便&#xff…

12c集群日志位置_Kubernetes(k8s)那些套路之日志收集

准备关于容器日志Docker的日志分为两类&#xff0c;一类是 Docker引擎日志&#xff1b;另一类是容器日志。引擎日志一般都交给了系统日志&#xff0c;不同的操作系统会放在不同的位置。本文主要介绍容器日志&#xff0c;容器日志可以理解是运行在容器内部的应用输出的日志&…

数字摄像头测试软件,图像测量软件(Camera Measure)

Camera Measure是一款简单易用的数字图像测量工具&#xff0c;该软件可用于显微镜图像测量、测绘等专业领域或者普通用户日常的各种测量&#xff0c;可以播放Windows相机并拍照或录制视频&#xff0c;打开图片或视频文件&#xff0c;并在画面中进行实时的高性能图像测量。软件功…

过滤特征_机器学习深度研究:特征选择中几个重要的统计学概念

机器学习深度研究&#xff1a;特征选择过滤法中几个重要的统计学概念————卡方检验、方差分析、相关系数、p值问题引出当我们拿到数据并对其进行了数据预处理&#xff0c;但还不能直接拿去训练模型&#xff0c;还需要选择有意义的特征&#xff08;即特征选择&#xff09;&am…

win10计算机优化技巧,让Win10系统运行更流畅的优化技巧

虽然Win10系统对配置要求不高&#xff0c;在普通条件电脑的环境中也能流畅运行。但是用户总不会介意进一步优化Win10&#xff0c;让Win10在流畅的基础上变得更流畅一些。本文就来介绍一下让Win10系统更流畅的优化技巧。Win10优化技巧1、用360优化win10后开不了机的问题原因是禁…

c include 多层目录_python+C、C++混合编程的应用

TIOBE每个月都会新鲜出炉一份流行编程语言排行榜&#xff0c;这里会列出最流行的20种语言。排序说明不了语言的好坏&#xff0c;反应的不过是某个软件开发领域的热门程度。语言的发展不是越来越common&#xff0c;而是越来越专注领域。有的语言专注于简单高效&#xff0c;比如p…

校友会2019中国大学计算机,校友会2019中国计算机类一流专业排名,清华大学排名第一...

原标题&#xff1a;校友会2019中国计算机类一流专业排名&#xff0c;清华大学排名第一中国哪些高校的计算机类本科专业跻身2019世界一流专业、中国顶尖专业和中国一流专业行列&#xff1f;哪些计算机类本科专业是2019年中国高考最优秀考生的最佳选择&#xff1f;为了给2019年全…

查询结果取交集_Elasticsearch 查询过程中的 prefilter 原理

大家都知道在对索引执行查询的时候&#xff0c;需要在所有的分片上执行查询&#xff0c;因为无法知道被查询的关键词位于哪个分片&#xff0c;对于全文查询来说诚然如此&#xff0c;然而对于时序型的索引&#xff0c;当你从 my_index-* 中执行 now-3d 的范围查询时&#xff0c;…

语音识别插件_AnsweringMachine XS: 越狱理由之二,iPhone 电话语音答录机

Apps & Tweaks| Jailbreak Guide| iDevicesTweak&#xff1a;AnsweringMachine XSVersion&#xff1a;XSRepo&#xff1a;http://limneos.net/iOS Support&#xff1a;12-13Price&#xff1a;3.99iOS 开发人员 Elias Limneos 开发了电话辅助系列插件&#xff0c;AnsweringM…

计算机账务处理流程图,账务处理流程图

手工业务流程图账务处理流程主要有 5 种形式&#xff1a;记账凭证核算形式、科目汇总表核算形式、汇总记账凭证核算形式、日记总账核算形式、和多栏式日记账核算形式。不同的账务处理流程其差别主要体现在登记总账的方法和依据不同&#xff0c;其中科目汇总表核算形式最为常见&…

css滑动门的用处,CSS滑动门是什么?有什么用处?[web前端培训]

在制作网页导航时&#xff0c;经常会碰到导航栏长度不同&#xff0c;但背景相同的情形。此时如果通过拉伸背景图的方式来适应文本内容&#xff0c;就会造成背景图变形。在制作网页时&#xff0c;为了使各种特殊形状的背景能够自适应元素中的文本内容&#xff0c;并且不会变形&a…

Gen系列服务器,新计算、新体验 | 新华三全新HPE Gen10系列服务器响彻“云”端

数字经济时代的数据中心正在加速向云计算融合&#xff0c;用户将面临传统架构与云架构并存的混合IT模式。如何既拥有专有数据中心对数据完全可控以及对关键业务充分优化的优势&#xff0c;又能拥有云计算的灵活弹性&#xff1f;如何有效利旧并满足混合IT架构的需求&#xff1f;…

收藏功能_微软Edge获得了新的收藏夹菜单、PDF功能等

作为其今年早些时候概述的战略的一部分&#xff0c;微软Edge现在正在向所有Windows 10 PC推出。与经典的Edge不同&#xff0c;Chromium Edge与任何特定的Windows更新无关&#xff0c;但微软又开始为该浏览器进行了一系列令人兴奋的改进。新的Edge基于Chromium&#xff0c;它还带…

服务器LIMIT是什么信号,Postfix添加milter-limit配置方案

[安装环境]操作系统&#xff1a;CentOS 5.6MAT&#xff1a;POSTFIX2.8.4安装之前必须保证POSTFIX能正常收发信如果已经安装过Berkeley Db3以上版本可以不安装新的DB(但是注意引入db.so)[安装步骤]1、milter-limit-0.14.tar.gz及libsnert-1.71.6.tar.gz包的获取方法需要创建一个…

数据存储方式_视频监控系统的数据存储方式的概念及应用

DAS&#xff1a;直连存储&#xff0c;直连式存储与服务器主机之间的连接通常采用SCSI连接&#xff0c;SCSI通道是IO瓶颈;服务器主机SCSI ID资源有限&#xff0c;能够建立的SCSI通道连接有限。无论直连式存储还是服务器主机的扩展&#xff0c;从一台服务器扩展为多台服务器组成的…

vue从url中获取token并加入到 请求头里_轻流amp;amp;企业微信——获取打卡数据...

企业微信开放了打卡应用的api&#xff0c;功能包括查询打卡数据&#xff0c;能获取到用户、地点、时间、打卡类型等信息&#xff0c;在轻流中可以基于以上数据做一段时间内的迟到/事假等统计&#xff0c;以及更深层数据处理&#xff0c;方便管理。第一步&#xff1a;获取access…