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
    • 这样就可以极大的降低资源服务对我们服务器的这样一个压力
    • 这样就能够提升我们整个系统的性能
  • 11
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值