Go: IM系统技术架构梳理 (2)

概述

  • 整个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) {// 处理 eventdata = 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.mzph.cn/web/46965.shtml

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

相关文章

WebGL-编译报错,如何定位sendfile报错位置

1)WebGL-编译报错,如何定位sendfile报错位置 2)设置DepthBufferBits和设置DepthStencilFormat的区别 3)Unity打包exe后,游戏内拉不起Steam的内购 4)使用了Play Asset Delivery提交版本被Google报错 这是第3…

前端组件化技术实践:Vue自定义顶部导航栏组件的探索

摘要 随着前端技术的飞速发展,组件化开发已成为提高开发效率、降低维护成本的关键手段。本文将以Vue自定义顶部导航栏组件为例,深入探讨前端组件化开发的实践过程、优势以及面临的挑战,旨在为广大前端开发者提供有价值的参考和启示。 一、引…

R语言画散点图-饼图-折线图-柱状图-箱线图-直方图-曲线图-热力图-雷达图

R语言画散点图-饼图-折线图-柱状图-箱线图-直方图-曲线图-热力图-雷达图 散点图示例解析效果 饼图示例解析效果 折线图示例解析效果 柱状图示例解析效果 箱线图示例解析效果 直方图示例解析效果 曲线图使用 curve() 函数示例效果 使用 plot() 函数示例效果 使用 ggplot2 包绘制…

super和this的作用与区别(java)

目录 (一)super关键字 (1)super的作用 (2)super的用法 2.1:super调用父类成员变量 2.2super调用父类成员方法 (3)super()的使用 (4)super注意…

NAS新品“翻车”后,绿联科技要上市了

在消费电子市场回暖的东风中,又一消费电子知名企业登陆A股。 近日,深圳市绿联科技股份有限公司(下称“绿联科技”)开启申购,将在创业板上市。本次上市,绿联科技的发行价为21.21元/股,发行数量为…

揭秘!Shopee/Lazada自养号测评高效法,三大策略助力商家快速出单

在东南亚电商的版图中,Lazada凭借其庞大的市场覆盖网络及卓越的服务品质,成功吸引了无数商家与消费者的瞩目。然而,对于渴望在Lazada平台上大展宏图的商家而言,出单的难易程度成为了一个值得深入探讨的议题。 一、Lazada出单的挑战…

泰迪科技2024年高校(本科/职业院校)大数据实验室建设及大数据实训平台整体解决方案

高校大数据应用人才培养目标 大数据专业是面向信息技术行业,培养德智体美劳全面发展的大数据领域的高素质管理型专门人才,毕业生具备扎实的管理学、经济学、自然科学、技术应用、人文社科的基本理论, 系统深入的大数据管理专业知识和实践能力&#xff0c…

JavaEE (1)

web开发概述 所谓web开发,指的是从网页中向后端程序发送请求,与后端程序进行 交互. 流程图如下 Web服务器是指驻留于因特网上某种类型计算机的程序. 可以向浏览器等Web客户端提供文档,也可以放置网站文件,让全世界浏览; 它是一个容器&…

FPGA-计数器

前言 之前一直说整理点FPGA控制器应用的内容,今天就从计数器这个在时序逻辑中比较重要的内容开始总结一下,主要通过还是通过让一个LED闪烁这个简单例子来理解。 寄存器 了解计数器之前先来认识一下寄存器。寄存器是时序逻辑设计的基础。时序逻辑能够避…

细说MCU用DMA控制ADC采样和串口传送的实现方法

目录 一、建立工程 1.相同的配置 2.配置ADC 3.配置DMA 二、代码修改 1.定义存储ADC采样结果的数组 2.启动ADC与定时器 3.编写主程序代码 4.重定义回调函数 5.查看结果 三、修改DMA模式 1. 修改DMA模式为Circular 2.查看结果 采用DMA(Direct Memory Access&#xf…

WebRTC QOS方法十三.1(TimestampExtrapolator接收时间预估)

一、背景介绍 虽然我们可通过时间戳的差值和采样率计算出发送端视频帧的发送节奏,但是由于网络延迟、抖动、丢包,仅知道视频发送端的发送节奏是明显不够的。我们还需要评估出视频接收端的视频帧的接收节奏,然后进行适当平滑,保证…

卷积神经网络【CNN】--池化层的原理详细解读

池化层(Pooling Layer)是卷积神经网络(CNN)中的一个关键组件,主要用于减少特征图(feature maps)的维度,同时保留重要的特征信息。 一、池化层的含义 池化层在卷积神经网络中扮演着降…

JavaScript与DOM的奇妙探险:从入门到精通的实战笔记

文章目录 JavaScript基本说明特点两种使用方式在script中写使用script标签引入JS文件 数据类型介绍特殊值 运算符算数运算符赋值运算符逻辑运算符:![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/bbf5c150699845af837d3c45c926e941.png)条件运算符 数组的…

Java_Docker

镜像和容器: 镜像仓库: 存储和管理镜像的平台,镜像仓库中有非常多常用软件的镜像,Docker官方维护了一个公共仓库​​​​​​:​Docker Hub 部署MySQL: docker run -d \--name mysql \-p 3306:3306 \-e TZAsia/Shang…

Guns v7.3.0:基于 Vue3、Antdv 和 TypeScript 打造的开箱即用型前端框架

摘要 本文深入探讨了Guns v7.3.0前端项目,该项目是基于Vue3、Antdv和TypeScript的前端框架,以Vben Admin的脚手架为基础进行了改造。文章分析了Guns 7.3.0的技术特点,包括其使用Vue3、vite2和TypeScript等最新前端技术栈,以及提供…

如何防止热插拔烧坏单片机

大家都知道一般USB接口属于热插拔,实际任意带电进行连接的操作都可以属于热插拔。我们前面讲过芯片烧坏的原理,那么热插拔就是导致芯片烧坏的一个主要原因之一。 在电子产品的整个装配过程、以及产品使用过程经常会面临接口热插拔或者类似热插拔的过程。…

IDEA的工程与模块管理

《IDEA破解、配置、使用技巧与实战教程》系列文章目录 第一章 IDEA破解与HelloWorld的实战编写 第二章 IDEA的详细设置 第三章 IDEA的工程与模块管理 第四章 IDEA的常见代码模板的使用 第五章 IDEA中常用的快捷键 第六章 IDEA的断点调试(Debug) 第七章 …

Redis的AOF持久化策略(AOF的工作流程、AOF的重写流程,操作演示、注意事项等)

文章目录 缓冲AOF 策略(append only file)AOF 的工作流程AOF 缓冲区策略AOF 的重写机制重写完的AOF文件为什么可以变小?AOF 重写流程 缓冲AOF 策略(append only file) AOF 的核心思路是 “实时备份“,只要我添加了新的数据或者更新了新的数据&#xff0…

问题:4、商业保险与政策性保险的主要不同之处是:经营主体不同、经营目标不同、承保机制不同。 #学习方法#其他#学习方法

问题:4、商业保险与政策性保险的主要不同之处是:经营主体不同、经营目标不同、承保机制不同。 参考答案如图所示

Linux云计算 |【第一阶段】ENGINEER-DAY3

主要内容: LVM逻辑卷管理、VDO、RAID磁盘阵列、进程管理 一、新建逻辑卷 1、什么是逻辑卷 逻辑卷(Logical Volume)是逻辑卷管理(Logical Volume Management,LVM)系统中的一个概念。LVM是一种用于磁盘管理…