Go: IM系统技术架构梳理

news2024/9/20 22:46:31

概述

  • 整个IM系统的一般架构如下
  • 我们这张图展示了整个IM系统的一般架构可见分为四层
  • 那最上面这一层是前端,包括哪些东西呢?
    • 它包括两部分,第一部分是跟用户直接交互的
    • 比如说各种IOS APP, 各种安卓 APP
    • 还有各种 web APP 在浏览器里面打开的
    • 以及windows上面跑的那种客户端
    • 第二部分是跟我们程序相关的SDK,API,Websocket
    • 这些我们都统称为前端
  • 第二个是接入层,这里展示了几种常用的接入协议
    • TCP,HTTPS, HTTPS2, Websocket
    • 实际上还会用到Mqtt, Xmpp 等各种协议
    • 这是接入层
  • 然后, 第三层是逻辑层,逻辑层里面比较熟悉的群聊单聊登录消息下发
    • 整个消息下发是整个系统应用的重点,
  • 最后,第四层是存储层,存储层,包括 Mysql,Redis, Mongodb 等
    • 这是用来做消息持久化用的,用来存储消息的历史记录
    • Hbase, Hive这些是我们做大数据存储用的
    • 当我们后面的数据量越来越大的时候可能会用到这是存储方式
    • 后面有一个文件服务器,为了提升系统的抗并发能力
    • 我们将应用服务跟文件服务相互分离
    • 一些服务器可能用第三方云来提供
    • 这样来提升我们系统的抗并发能力
  • 这就是我们整个IM系统的一般架构

网络结构

  • 我们整个网络结构也可以分为三部分
  • 第一部分是 Hybrid APP 浏览器各种微信环境,通过ws(s) 或 http(s) 协议, 接入到我们应用服务
  • 第二部分是应用服务,通过其他的网络途径读写我们的数据库(第三部分)
  • 这就是我们整个网络架构
  • 值得注意的是:
    • HTTP提供的是API服务比如说我们用户输入用名密码,点击登录这个调用的就是我们的HTTP服务。
    • 然后 websocket 提供的是长链接,比如说用户发送信息给对方,为了保持这个消息的及时性通过websocket 建立一个长链接,这个是用来做消息推送用的

Websocket 的使用


1 )选型

  • github.com/gorilla/websocket (生态案例多,推荐)
  • golang.org/x/net/websocket

2 )安装 gorilla 的 websocket

  • 注意,gorilla 包依赖 x/net 包,要先安装 x/net 包
  • 因为网络问题,x/net 包装不了,按照下面处理
  • $ cd $GOPATH
  • $ mkdir -p golang.org/x/net
  • $ cd golang.org/x/net
  • $ go get -u github.com/golang/net/websocket
  • $ go get github.com/gorilla/websocket

3 )Websocket 的鉴权

3.1 鉴权成功

3.2 鉴权失败

  • 如果是我们每个系统里面的用户,才能够接入聊天系统
  • 如果不是我们系统里面的用户,我们应该拒绝他,
  • 他不能对我们这个应用发送任何消息,这里涉及到鉴权的问题
  • 如上,这个鉴权有两个参数,一个是id,一个是token
  • id 相当于我们的QQ号,token 是每一个用户登录所产生的唯一的一个标识
  • 我们鉴权的思路就是
    • id跟token相互匹配,如果匹配,他就成功,这里往前走的就是101
    • 如果 id 跟 token 不一致, 我们返回的是 403
    • 也就是说以后这个链接,不能通过这个ws发送信息
    • 因为它拒绝了,还没有接到我们系统里面去
    • 这个就是鉴权失败的一个标识

4 ) 用户的基本结构

  • 主要关注 id 和 token, 其他的字段,我们不做过多的一个关注
  • 这是接入的用户信息表

5 ) 接入鉴权

  • 后端怎么实现这个鉴权的呢,如上,拿到了 id 和 token
  • 在这个 CheckOrigin 函数中,验证 id 和 token 是否匹配
  • 如果匹配返回 true ,对应200 (101);否则,返回false, 对应 403
  • 最后会拿到 conn,是我们每个客户端的标识,基于此来发送信息和读取信息

