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;叠层橡胶隔震支座叠层橡胶隔震支…

综合基础知识计算机,计算机综合基础知识总结.doc

计算机综合基础知识总结计算机专业知识试题计算机专业知识试题1.???? 一个字节包含的二进制位数是(?? )。A. 8位???? B. 16位???????????????? C. 32位???????????????? D. 256位2.???? 既可作为输入又可作为输出设备的是(???…

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

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

从网络访问计算机不能添加用户名,windows7 和 XP 能互相用\\ip访问,但是不能通过计算机名访问...

只要你的二台机器(winxp、win7)都能PING通对方的IP地址&#xff0c;并且能够通过IP正常访问&#xff0c;只是不能通过同一工作组的计算机名访问(其实&#xff0c;你先PING过一次要使用的对方的计算机名&#xff0c;就能直接在地址栏中输入\\计算机名 来访问了 只是在 网络 中仍…

jitter 如何优化网络_抖音优化该怎么做?

抖音在国内是比较知名的短视频运营商&#xff0c;也有很大的流量市场。这块优化好了&#xff0c;不管是对于用户还是企业来说&#xff0c;在品牌宣传上来说&#xff0c;作用还是蛮大的。抖音优化和电脑端优化&#xff0c;还是有所区别的&#xff0c;这里参考的是点赞数、评论以…

性价比高台式计算机,2021年台式电脑什么牌子好,性价比高?

大家都知道买台式电脑最主要是性价比高&#xff0c;用最少的价钱买到最好的性能配置&#xff0c;但那些大老牌子为了赚平民百姓的钱售价实在贵得太离谱了&#xff0c;性价比非常低今年什么台式电脑性价比最高&#xff1f;台式电脑配置推荐 组装电脑配置单 电脑配置方案-装机...…

golang 数组组合成最小的整数_golang数组-----寻找数组中缺失的整数方法

问题&#xff1a;由n-1个整数组成的未排序数组&#xff0c;元素都是1~n的不同整数&#xff0c;找出其中缺失的整数方法一&#xff1a;思路&#xff1a;是原数组的和 减去 丢失元素后的数组的和&#xff0c;就得到丢失的元素了代码如下&#xff1a;package mainimport ("er…

计算机专业线性代数教学大纲,《线性代数》课程教学大纲

一、课程简介线性代数是讨论代数学中线性关系经典理论的课程&#xff0c;它具有较强的抽象性与逻辑性&#xff0c;是高等学校工科本科各专业的一门重要的基础理论课&#xff0c;也是硕士研究生入学全国统一考试中必考的数学课程之一。由于线性问题广泛存在于科学技术的各个领域…

yuv420p 详解_图文详解YUV420数据格式

一.YUV格式与RGB格式的换算RGB 转换成 YUVY (0.257 * R) (0.504 * G) (0.098 * B) 16Cr V (0.439 * R) - (0.368 * G) - (0.071 * B) 128Cb U -( 0.148 * R) - (0.291 * G) (0.439 * B) 128YUV 转换成 RGBB 1.164(Y - 16) 2.018(U - 128)G 1.164(Y - 16) - 0.813…

计算机存储器发展历史,存储器及其发展历史与前景(4页)-原创力文档

存储器及其发展历史与前景高兴(电子信息科学与技术专业12-1班)摘 要&#xff1a;存储器件是计算机系统的重要组成部分&#xff0c;现代计算机的内存储器多采用半导体存储器。由于RAM的存取速度比ROM快的多&#xff0c;其成为内存的主要组成元件。内存发展主要经历了FP DRAM(快…

jq父级绑定事件的意义_用jQuery.delegate()将事件绑定在父元素上面

1.先看看官方的示例&#xff1a;$(document).ready(function(){$("div").delegate("button","click",function(){$("p").slideToggle();});});这是一个段落。请点击这里2.该方法的定义如下&#xff1a;$(selector).delegate(childSele…

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

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

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

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

flash 文件计算机课件,计算机软件及应用Flash.ppt

计算机软件及应用Flash.pptFlash CS3 提供了一种用幻灯片屏幕创建演示文稿的新方法。它可以将媒体放在幻灯片屏幕上&#xff0c;添加从其它幻灯片继承媒体的嵌套幻灯片&#xff0c;并且在运行时使用内置控件在幻灯片之间导航。其实&#xff0c;这和PPT基本模式一样。 6.8 Flash…

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…