Go: IM系统技术架构梳理

概述

  • 整个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
    • 这样就可以极大的降低资源服务对我们服务器的这样一个压力
    • 这样就能够提升我们整个系统的性能

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

区块链学习05-web3中solidity和move语言

Solidity 和 Move 语言的比较:Web3 开发中的两种选择 Solidity 和 Move 都是用于开发区块链平台智能合约的编程语言。它们具有一些相似之处,但也存在一些关键差异。 相似之处: Solidity 和 Move 都是图灵完备语言,这意味着它们可以表达计算…

Anything in Any Scene:无缝融入任何场景,实现逼真视频对象插入技术

现实世界的视频捕获虽然因其真实性而宝贵,但常常受限于长尾分布的问题,即常见场景过度呈现,而关键的罕见场景却鲜有记录。这导致了所谓的"分布外问题",在模拟复杂环境光线、几何形状或达到高度逼真效果方面存在局限。传…

CentOS配置时钟服务

一、ntp协议 1.1 基础 NTP(Network Time Protocol,网络时间协议)是用于同步计算机网络中各个设备时间的协议。 下面了解一下 ntp 的配置选项 1.) iburst 功能: 通过发送一组八个数据包来加速初始同步。 用法: server 0.pool.ntp.org i…

Python实现简单的ui界面设计(小白入门)

引言: 当我们书写一个python程序时,我们在控制台输入信息时,往往多有不便,并且为了更加美观且直观的方式输入控制命令,我们常常设计一个ui界面,这样就能方便执行相关功能。如计算器、日历等界面。 正文&a…

Docker安装RabbitMQ(带web管理端)

1.拉取带web管理的镜像 可以拉取rabbitmq对应版本的web管理端,比如:rabbitmq:3.9.11-management,也可以直接拉取带web管理端的最新版本 rabbitmq:management. docker pull rabbitmq:3.9.11-management 注意:如果docker pull ra…

Linux目录网络设置远程工具的使用

文章目录 Linux目录虚拟机⽹络配置查看⽹络信息修改⽹络配置信息 虚拟机管理操作远程⼯具的使⽤ Linux目录 Linux的⽬录结构 Linux中的常⻅⽬录 Linux常⻅的⽬录结构,不同版本的Linux⽬录结构可能略有不同 Centos7的⽂件⽬录结构 Linux根⽬录下的常⻅⽬录及作⽤ …

C语言之qsort函数

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

COLING 2024 | AlphaFin:基于LLM的股票预测大模型,显著提高预测能力

COLING 2024 | AlphaFin&#xff1a;基于LLM的股票预测大模型&#xff0c;显著提高预测能力 发布于 2024-06-13 18:31:49 目前&#xff0c;机器学习和深度学习算法&#xff08;ML&DL&#xff09;已被广泛应用于股票趋势预测&#xff0c;并取得了显著进展。然而&#xff0c…

小霸王游戏卡是用什么编程的?

小霸王游戏卡曾经很流行。以超级马里奥为例&#xff0c;超级马里奥免费在线网址&#xff1a;https://supermarioplay.com/cn 游戏画面如下图&#xff1a; 这款游戏的编程语言是什么了&#xff1f; 汇编6502

【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【25】【分布式事务】

持续学习&持续更新中… 守破离 【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【25】【分布式事务】 本地事务事务的基本性质事务的隔离级别&#xff08;下面四个越往下&#xff0c;隔离级 别越高&#xff0c;并发能力越差&#xff09;事务的传播行为&#xff08;是否…

Linux系统调优与日志管理全面指南

文章目录 一、文件存储基础1.1 文件与硬盘存储单位1.2 文件数据与元信息1.3 inode机制查看文件名对应的inode号码有两种方式: 1.4 inode与硬盘空间1.5 特有现象 二、inode节点耗尽故障处理2.1 模拟inode节点耗尽故障 三、恢复误删除的文件3.1 恢复误删除的ext3文件EXT类型文件恢…

Zynq7000系列FPGA中的DDRI和DDRC

在AXI接口设计中&#xff0c;主端口&#xff08;Master Port&#xff09;和从端口&#xff08;Slave Port&#xff09;的交互是通过仲裁器&#xff08;Arbiter&#xff09;来管理的&#xff0c;以确保多个主设备能够有序地访问共享资源&#xff08;如DDR内存&#xff09;。这个…

C++20中的constinit说明符

constinit说明符断言(assert)变量具有静态初始化&#xff0c;即零初始化和常量初始化(zero initialization and constant initialization)&#xff0c;否则程序格式不正确(program is ill-formed)。 constinit说明符声明具有静态或线程存储持续时间(thread storage duration)的…

捷配笔记-PCB阻焊颜色对产品有什么影响?

阻焊层也称为阻焊层或阻焊剂。它是一种薄的聚合物层&#xff0c;应用于&#xff08;PCB&#xff09;。阻焊层的目的是保护PCB表面&#xff0c;并有助于防止焊桥。焊桥是两个导体之间的无意连接&#xff0c;通常是由于存在一小块焊料。需要注意的是&#xff0c;阻焊层被视为其单…

书生大模型实战营--L0关卡-Linux

一、SSH登录并完成nvidia-smi查看显卡以及安装pip install gradio4.29.0 二、完成vscode连接远程服务器 三、运行http://127.0.0.1:7860/

HTML表格表单及框架标签

一.表格标签 1.<table></table> 创建表格 2.<caption></caption> 表格的标题 3.<tr></tr>Table Row&#xff08;表格行&#xff09; 4.<td></td>Table Data&#xff08;表格数据&#xff09;其中有属性rowspan"2&quo…

单点触摸屏和多点触摸屏介绍以及原理简略

单点和多点触摸屏技术是现代触摸设备的基础&#xff0c;下述简单解释这两种技术及其差异。 单点触摸屏 单点触摸屏只能在某一时刻检测一个触摸点的位置。这种触摸屏适用于简单的触摸交互&#xff0c;如点击和拖动。 工作原理 单点触摸屏主要通过以下几种技术实现&#xff1…

SQL server 练习题2

课后作业 作业 1&#xff1a;自己查找方法&#xff0c;将 homework_1.xls 文件数据导入到 SQLServer 的 homework 数据库中。数据导入完成后&#xff0c;把表名统一改为&#xff1a;外卖表 如下所示&#xff1a; 作业 2&#xff1a;找出所有在 2020 年 5 月 1 日至 5 月 31 …

【Redis从0到1进阶】Redis 持久化

笔记内容来自B站博主《遇见狂神说》&#xff1a;Redis视频链接 Redis 是内存数据库&#xff0c;如果不将内存中的数据库状态保存到磁盘&#xff0c;那么一旦服务器进程退出&#xff0c;服务器中的数据库状态也会消失。所有Redis 提供了持久化功能&#xff01; 一、RDB&#xff…

【256 Days】我的创作纪念日

目录 &#x1f33c;01 机缘 &#x1f33c;02 收获 &#x1f33c;03 日常 &#x1f33c;04 成就 &#x1f33c;05 憧憬 最近收到官方来信&#xff0c; 突然发现&#xff0c;不知不觉间&#xff0c;距离发布的第一篇博客已过256天&#xff0c;这期间我经历了春秋招、毕业答辩…