conn的维护

  • 我们怎么来维护这个 conn 呢?
  • 这里给出一个最简单的一个维护方案
    • userid 和 conn 形成一个映射关系
    • 最后形成一个map,这就是我们定义的 ClientMap
    • 可见,对应的 userid 是 int64 类型
  • 但是往往在实际的生产过程中,这些关系是远远不够的
    • 我们需要定义一个结构体,这个结构体包含这个conn
    • 也可能包含其他的一些属性,比如说用户的头像,昵称,性别
    • 这样的一些东西,放在这个 ClientNode 里面
    • 也就是 userid 和 ClientNode 然后形成一个映射关系
  • map的维护,还有其他一些技巧
    • 比如说这里面会加锁,以及其他的东西,考虑并发性的需求

6 )消息的发送

  • 消息发送之前,先回顾一下消息体的一个格式
  • Id 是消息的id
  • Userid 表示哪个用户发的
  • Cmd 是代表群聊还是私聊
    • 如果是群聊,Dstid 是 群id
    • 如果是私聊,Dstid 是 目标用户id
  • 如果 Dstid 是群id
    • 则需要通过 Dstid 获取加入群的所有用户id
    • 然后通过这个用户ID 获取我们的换取我们的 ClientNode, 拿到里面的 conn
  • 如果 Dstid 是userid, 这里就不赘述,参考上面获取 conn
  • 其他字段就先略过

7 )消息的接收

  • 启动 websocket 的时候,我们要启动一个协程
  • 在这个协程里面它是有一个循环,一个阻塞
  • readMessage一直在这里,等待一有消息发过来
  • 它就把这个数据读到 message 这个字符串里面
  • 然后再对这个字符串进行解析到 msg 类型的对象里面去
  • 这个就是我们最核心的API readMessage 写它readmessage
  • 最后注意这里有一个 go dispatch,也是也是利用协程的一个属性
  • 为了让我们整个系统啊跑起来更流畅

8 )消息的发送

  • 首先是需要将这个消息的Json对象转成 []byte 类型的 msg 字符串
  • 然后, 再通过这个 conn.WriteMessage(websocket.TextMessage, msg) 方法把这个 msg 输出
  • 注意这第一个参数是 TextMessage,代表这个地方是文本格式

9 ) 前端 JS 打开 websocket

// 火狐,chrome
var websocket = new WebSocket(url);

// 打开事件回调
websocket.onopen = function(ev) {
	// 启用心跳
}
  • 首先是通过 new WebSocket 方法, 传入 URL, 这个url 就是有userid 和 token 的url
  • 然后这里有一个onopen方法,如果我们打开成功了,它就会调用这个onopen方法, 这是一个事件回调
  • 在这个回调内启动心跳,什么是心跳呢?
    • 在传统的网络结构里面,前端跟后端通信的时候
    • 如果我们在一定时间内,比如说一百秒以内
    • 没有数据在这个管道里面传输,那么系统就认为这个网络已经是空闲状态了
    • 它会把这个网络回收掉,具体这个时间是多少,是由系统配置的
    • 有些服务器,它有这个参数可以配置的
    • 为了保持这个网络,是一个正常的状态,不让服务器回收它
    • 我们需要往里面发送一些特殊字符,这个服务器接收到,就认为这个网络还是连接的
    • 这样一个字符,就叫做心跳

