# im **Repository Path**: zhou-lincong/im ## Basic Information - **Project Name**: im - **Description**: 用go打造支持同时10万100万人在线的IM系统 - **Primary Language**: Go - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-12-30 - **Last Updated**: 2023-04-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # im #### 介绍 用go打造支持同时10万、100万人在线的IM系统 1. 为什么要做IM? + 是社交、电商、办公的标配,如微信、QQ。 + 技术含量高,当出现高并发场景时,对于大量数据的同时处理能力要求非常高,如果不能在短时间处理,会给用户带来长时间的延迟,造成不好的使用体验,所以这个产品用户会对性能和体验非常敏感。 + 物联网要求实时通信,IM是实时推送技术的代表。 2. 功能 + IM可以发送文字、表情包、图片、语音、视频 + 扩张,实现红包、表单 3. 实现并发及性能调优 + websocket + golang并发优化使用 4. 分布式部署 + 方案 #### 软件架构 1. 一般架构: ![](./README-STATIC/IM%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84.png) 2. 实现了的以及重点: ![](./README-STATIC/%E5%B7%B2%E7%BB%8F%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%8A%9F%E8%83%BD.png) 3. 网络结构: ![](./README-STATIC/%E7%BD%91%E7%BB%9C%E7%BB%93%E6%9E%84.png) 4. 前端 vue: 制作单页app ajax: 发送图片和语言和上传 h5: 获取音频/websocket发送消息 mui/css3/js 5. 后端 websocket channel/goroutine templete: 模板渲染,可以有助于项目的组件化管理、层次分明 反向代理Nginx 消息总线MQ/Redis Udp/Http2协议 #### 安装教程 1. websocket github.com/gorilla/websocket 案例比较多,推荐使用 golang.org/x/net/websocket + 1.1 安装 cd $GOPATH mkdir -p $GOPATH/src/golang.org/x/net cd $GOPATH/src/golang.org/x/net go get -u github.com/golang/net/websocket go get github.com/gorilla/websocket + 1.2 本项目依赖`x/net`,`x/time`包可能会被墙,使用如下指令可以安装: mkdir -p $GOPATH/src/golang.org/x/ cd $GOPATH/src/golang.org/x/ git clone https://github.com/golang/net.git net git clone https://github.com/golang/time.git time 2. 其他包依赖 go get github.com/go-xorm/xorm go get github.com/gorilla/websocket go get gopkg.in/fatih/set.v0 go get github.com/go-xorm/xorm 3. xxxx #### 使用说明 1. xxxx 2. xxxx 3. xxxx #### 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request #### 开发过程 1. 需求分析10% 将一些业务表象抽象成具体实现的模块,如发送文字、图片,这可列为发送模块。 + 1.1 基本需求: - 发送、接收 - 实现群聊 - 高并发=单机最好+分布式+弹性扩容 - 功能页面: '好友'、'群聊'、'我的'、发送/接收的显示界面(聊天界面)--自己发送接收消息的样式、对方发送接收消息消息的样式。 + 1.2 实现资源标准化编码 - 资源信息采集并标准化,将本地资源转化成可以访问的资源,转化成content/url。如发送图片,先点开相册,点击图片,然后点击发送到服务器,服务器再返回一个链接,这个链接就是标准化的结果。 - 标准化以后,进行资源编码,最终目标都是拼接一个消息体(json/xml)。如拿到文字链接后,需要将它编码到一个消息体中去,最后通过API转发到群聊或者单聊。 ![](./README-STATIC/%E8%B5%84%E6%BA%90%E6%A0%87%E5%87%86%E5%8C%96.png) + 1.3 确保消息体的可扩展性 - 兼容基础媒介如图片文字语音(url/picture/content)。 - 能承载大量新业务,扩张不能对现有业务产生影响。 - 红包、打卡、签到等本质上是消息内容不一样。 + 1.4 接收消息并解析显示 - 接收到消息体(json)并进行解析,如发了文字过来显示文字、发了图片过来就显示图片。 - 区分不同显示形式(图片、文字、语音),如点击图片,可以进行编辑,是否查看大图、是否保存等操作。如点击语音,可以播放语音的操作。 - 界面显示自己发的和别人发的,如自己发送的在右端,显示绿色底,别人发送的在左端,显示白色底。 + 1.5 群聊的特殊需求 - 和单聊在基础功能上无区别。 - 1条消息多个参与群聊的终端及时接收到,对服务器的压力和流量是一个大的挑战。 - 服务器负载分析(流量计算)。 - a发送图片512K - 100人在线群人员同时接收到512kb×100=1024kb×50=50M - 1024哥群就是50M×1024=50G - 解决方案 - 使用缩略图(51.2k),提高单图下载速度、渲染速度。(所以需要再单独支持查看原图的功能)。 - 使用资源分离的方式(应用服务和资源服务对性能的要求不一样,资源服务没有太多计算的因素在里面,而应用服务需要处理很多逻辑计算、就对cpu的要求比较高,所以采用将应用服务和资源服务分离),可以将资源服务存储在本地oss或云服务(qos/alioss),提高资源服务并发能力,可以达到100ms以内的响应。 - 使用压缩消息体,发送文件路径而不是整个文件。 + 1.6 高并发 - 单机并发性能最优,如没有优化过的代码只能并发1万,优化后可以支持10万。 - 海量用户分布式部署,如当业务扩张,一台服务器已经支撑不了了,需要增加服务器。 - 应对突发事件弹性扩容,如群里面平时没人聊天,突然一个火爆的新闻导致很多人加进来聊天,可以使用云平台做弹性扩容。 2. 重点20% 如前端怎么获取语音信号,前端怎么发送文字、图片, 后端怎么通过websocket群发、转发信息, 3. 功能实现60% 4. 上线部署10% 通过脚本,降低工作量 #### 进入开发 1. 消息 + 1.1 消息体对象 ```golang type Message struct { // 消息ID Id int64 `json:"id,omitempty" form:"id"` // 谁发的 Userid int64 `json:"userid,omitempty" form:"userid"` // 群聊还是私聊 Cmd int `json:"cmd,omitempty" form:"cmd"` // 对方ID/群ID Dstid int64 `json:"dstid,omitempty" form:"dstid"` // 消息样式 Media int `json:"media,omitempty" form:"media"` // 消息的内容 Content string `json:"content,omitempty" form:"content"` // 预览图片 Pic string `json:"pic,omitempty" form:"pic"` // 服务的url Url string `json:"url,omitempty" form:"url"` // 简单描述 Memo string `json:"memo,omitempty" form:"memo"` // 和数字相关的 Amount int `json:"amount,omitempty" form:"amount"` } ``` + 1.2 群聊的逻辑 如果是群聊,dstid就是群的id,通过群id获取所有加入这个群的用户的id,然后通过用户id获取conn + 1.3 消息接收 ```golang for { // 阻塞等待直到有数据发送过来,用message承接 _,message,err := conn.ReadMessage() // 将json字符串数据message解析成Message类型的msg json.Unmarshal(message,&msg) // go dispatch(msg) } ``` + 1.4 消息发送 ```golang // 要先将对象json转化成[]byte类型msg // 然后发送 conn.WriteMessage(websocket.TextMessage,msg) ``` + 1.5 前端js打开websocket ```golang // 火狐,chrome url就是/chat?id={userid}&token=xxx var websocket = new WebSocket(url) // 如果url打开成功,就打开事件回调 websocket.onopen=function(ev){ // 启用心跳保证不被回收(如果没有数据在传输,连接可能会被回收) // 1.每30秒发送一次 2.距离最近一次发送30s后发出一次 // 心跳机制还会影响服务器的负载能力,比如线下有一批设备全停电了,当统一上电,会几乎在同一时间连上来,这时候服务器在这一刻承受的负担是比较大的。但启用心跳机制,比如1到5秒是这10台设备进行连接,5到10秒时另外10台设备,均匀的上线会让服务器的负担降低。 } ``` + 1.6 前端发送消息 ```golang websocket.send(data) // msg对象做json序列化转成字符串 data = JSON.stringify(msg) ``` ```js // 有些消息有顺序要求,可以使用队列发送 var dataqueue=[] function push(m) { if(!dataqueue) {dataqueue=[]} dataque.push(m) } function pop() { if (!!dataqueue) { return dataqueue.shift(); }else{ return null } } ``` + 1.7 前端消息发送的格式 ![](./README-STATIC/%E5%89%8D%E7%AB%AF%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF-%E6%A0%BC%E5%BC%8F.png) + 1.8 前端接收消息 ```js // 回调函数event里面传入了一个事件 websocket.onmessage = function(event) { // 处理env } // event.data对应后端发送过来的数据,用JSON.parse将其转化成一个对象,可以使用'data.'获取属性 data = JSON.parse(event.data) ``` + 1.9 整个流程:a如何发送消息给b? - 1、a尝试打开websocket,路径/chat?id=xxx&token=yyy - 2、后端通过鉴权,建立userid=>websocket的映射 - 3、启用协程,通过conn.ReadMessage等待和读取消息 - 4、a发送json字符串消息,里面携带了目标用户dstid - 5、如果是群消息,则分解成群用户ID - 6、后端通过ClientMap[userid]获得目的用户的conn - 7、connWriteMessage 2. 鉴权 + 2.1 鉴权/成功 系统里面的用户才能接入聊天系统,不是系统里面的用户需要拒绝接入和发送消息。 ![](./README-STATIC/%E6%8E%A5%E5%85%A5%E9%89%B4%E6%9D%83%E6%88%90%E5%8A%9F.png) 有两个参数,一个是id相当于qq号,一个是token,是每个用户在登录时候产生的唯一标识。 鉴权思路就是id和token均与数据库中存储的数据匹配,才能成功。 + 2.2 鉴权/失败 不一致就返回403: ![](./README-STATIC/%E6%8E%A5%E5%85%A5%E9%89%B4%E6%9D%83%E5%A4%B1%E8%B4%A5.png) + 2.3 用户信息表 ![](./README-STATIC/%E6%8E%A5%E5%85%A5%E9%89%B4%E6%9D%83-%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF%E8%A1%A8.png) + 2.4 conn ```golang conn,err :=(&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { // 鉴权 通过*http.Request取得id和token /chat?id={userid}&token=xxx // 去数据库查询 如果id和token匹配鉴权成立。返回true,code=200 // 否则返回false,code=403 }}.Upgrade(w,req,nil)) ``` - conn的维护 ```golang // userid => conn的映射 // ClientMap map[int64]*websocket.Conn type ClientNode struct { Conn *webcoket.Conn // ... 还可能包括用户的头像、昵称等 } // 考虑到并发,map的维护需要加锁 var ClientMap map[int64]*ClientNode = make(map[int64]*ClientNode) ``` 3. 单机支持高并发 + 3.1 设计高质量代码 - 优化map使用读写锁 对map的一个频繁读写,就会有一个安全问题,所以需要加锁。 读写锁的适用场景是,读的次数非常多,写的比较少。在这里就比较切合业务。 IM里面,写的次数就是用户的接入,读的次数会比较多,因为群发需要通过map获得用户的conn连接。 - map不要太大 一个map维护10万个用户已经可以了,太多没有意义。 + 3.2 突破系统瓶颈优化连接数 - Linux系统还是windows系统 普遍认为Linux系统在综合方面优于windows系统,更适合生产环境。 - 优化最大文件数 Linux里面有一个最大文件数,需要解除掉。 + 3.3 降低对CPU资源的使用 - 降低json编码频次 - 尽量一次编码多次使用 + 3.4 降低对io资源的使用 - 合并写数据库次数 访问数据库的过程是相对耗时多的,次数多的时候就能体现出来。如1秒写5次可以合并5秒写一次。 - 优化对数据库读操作,能缓存的就缓存 可以将一些用户的头像等存在数据库的资源进行缓存,避免多次读数据库 + 3.5 应用/资源服务分离 - 系统提供动态服务 如用户注册、用户登录。 - 文件/资源服务迁移到云服务oss 比如图片等静态资源梵高云服务上。 4. web http编程核心API ```go // 请求格式和处理函数绑定 func HandleFunc( // 请求的格式,如http://localhost/user/login pattern string, // 处理函数 handler func(ResponseWriter,*Request) ) // 启动服务器 func ListenAndServe( // 地址:如8080 addr string, // handler Handler ) ```