10 )WS的心跳机制

  • 心跳应该是隔多长时间发一次,还有每次发心跳,要发什么样的一个格式
  • 我们这里有几种方案
    • 1 )隔30秒发一次,非常简单,非常机械的一个方案,但能达到目的
    • 但是我们不建议隔30秒发一次
    • 2 )我们建议在距离最近一次发送的时间30秒以内或者45秒或者自己设置秒来重发
    • 也就是说你最后一次发送的时候,我们将当前的最后一次发送时间记录下来
    • 然后随着这个时间增加,如果在30秒以内,比如说增加了27秒的时候,
    • 有数据发送有数据更新,那这时候我们可以将这个当前的最后一次发送这个时间清零, 这时候我们又从零开始往前计数
    • 这个就是最近一次发送的30秒这个有效范围内发送,这个是心跳机制 (重要)
  • 心跳机制在物联网应用里面,心跳设置的这时间间隔会影响你整个系统的复杂程度,也会影响整个系统的负载和抗并发的能力
  • 打个比方,如果线下有一批设备都停电了,后续统一都连上来了
  • 这时候服务器在一瞬间,各种数据,各种心跳一下子都来了,而且都在同样一个时间段过来了,服务器承受的负担是非常大的。
  • 但是如果它是非常均匀的啊,比如说一到五秒是十台设备
  • 五到十秒是另外十台设备,然后是非常均匀的部署,后端服务器的这个负担是非常轻的

11 )前端发送消息

data = JSON.stringify(msg对象)
websocket.send(data)

// 队列发送
  • 这里有一个API是 send,参数 data 是一个序列化之后的字符串
  • 这里还有一个技巧,就是用队列发送
  • 比如说有些消息是讲究时序的,也就是先后顺序
  • 可以对它进行用队列来发送,这里还有一个简单demo
  • 这里定义了一个数组叫做 dataqueue
  • push方法就是往这个队列里面添加数据
  • 然后pop就是把这个数据从队列里取出来
  • 可以通过一个 while true 的循环,不断的pop,然后那边添加进来push进去
  • 这就是我们简单的一个队列
  • 上面是消息发送的格式,对象序列化以后,就是上述字符串
  • 这里,content 是 你好, media 是 1 代表文字
  • 然后这个 cmd 是 10 代表点对点的单聊模式
  • userid 代表谁发的消息
  • dstid 就是目标用户id

12 )前端接收的消息

websocket.onmessage = function(event) {
	// 处理 event
	data = JSON.parse(event.data)
}

整个消息流程梳理


我们看下,A 如何发送消息给B?

1 )A 尝试打开 websocket,路径 /chat?id=xxx&token=sdsdfss
2 ) 后端通过鉴权,建立 userid => websocket 的映射
3 )启用协程,通过 conn.ReadMessage 等待和读取消息
4 )A 发送 Json 字符串消息,里面携带了目标用户 dstid
5 ) 如果是群消息,则分解成群用户ID,进行群发处理
6 ) 后端通过 ClientMap[userid]获得目的用户的 conn
7 ) conn.WriteMessage,这时候在存在连接的情况下就发送给客户端的B了

设计高质量代码


1 )优化Map

  • 现在,我们探讨下对单机性能的优化,如何支持高并发
  • 那为什么要用map呢?因为我们对map的频繁读写,就会导致map的一个安全性问题
  • 这里,我们需要加锁,典型的就是读写锁,读写锁非常适合的场景是什么?
    • 读的次数非常多,写的次数非常少的一个场景
    • 所以这跟我们的业务正好非常切合,我们写的场景就是用户的接入
    • 我们读的次数非常多,每次群发都需要通过这个map来获得用户的 conn
  • 我们map不能太大
  • 一个map为十万个用户, 已经非常不错了,维护一百万个用户是没有意义的
  • 这是我们的map相应的这一个优化

2 ) 突破系统的瓶颈, 优化最大的连接数

  • 典型的我们普遍认为 linux 系统会 优于 windows系统, 我们要把系统迁移到 linux
  • 但是,linux 下有一个最大的文件数,他会直接影响的我们网络的连接数
  • 我们要把最大的文件数先解除掉,这是我们对系统的一个优化

3 )优化这个CPU资源的使用率

  • 典型的占用CPU资源的编码,出现在对JSON的编码上
  • 每次我们对JSON进行编码占用的CPU资源非常多
  • 要尽量降低这种编码的使用频次,做到一次编码多次使用

4 )降低IO资源的使用

  • 怎么会降低IO资源的使用呢?比如说我们用户访问数据库,数据库连接需要时间
  • 另外数据库访问是一个非常耗时的过程,所以我们在这个过程中,整个IO也是处于占用状态
  • 那这应该怎么办呢?那很简单,我们需要合并这个写数据库的这个次数
  • 以前是一秒钟写五次,我们可以让他五秒钟写一次,只是把这个数据合并起来,再写
  • 以前每次要去数据库里面读数据,比如用户的头像信息,我们可以引入缓存
  • 这样就可以直接先去缓存中去查,降低对数据库的依赖,这是对IO资源的这样一个优化

5 )对应用服务和资源分布服务相互分离的这一个优化策略

  • 我们应用服务是指什么呢?
    • 是指提供动态的这样一个服务
    • 比如说用户注册,登录这样一个服务
  • 然后我们资源服务是指什么呢?
    • 是指我们的图片文件CSS等文件
    • 我们把这个资源服务部署到我们的云服务上,由第三方服务提供,比如 OSS
    • 这样就可以极大的降低资源服务对我们服务器的这样一个压力
    • 这样就能够提升我们整个系统的性能

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

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

相关文章

【爬虫】爬虫基础

目录 一、Http响应与请求1、Http请求2、Http响应3、状态码 二、Requests库1、发起GET请求2、发起POST请求3、处理请求头 三、BeautifulSoup库1、解析HTML文档2、查找和提取数据Ⅰ、查找单个元素Ⅱ、查找所有元素Ⅲ、使用CSS选择器Ⅳ、获取元素属性 四、爬取豆瓣电影榜 一、Http…

PBE注册/PBE汉化/美测服汉化/美测服下载/美测服注册教程

英雄联盟PBE美测服是一个可以提前参与游戏测试,提前享受英雄联盟或者云顶之弈新版本的服务器,我们可以更快的了解到游戏的更新内容,很多玩家都想要游玩pbe美测服,下面我就个idaj带来美测服汉化/美测服下载/美测服注册教程&#xf…

【动态规划Ⅰ】斐波那契、爬楼梯、杨辉三角

动态规划—斐波那契系列 什么是动态规划斐波那契数组相关题目509. 斐波那契数 Easy1137. 第 N 个泰波那契数 Easy 杨辉三角118. 杨辉三角 Easy 爬楼梯相关题目70. 爬楼梯 Easy746. 使用最小花费爬楼梯 Easy 什么是动态规划 动态规划是一种通过将原问题分解为相对简单的子问题来…

【代码规范】.train(False)和.eval()的相似性和区别

【代码规范】.train(False)和.eval()的相似性和区别 文章目录 一、.train(False) 和 .eval() 的功能二、.train(False) 和 .eval() 的区别2.1 .eval()2.2 .train(False)2.3 总结 三、.eval()更加规范 一、.train(False) 和 .eval() 的功能 .train(False) 和 .eval() 在功能上非…

STM32CubeMX配置STM32G071输入捕获(HAL库开发)

1.时钟配置HSI主频配置64M 2.配置好串口,选择异步模式 3.配置TIM1_CH1产生1KHz的信号,主频64MHz,分频(64-1),计数周期(1000-1),这样即可生成1KHz信号。 4.配置TIM3_CH1和…

Linux 07:基础IO

stdin & stdout & stderr C默认会打开三个输入输出流,分别是stdin, stdout, stderr。仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针。 文件读取函数(库函数): fopen、fread、…

2024 年你需要知道的免费 API-独立产品灵感周刊 DecoHack #061

本周刊记录有趣好玩的独立产品设计开发相关内容,每周发布,往期内容同样精彩,感兴趣的伙伴可以 点击订阅我的周刊。为保证每期都能收到,建议邮件订阅。欢迎通过 Twitter 私信推荐或投稿。 💻 产品推荐 1. Pintree 首先…

01- 收入数据集【Pytorch入门实战】

目录 一、机器学习基础 二、实战例子 1.数据集分析 2.实战训练 3.总结 三、参考资料 一、机器学习基础 为了解决这个问题,人们想到数据驱动方法,也就是让计算机从现有的大量的带标签图片电学习规律,一旦计算机学习到了其中的规律&…

【绝命Coding助力秋招】Python实现<实习僧>海投脚本

hello hello~ ,这里是绝命Coding——老白~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹 💥个人主页:绝命Coding-CSDN博客 &a…

异常、File

异常、File 异常 异常就是代表程序出现的问题 Error:代表的系统级别错误(属于严重问题)。系统一旦出现问题,sun公司会把这些错误封装成Error对象。Error是给sun公司自己用,不是给我们程序员用的。因此我们开发人员不…

SpringCloud--常用组件和服务中心

常用组件 Euroke和nacos 区别 负载均衡 负载均衡策略有哪些 自定义负载均衡策略

AV1 编码标准中帧内预测技术概述

AV1 编码标准帧内预测 AV1(AOMedia Video 1)是一种开源的视频编码格式,旨在提供比现有标准更高的压缩效率和更好的视频质量。在帧内预测方面,AV1相较于其前身VP9和其他编解码标准,如H.264/AVC和H.265/HEVC,…

自建文件分享平台Pingvin Share系统

Pingvin Share可以用来代替 WeTransfer 等的分享网站,如果你有自己的服务器,可以支持通过 Docker 部署或者 Stand-alone 部署。 功能介绍 完全自建:轻松使用私有服务器搭建文件共享平台 完全隐私:你的文件只属于你!不…

相交链表+判断环型链表+求环型链表的入口节点

链表OJ题 一.相交链表二.判断环型链表三.求环型链表的入口节点 一.相交链表 相交链表 相交:两个链表从头开始遍历,尾节点一定是同一个节点。 情况一:当两个链表长度相同时: 情况二:当两个链表长度不同时&#xff1…

【详解】Spring Cloud概述

🎥 个人主页:Dikz12🔥个人专栏:Spring学习之路📕格言:吾愚多不敏,而愿加学欢迎大家👍点赞✍评论⭐收藏 目录 1. 认识微服务 1.1 单体架构 1.2 集群和分布式架构 1.3 集群和分布式…

如何利用windows本机调用Linux服务器,以及如何调用jupyter界面远程操控

其实这篇文章没必要存在,教程太多了 参考博客(1 2 3),如侵删 奈何网上的大神总是会漏掉一些凡人遇到的小问题 (1) 建议下载PuTTy for windows,从而建立与远程服务器的SSH连接 需要确认目标服…

nodejs安装部署运行vue前端项目

文章目录 1.安装nodejs2.安装Vue CLI1.配置npm镜像源:2.安装Vue CLI:3.创建Vue项目4.启动Vue项目5.Express 1.安装nodejs Node.js 是一个免费、开源、跨平台的 JavaScript 运行时环境,它让开发人员能够创建服务器、Web 应用、命令行工具和脚…

【C++PythonJava】字符处理详细解读_字符_ASCLL码_字母数字转换_算法竞赛_开发语言

文章目录 Beginning1)ASCLL 码2)大小比较2)判断数字字符3)字符、数字间的相互转换End Beginning 在 C 中,字符和整数有着密不可分的关系。原因就是在计算机中,字符是以一种较 ASCLL 码的整数存储的。自然&…

SpringSecurity框架【认证】

目录 一. 快速入门 二. 认证 2.1 登陆校验流程 2.2 原理初探 2.3 解决问题 2.3.1 思路分析 2.3.2 准备工作 2.3.3 实现 2.3.3.1 数据库校验用户 2.3.3.2 密码加密存储 2.3.3.3 登录接口 2.3.3.4 认证过滤器 2.3.3.5 退出登录 Spring Security是Spring家族中的一个…

C语言之qsort函数

一、qsort 1.库函数qsort qsort是库函数&#xff0c;直接可以用来排序数据&#xff0c;底层使用的是快速排序。 qsort函数可以排序任意类型的数据。 2.头文件 #include<stdlib.h> 3.参数讲解 void*类型的指针是无具体类型的指针&#xff0c;这种类型的指针的不能直接解…