配套源码:本文写的虽然有点浅近但涉及内容不少,建议结合代码一起来读,文章配套的完全源码 请从本文文末 “11、完全源码下载” 处下载!
本站的另几篇同类代码你可能也喜好:
《自已开拓IM有那么难吗?手把手教你自撸一个Andriod版大略单纯IM (有源码)》

《拿起键盘便是干:跟我一起徒手开拓一套分布式IM系统》
《适宜新手:从零开拓一个IM做事端(基于Netty,有完全源码)》
其余:本文作者的另一篇文章,有兴趣也可以关注一下:《12306抢票带来的启迪:看我如何用Go实现百万QPS的秒杀系统(含源码)》。
本文已同步发布于“即时通讯技能圈”"大众号。
本文在"大众号上的链接是:https://mp.weixin.qq.com/s/ycC-25dkOwAVymHY6WHOEg
1、正文概述前阵子看了《创业时期》,电视剧的剧情大概是这样的:IT工程师郭鑫年与好友罗维与投行精英那蓝等人一起,踏上互联网创业之路。创业开拓的是一款叫做“魔晶”的IM产品。郭鑫年在第一次创业失落败后,离了婚,还欠了很多外债,骑着自行车经历了西藏一次死活诀别之后产生了灵感,想要创作一款IM产品“魔晶”,“魔晶”的初衷是为了增加人与人之间的感情。虽然剧情纯属虚构,但确实让人浮想QQ当初的设想是不是便是这样的呢?
有一点是可以确定的,即时通讯确实是一个时期的里程碑。腾讯的强大离不开两款产品:QQ和微信。这两款产品设计的思路是不一样的,QQ依托于IM系统,为了打造个人空间、全民娱乐而设计,我们常常会看到QQ被初高中生喜好,QQ账号也每每与音乐、游戏绑定在一起;微信从QQ导流往后,主打商业领域,从刚开始推出微信支付与支付宝竞争,在商业支付领域占得了一席之地(微信支付紧张被用户用于小额支付场景,支付宝紧张用在企业大额转账、个人金融理财领域)往后。微信又相继推出了"大众年夜众号、小程序,很明显在商业领域已经霸占了支付宝的上风,成为了商业APP中的霸主,后来才有了谈天宝、多闪和马桶三大门派围攻微信的闹剧,结果大家可能都知道了......
阿里依托于IM系统攻击办公领域,打造了“钉钉”。这又是一款比较精细的产品,个中打卡考勤、请假审批、会议管理都做的非常好,和微信不同的是,企业通过钉钉互换的信息,对方是能看到信息是否“已读”的(毕竟是办公,这个功能还是很有必要的)。腾讯也不甘示弱,创建“企业微信”,开始和“钉钉”正面交手,虽然在市场份额上还是掉队于钉钉,但用户增长很快。
企业微信于2016年4月发布1.0版本,也只有大略的考勤、请假、报销等功能,在产品功能上略显平淡。彼时再看钉钉,凭借先发上风,初期就确定的产品线“谄媚”老板,2016年企业数100万,2018年这个数量上升到700万,可见钉钉发展速率之快,稳固了钉钉在B端市场的地位。企业微信早期迟疑不决的打法,也让它在企业OA办公上玩不过钉钉。但企业微信在发布3.0版本后,局势开始旋转,钉钉在用户数量上彷佛已经饱和,难以有新的打破,而企业微信才真正开始逐渐霸占市场。
依托于IM系统发展起来的企业还有陌陌、探探。比较较与微信来讲,它们的功能更集中于交友和情绪。(不知道这是不是人家企业每年年终都人手一部iphone的缘故原由,开个玩笑)
笔者今年参加了一次Gopher大会,有幸听探探的架构师分享了它们今年微做事化的过程,本文快速搭建的IM系统也是利用Go措辞来快速实现的,这里先和各位分享一下探探APP的架构图:
以上讲了一些IM系统的产品方面不着边际的废话,下边我们回归主题,大概说一下本文的章节内容安排。
本文的目的是帮助读者较为深入的理解socket协议,并快速搭建一个高可用、可拓展的IM系统。同时帮助读者理解IM系统后续可以做哪些优化和改进。
本文的内容概述:
1)本文演示的IM系统包含基本的注册、登录、添加好友根本功能;
2)供应单聊、群聊,并且支持发送笔墨、表情和图片,在搭建的系统上,读者可轻松的拓展语音、视频谈天、发红包等业务。
2)为了帮助读者更清楚的理解IM系统的事理,第3节我会专门深入讲解一下websocket协议,websocket是长链接中比较常用的协议;
3)然后第4节会讲解快速搭建IM系统的技巧和紧张代码实现;
4)在第5节笔者会对IM系统的架构升级和优化提出一些建媾和思路;
5)在末了章节做本文的回顾总结。
2、干系文章更多实践性代码参考:
《开源移动端IM技能框架MobileIMSDK》( 推举)
《自已开拓IM有那么难吗?手把手教你自撸一个Andriod版大略单纯IM (有源码)》
《适宜新手:从零开拓一个IM做事端(基于Netty,有完全源码)》
《拿起键盘便是干:跟我一起徒手开拓一套分布式IM系统》
《一种Android端IM智能心跳算法的设计与实现磋商(含样例代码)》
《精确理解IM长连接的心跳及重连机制,并动手实现(有完全IM源码)》
《手把手教你用Netty实现网络通信程序的心跳机制、断线重连机制》
《NIO框架入门(一):做事端基于Netty4的UDP双向通信Demo演示 [附件下载]》
《NIO框架入门(二):做事端基于MINA2的UDP双向通信Demo演示 [附件下载]》
《NIO框架入门(三):iOS与MINA2、Netty4的跨平台UDP双向通信实战 [附件下载]》
《NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战 [附件下载]》
《一个WebSocket实时谈天室Demo:基于node.js+socket.io [附件下载]》
干系IM架构方面的文章:
《浅谈IM系统的架构设计》
《简述移动端IM开拓的那些坑:架构设计、通信协议和客户端》
《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》
《一套原创分布式即时通讯(IM)系统理论架构方案》
《从零到卓越:京东客服即时通讯系统的技能架构演进进程》
《蘑菇街即时通讯/IM做事器开拓之架构选择》
《一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践》
3、深入理解websocket协议3.1 简介WebSocket的目标是在一个单独的持久连接上供应全双工、双向通信。在Javascript创建了Web Socket之后,会有一个HTTP要求发送到浏览器以发起连接。在取得做事器相应后,建立的连接会将HTTP升级从HTTP协议交流为WebSocket协议。
由于WebSocket利用自定义的协议,以是URL模式也略有不同。未加密的连接不再是http://,而是ws://;加密的连接也不是https://,而是wss://。在利用WebSocket URL时,必须带着这个模式,由于将来还有可能支持其他的模式。
利用自定义协议而非HTTP协议的好处是,能够在客户端和做事器之间发送非常少量的数据,而不必担心HTTP那样字节级的开销。由于通报的数据包很小,以是WebSocket非常适宜移动运用。
接下来的篇幅会对Web Sockets的细节实现进行深入的探索,本文接下来的四个小节不会涉及到大量的代码片段,但是会对干系的API和技能事理进行剖析,相信大家读完下文之后再来看这段描述,会有一种豁然开朗的觉得。
即时通讯网有大量关于Web端即时通讯技能的文章,以下目录可供你系统地学习和理解。
Web即时通讯新手入门贴:
《新手入门贴:详解Web端即时通讯技能的事理》
Web端即时通讯技能盘点请拜会:
《Web端即时通讯技能盘点:短轮询、Comet、Websocket、SSE》
关于Ajax短轮询:
找这方面的资料没什么意义,除非忽悠客户,否则请考虑其它3种方案即可。
有关Comet技能的详细先容请拜会:
《Comet技能详解:基于HTTP长连接的Web端实时通信技能》
《WEB端即时通讯:HTTP长连接、长轮询(long polling)详解》
《WEB端即时通讯:不用WebSocket也一样能搞定的即时性》
《开源Comet做事器iComet:支持百万并发的Web端即时通讯方案》
更多WebSocket的详细先容请拜会:
《新手快速入门:WebSocket简明教程》
《WebSocket详解(一):初步认识WebSocket技能》
《WebSocket详解(二):技能事理、代码演示和运用案例》
《WebSocket详解(三):深入WebSocket通信协议细节》
《WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)》
《WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)》
《WebSocket详解(六):刨根问底WebSocket与Socket的关系》
《理论联系实际:从零理解WebSocket的通信事理、协议格式、安全性》
《Socket.IO先容:支持WebSocket、用于WEB真个即时通讯的框架》
《socket.io和websocket 之间是什么关系?有什么差异?》
有关SSE的详细先容文章请拜会:
《SSE技能详解:一种全新的HTML5做事器推送事宜技能》
4、开始动手,快速搭建高性能、可拓展的IM系统4.1 系统架构和代码文件目录构造下图是一个比较完备的IM系统架构:包含了C端(客户端)、接入层(通过协议接入)、S端(做事端)处理逻辑和分发、存储层用来持久化数据。
简要先容一下本次IM的技能实现情形:
1)我们本节C端利用的是Webapp, 通过Go措辞渲染Vue模版快速实现功能;
2)接入层利用的是websocket协议,前边已经进行了深入的先容;
3)S端是我们实现的重点,个中鉴权、登录、关系管理、单聊和群聊的功能都已经实现,读者可以在这部分功能的根本上再拓展其他的功能,比如:视频语音谈天、发红包、朋友圈等业务模块;
4)存储层我们做的比较大略,只是利用Mysql大略持久化存储了用户关系,然后谈天中的图片资源我们存储到了本地文件中。
虽然我们的IM系统实现的比较简化,但是读者可以在次根本上进行改进、完善、拓展,依然能够作出高可用的企业级产品。
我们的系统做事利用Go措辞构建,代码构造比较简洁,但是性能比较精良(这是Java和其他措辞所无法比拟的),单机支持几万人的在线谈天。
下边是代码文件的目录构造(完全源码下载见文末):
app
│ ├── args
│ │ ├── contact.go
│ │ └── pagearg.go
│ ├── controller //掌握器层,api入口
│ │ ├── chat.go
│ │ ├── contract.go
│ │ ├── upload.go
│ │ └── user.go
│ ├── main.go //程序入口
│ ├── model //数据定义与存储
│ │ ├── community.go
│ │ ├── contract.go
│ │ ├── init.go
│ │ └── user.go
│ ├── service //逻辑实现
│ │ ├── contract.go
│ │ └── user.go
│ ├── util //帮助函数
│ │ ├── md5.go
│ │ ├── parse.go
│ │ ├── resp.go
│ │ └── string.go
│ └── view //模版资源
│ │ ├── ...
asset //js、css文件
resource //上传资源,上传图片会放到这里
源码的详细解释如下:
1)从入口函数main.go开始,我们定义了controller层,是客户端api的入口;
2)service用来处理紧张的用户逻辑,分发、用户管理都在这里实现;
3)model层定义了一些数据表,紧张是用户注册和用户好友关系、群组等信息,存储到mysql;
4)util包下是一些帮助函数,比如加密、要求相应等;
5)view下边存储了模版资源信息,上边所说的这些都在app文件夹下存储;
6)外层还有asset用来存储css、js文件和谈天中会用到的表情图片等;
7)resource下存储用户谈天中的图片或者视频等文件。
总体来讲,我们的代码目录机构还是比较简洁清晰的。
理解了我们要搭建的IM系统架构,我们再来看一下架构重点实现的功能吧。
4.2 10行代码万能模版渲染Go措辞供应了强大的html渲染能力,非常大略的构建web运用,下边是实现模版渲染的代码,它太大略了,以至于可以直接在main.go函数中实现。
代码如下:
func registerView() {
tpl, err := template.ParseGlob("./app/view//")
if err != nil{
log.Fatal(err.Error())
}
for _, v := rangetpl.Templates() {
tplName := v.Name()
http.HandleFunc(tplName, func(writer http.ResponseWriter, request http.Request) {
tpl.ExecuteTemplate(writer, tplName, nil)
})
}
}
...
func main() {
......
http.Handle("/asset/", http.FileServer(http.Dir(".")))
http.Handle("/resource/", http.FileServer(http.Dir(".")))
registerView()
log.Fatal(http.ListenAndServe(":8081", nil))
}
Go实现静态资源做事器也很大略,只须要调用http.FileServer就可以了,这样html文件就可以很轻松的访问依赖的js、css和图标文件了。利用http/template包下的ParseGlob、ExecuteTemplate又可以很轻松的解析web页面,这些事情完备不依赖与nginx。
现在我们就完成了登录、注册、谈天C端界面的构建事情:
4.3 注册、登录和鉴权之前我们提到过,对付注册、登录和好友关系管理,我们须要有一张user表来存储用户信息。我们利用https://github.com/go-xorm/xorm来操作mysql。
首先看一下mysql表的设计。
app/model/user.go:
package model
import"time"
const(
SexWomen = "W"
SexMan = "M"
SexUnknown = "U"
)
type User struct{
Id int64`xorm:"pk autoincr bigint(64)" form:"id" json:"id"`
Mobile string`xorm:"varchar(20)" form:"mobile" json:"mobile"`
Passwd string`xorm:"varchar(40)" form:"passwd" json:"-"`// 用户密码 md5(passwd + salt)
Avatar string`xorm:"varchar(150)" form:"avatar" json:"avatar"`
Sex string`xorm:"varchar(2)" form:"sex" json:"sex"`
Nickname string`xorm:"varchar(20)" form:"nickname" json:"nickname"`
Salt string`xorm:"varchar(10)" form:"salt" json:"-"`
Online int`xorm:"int(10)" form:"online" json:"online"`//是否在线
Token string`xorm:"varchar(40)" form:"token" json:"token"`//用户鉴权
Memo string`xorm:"varchar(140)" form:"memo" json:"memo"`
Createat time.Time `xorm:"datetime" form:"createat" json:"createat"`//创建韶光, 统计用户增量时利用
}
我们user表中存储了用户名、密码、头像、用户性别、手机号等一些主要的信息,比较主要的是我们也存储了token标示用户在用户登录之后,http协议升级为websocket协议进行鉴权,这个细节点我们前边提到过,下边会有代码演示。
接下来我们看一下model初始化要做的一些事情吧。
app/model/init.go:
package model
import(
"errors"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm"
"log"
)
varDbEngine xorm.Engine
func init() {
driverName := "mysql"
dsnName := "root:root@(127.0.0.1:3306)/chat?charset=utf8"
err := errors.New("")
DbEngine, err = xorm.NewEngine(driverName, dsnName)
if err != nil&& err.Error() != ""{
log.Fatal(err)
}
DbEngine.ShowSQL(true)
//设置数据库连接数
DbEngine.SetMaxOpenConns(10)
//自动创建数据库
DbEngine.Sync(new(User), new(Community), new(Contact))
fmt.Println("init database ok!")
}
我们创建一个DbEngine全局mysql连接工具,设置了一个大小为10的连接池。model包里的init函数在程序加载的时候会先实行,对Go措辞熟习的同学该当知道这一点。我们还设置了一些额外的参数用于调试程序,比如:设置打印运行中的sql,自动的同步数据表等,这些功能在生产环境中可以关闭。我们的model初始化事情就做完了,非常简陋,在实际的项目中,像数据库的用户名、密码、连接数和其他的配置信息,建议设置到配置文件中,然后读取,而不像本文硬编码的程序中。
注册是一个普通的api程序,对付Go措辞来说,完成这件事情太大略了。
我们来看一下代码:
############################
//app/controller/user.go
############################
......
//用户注册
func UserRegister(writer http.ResponseWriter, request http.Request) {
var user model.User
util.Bind(request, &user)
user, err := UserService.UserRegister(user.Mobile, user.Passwd, user.Nickname, user.Avatar, user.Sex)
if err != nil{
util.RespFail(writer, err.Error())
} else{
util.RespOk(writer, user, "")
}
}
......
############################
//app/service/user.go
############################
......
type UserService struct{}
//用户注册
func (s UserService) UserRegister(mobile, plainPwd, nickname, avatar, sex string) (user model.User, err error) {
registerUser := model.User{}
_, err = model.DbEngine.Where("mobile=? ", mobile).Get(®isterUser)
if err != nil{
returnregisterUser, err
}
//如果用户已经注册,返回缺点信息
if registerUser.Id > 0 {
return registerUser, errors.New("该手机号已注册")
}
registerUser.Mobile = mobile
registerUser.Avatar = avatar
registerUser.Nickname = nickname
registerUser.Sex = sex
registerUser.Salt = fmt.Sprintf("%06d", rand.Int31n(10000))
registerUser.Passwd = util.MakePasswd(plainPwd, registerUser.Salt)
registerUser.Createat = time.Now()
//插入用户信息
_, err = model.DbEngine.InsertOne(®isterUser)
return registerUser, err
}
......
############################
//main.go
############################
......
func main() {
http.HandleFunc("/user/register", controller.UserRegister)
}
首先,我们利用util.Bind(request, &user)将用户参数绑定到user工具上,利用的是util包中的Bind函数,详细实现细节读者可以自行研究,紧张模拟了Gin框架的参数绑定,可以拿来即用,非常方便。然后我们根据用户手机号搜索数据库中是否已经存在,如果不存在就插入到数据库中,返回注册成功信息,逻辑非常大略。
登录逻辑更大略:
############################
//app/controller/user.go
############################
...
//用户登录
func UserLogin(writer http.ResponseWriter, request http.Request) {
request.ParseForm()
mobile := request.PostForm.Get("mobile")
plainpwd := request.PostForm.Get("passwd")
//校验参数
if len(mobile) == 0 || len(plainpwd) == 0 {
util.RespFail(writer, "用户名或密码禁绝确")
}
loginUser, err := UserService.Login(mobile, plainpwd)
if err != nil{
util.RespFail(writer, err.Error())
} else{
util.RespOk(writer, loginUser, "")
}
}
...
############################
//app/service/user.go
############################
...
func (s UserService) Login(mobile, plainpwd string) (user model.User, err error) {
//数据库操作
loginUser := model.User{}
model.DbEngine.Where("mobile = ?", mobile).Get(&loginUser)
if loginUser.Id == 0 {
return loginUser, errors.New("用户不存在")
}
//判断密码是否精确
if !util.ValidatePasswd(plainpwd, loginUser.Salt, loginUser.Passwd) {
return loginUser, errors.New("密码禁绝确")
}
//刷新用户登录的token值
token := util.GenRandomStr(32)
loginUser.Token = token
model.DbEngine.ID(loginUser.Id).Cols("token").Update(&loginUser)
//返回新用户信息
return loginUser, nil
}
...
############################
//main.go
############################
......
func main() {
http.HandleFunc("/user/login", controller.UserLogin)
}
实现了登录逻辑,接下来我们就到了用户首页,这里列出了用户列表,点击即可进入谈天页面。用户也可以点击下边的tab栏查看自己所在的群组,可以由此进入群组谈天页面。
详细这些事情还须要读者自己开拓用户列表、添加好友、创建群组、添加群组等功能,这些都是一些普通的api开拓事情,我们的代码程序中也实现了,读者可以拿去修正利用,这里就不再演示了。
我们再重点看一下用户鉴权这一块吧,用户鉴权是指用户点击谈天进入谈天界面时,客户端会发送一个GET要求给做事端,要求建立一条websocket长连接,做事端收到建立连接的要求之后,会对客户端要求进行校验,以确实是否建立长连接,然后将这条长连接的句柄添加到map当中(由于做事端不仅仅对一个客户端做事,可能存在万万千万个长连接)掩护起来。
我们下边来看详细代码实现:
############################
//app/controller/chat.go
############################
......
//本核心在于形成userid和Node的映射关系
type Node struct{
Conn websocket.Conn
//并行转串行,
DataQueue chan[]byte
GroupSets set.Interface
}
......
//userid和Node映射关系表
var clientMap map[int64]Node = make(map[int64]Node, 0)
//读写锁
var rwlocker sync.RWMutex
//实现谈天的功能
func Chat(writer http.ResponseWriter, request http.Request) {
query := request.URL.Query()
id := query.Get("id")
token := query.Get("token")
userId, _ := strconv.ParseInt(id, 10, 64)
//校验token是否合法
islegal := checkToken(userId, token)
conn, err := (&websocket.Upgrader{
CheckOrigin: func(r http.Request) bool{
returnislegal
},
}).Upgrade(writer, request, nil)
if err != nil{
log.Println(err.Error())
return
}
//得到websocket链接conn
node := &Node{
Conn: conn,
DataQueue: make(chan[]byte, 50),
GroupSets: set.New(set.ThreadSafe),
}
//获取用户全部群Id
comIds := concatService.SearchComunityIds(userId)
for _, v := rangecomIds {
node.GroupSets.Add(v)
}
rwlocker.Lock()
clientMap[userId] = node
rwlocker.Unlock()
//开启协程处理发送逻辑
go sendproc(node)
//开启协程完成吸收逻辑
go recvproc(node)
sendMsg(userId, []byte("welcome!"))
}
......
//校验token是否合法
func checkToken(userId int64, token string) bool{
user := UserService.Find(userId)
return user.Token == token
}
......
############################
//main.go
############################
......
func main() {
http.HandleFunc("/chat", controller.Chat)
}
......
进入谈天室,客户端发起/chat的GET要求,做事端首先创建了一个Node构造体,用来存储和客户端建立起来的websocket长连接句柄,每一个句柄都有一个管道DataQueue,用来收发信息,GroupSets是客户端对应的群组信息,后边我们会提到。
typeNode struct{
Conn websocket.Conn
//并行转串行,
DataQueue chan[]byte
GroupSets set.Interface
}
做事端创建了一个map,将客户端用户id和其Node关联起来:
//userid和Node映射关系表
var clientMap map[int64]Node = make(map[int64]Node, 0)
接下来是紧张的用户逻辑了,做事端吸收到客户真个参数之后,首先校验token是否合法,由此确定是否要升级http协议到websocket协议,建立长连接,这一步称为鉴权。
代码如下:
//校验token是否合法
islegal := checkToken(userId, token)
conn, err := (&websocket.Upgrader{
CheckOrigin: func(r http.Request) bool{
return islegal
},
}).Upgrade(writer, request, nil)
鉴权成功往后,做事端初始化一个Node,搜索该客户端用户所在的群组id,添补到群组的GroupSets属性中。然后将Node节点添加到ClientMap中掩护起来,我们对ClientMap的操作一定要加锁,由于Go措辞在并发情形下,对map的操作并不担保原子安全。
代码如下:
//得到websocket链接conn
node := &Node{
Conn: conn,
DataQueue: make(chan[]byte, 50),
GroupSets: set.New(set.ThreadSafe),
}
//获取用户全部群Id
comIds := concatService.SearchComunityIds(userId)
for _, v := rangecomIds {
node.GroupSets.Add(v)
}
rwlocker.Lock()
clientMap[userId] = node
rwlocker.Unlock()
做事端和客户端建立了长链接之后,会开启两个协程专门来处理客户真个收发事情,对付Go措辞来说,掩护协程的代价是很低的,以是说我们的单机程序可以很轻松的支持成千上完的用户谈天,这还是在没有优化的情形下。
代码如下:
......
//开启协程处理发送逻辑
go sendproc(node)
//开启协程完成吸收逻辑
go recvproc(node)
sendMsg(userId, []byte("welcome!"))
......
至此,我们的鉴权事情也已经完成了,客户端和做事真个连接已经建立好了,接下来我们就来实现详细的谈天功能吧。
4.4 实现单聊和群聊实现谈天的过程中,体的设计至关主要,体设计的合理,功能拓展起来就非常的方便,后期掩护、优化起来也比较大略。
我们先来看一下,我们体的设计:
############################
//app/controller/chat.go
############################
type Message struct{
Id int64`json:"id,omitempty" form:"id"`//ID
Userid int64`json:"userid,omitempty" form:"userid"`//谁发的
Cmd int`json:"cmd,omitempty" form:"cmd"`//群聊还是私聊
Dstid int64`json:"dstid,omitempty" form:"dstid"`//对端用户ID/群ID
Media int`json:"media,omitempty" form:"media"`//按照什么样式展示
Content string`json:"content,omitempty" form:"content"`//的内容
Pic string`json:"pic,omitempty" form:"pic"`//预览图片
Url string`json:"url,omitempty" form:"url"`//做事的URL
Memo string`json:"memo,omitempty" form:"memo"`//大略描述
Amount int`json:"amount,omitempty" form:"amount"`//其他和数字干系的
}
每一条都有一个唯一的id,将来我们可以对持久化存储,但是我们系统中并没有做这件事情,读者可根据须要自行完成。然后是userid,发起的用户,对应的是dstid,要将发送给谁。
还有一个参数非常主要,便是cmd,它表示是群聊还是私聊,群聊和私聊的代码处理逻辑有所差异。
我们为此专门定义了一些cmd常量:
//定义命令行格式
const(
CmdSingleMsg = 10
CmdRoomMsg = 11
CmdHeart = 0
)
media是媒体类型,我们都知道微信支持语音、视频和各种其他的文件传输,我们设置了该参数之后,读者也可以自行拓展这些功能;
content是文本,是谈天中最常用的一种形式;
pic和url是为图片和其他链接资源所设置的;
memo是简介;
amount是和数字干系的信息,比如说发红包业务有可能利用到该字段。
体的设计便是这样,基于此体,我们来看一下,做事端如何收发,实现单聊和群聊吧。还是从上一节提及,我们为每一个客户端长链接开启了两个协程,用于收发,谈天的逻辑就在这两个协程当中实现。
代码如下:
############################
//app/controller/chat.go
############################
......
//发送逻辑
func sendproc(node Node) {
for{
select{
case data := <-node.DataQueue:
err := node.Conn.WriteMessage(websocket.TextMessage, data)
if err != nil{
log.Println(err.Error())
return
}
}
}
}
//吸收逻辑
func recvproc(node Node) {
for{
_, data, err := node.Conn.ReadMessage()
if err != nil{
log.Println(err.Error())
return
}
dispatch(data)
//todo对data进一步处理
fmt.Printf("recv<=%s", data)
}
}
......
//后端调度逻辑处理
func dispatch(data []byte) {
msg := Message{}
err := json.Unmarshal(data, &msg)
if err != nil{
log.Println(err.Error())
return
}
switch msg.Cmd {
case CmdSingleMsg:
sendMsg(msg.Dstid, data)
case CmdRoomMsg:
for _, v := rangeclientMap {
if v.GroupSets.Has(msg.Dstid) {
v.DataQueue <- data
}
}
case CmdHeart:
//检测客户真个心跳
}
}
//发送,发送到的管道
func sendMsg(userId int64, msg []byte) {
rwlocker.RLock()
node, ok := clientMap[userId]
rwlocker.RUnlock()
if ok {
node.DataQueue <- msg
}
}
......
做事端向客户端发送逻辑比较大略,便是将客户端发送过来的,直接添加到目标用户Node的channel中去就好了。
通过websocket的WriteMessage就可以实现此功能:
func sendproc(node Node) {
for{
select{
case data := <-node.DataQueue:
err := node.Conn.WriteMessage(websocket.TextMessage, data)
if err != nil{
log.Println(err.Error())
return
}
}
}
}
收发逻辑是这样的,做事端通过websocket的ReadMessage方法吸收到用户信息,然后通过dispatch方法进行调度:
func recvproc(node Node) {
for{
_, data, err := node.Conn.ReadMessage()
if err != nil{
log.Println(err.Error())
return
}
dispatch(data)
//todo对data进一步处理
fmt.Printf("recv<=%s", data)
}
}
dispatch方法所做的事情有两件:
1)解析体到Message中;
2)根据类型,将体添加到不同用户或者用户组的channel当中。
Go措辞中的channel是协程间通信的强大工具, dispatch只要将添加到channel当中,发送协程就会获取到信息发送给客户端,这样就实现了谈天功能。
单聊和群聊的差异只是做事端将发送给群组还是个人,如果发送给群组,程序会遍历全体clientMap, 看看哪个用户在这个群组当中,然后将发送。
实在更好的实践是我们再掩护一个群组和用户关系的Map,这样在发送群组的时候,取得用户信息就比遍历全体clientMap代价要小很多了。
func dispatch(data []byte) {
msg := Message{}
err := json.Unmarshal(data, &msg)
if err != nil{
log.Println(err.Error())
return
}
switch msg.Cmd {
case CmdSingleMsg:
sendMsg(msg.Dstid, data)
case CmdRoomMsg:
for _, v := rangeclientMap {
if v.GroupSets.Has(msg.Dstid) {
v.DataQueue <- data
}
}
case CmdHeart:
//检测客户真个心跳
}
}
......
func sendMsg(userId int64, msg []byte) {
rwlocker.RLock()
node, ok := clientMap[userId]
rwlocker.RUnlock()
if ok {
node.DataQueue <- msg
}
}
可以看到,通过channel,我们实现用户谈天功能还是非常方便的,代码可读性很强,构建的程序也很健壮。
下边是笔者本地谈天的示意图:
4.5 发送表情和图片
下边我们再来看一下谈天中常常利用到的发送表情和图片功能是如何实现的吧。
实在表情也是小图片,只是和谈天中图片不同的是,表情图片比较小,可以缓存在客户端,或者直接存放到客户端代码的代码文件中(不过现在微信谈天中有的表情包都是通过网络传输的)。
下边是一个谈天中返回的图标文本数据:
{
"dstid":1,
"cmd":10,
"userid":2,
"media":4,
"url":"/asset/plugins/doutu//emoj/2.gif"
}
客户端拿到url后,就加载本地的小图标。
谈天中用户发送图片也是一样的事理,不过谈天中用户的图片须要先上传到做事器,然后做事端返回url,客户端再进行加载,我们的IM系统也支持此功能。
我们看一下图片上传的程序:
############################
//app/controller/upload.go
############################
func init() {
os.MkdirAll("./resource", os.ModePerm)
}
func FileUpload(writer http.ResponseWriter, request http.Request) {
UploadLocal(writer, request)
}
//将文件存储在本地/im_resource目录下
func UploadLocal(writer http.ResponseWriter, request http.Request) {
//得到上传源文件
srcFile, head, err := request.FormFile("file")
if err != nil{
util.RespFail(writer, err.Error())
}
//创建一个新的文件
suffix := ".png"
srcFilename := head.Filename
splitMsg := strings.Split(srcFilename, ".")
if len(splitMsg) > 1 {
suffix = "."+ splitMsg[len(splitMsg)-1]
}
filetype := request.FormValue("filetype")
if len(filetype) > 0 {
suffix = filetype
}
filename := fmt.Sprintf("%d%s%s", time.Now().Unix(), util.GenRandomStr(32), suffix)
//创建文件
filepath := "./resource/"+ filename
dstfile, err := os.Create(filepath)
if err != nil{
util.RespFail(writer, err.Error())
return
}
//将源文件拷贝到新文件
_, err = io.Copy(dstfile, srcFile)
if err != nil{
util.RespFail(writer, err.Error())
return
}
util.RespOk(writer, filepath, "")
}
......
############################
//main.go
############################
func main() {
http.HandleFunc("/attach/upload", controller.FileUpload)
}
我们将文件存放到本地的一个磁盘文件夹下,然后发送给客户端路径,客户端通过路径加载干系的图片信息。
关于发送图片,我们虽然实现功能,但是做的太大略了,我们在接下来的章节详细的和大家磋商一下系统优化干系的方案。若何让我们的系统在生产环境中用的更好。
5、程序优化和系统架构升级方案我们上边实现了一个功能健全的IM系统,要将该系统运用在企业的生产环境中,须要对代码和系统架构做优化,才能实现真正的高可用。
本节紧张从代码优化和架构升级上谈一些个人不雅观点,能力有限不可能面面俱到,希望读者也在回答中给出更多好的建议。
5.1 代码优化关于框架:我们的代码没有利用框架,函数和api都写的比较简陋,虽然进行了大略的构造化,但是很多逻辑并没有解耦,以是建议大家业界比较成熟的框架对代码进行重构,Gin便是一个不错的选择。
关于Map:系统程序中利用clientMap来存储客户端长链接信息,Go措辞中对付大Map的读写要加锁,有一定的性能限定,在用户量特殊大的情形下,读者可以对clientMap做拆分,根据用户id做hash或者采取其他的策略,也可以将这些长链接句柄存放到redis中。
关于图片上传:上边提到图片上传的过程,有很多可以优化的地方,首先是图片压缩(微信也是这样做的)。图片资源的压缩不仅可以加快传输速率,还可以减少做事端存储的空间。其余对付图片资源来说,实际上做事端只须要存储一份数据就够了,读者可以在图片上传的时候做hash校验,如果资源文件已经存在了,就不须要再次上传了,而是直接将url返回给客户端(各大网盘厂商的秒传功能便是这样实现的)。
代码还有很多优化的地方,比如:
1)我们可以将鉴权做的更好,利用wss://代替ws://;
2)在一些安全领域,可以对体进行加密,在高并发领域,可以对体进行压缩;
3)对Mysql连接池再做优化,将持久化存储到mongo,避免对数据库频繁的写入,将单条写入改为多条一块写入;
4)为了使程序耗费更少的CPU,降落对体进行Json编码的次数,一次编码,多次利用......
5.2 系统架构升级我们的系统太过于大略,所在在架构升级上,有太多的事情可以做,笔者在这里只提几点比较主要的。
1)运用/资源做事分离:
我们所说的资源指的是图片、视频等文件,可以选择成熟厂商的Cos,或者自己搭建文件做事器也是可以的,如果资源量比较大,用户比较广,cdn是不错的选择。
2)打破系统连接数,搭建分布式环境:
对付做事器的选择,一样平常会选择linux,linux下统统皆文件,长链接也是一样。单机的系统连接数是有限定的,一样平常来说能达到10万就很不错了,以是在用户量增长到一定程序,须要搭建分布式。分布式的搭建就要优化程序,由于长链接句柄分散到不同的机器,实现广播和分发是首先要办理的问题,笔者这里不深入阐述了,一来是没有足够的履历,二来是办理方案有太多的细节须要磋商。搭建分布式环境所面临的问题还有:若何更好的弹性扩容、应对突发事宜等。
3)业务功能分离:
我们上边将用户注册、添加好友等功能和谈天功能放到了一起,真实的业务场景中可以将它们做分离,将用户注册、添加好友、创建群组放到一台做事器上,将谈天功能放到其余的做事器上。业务的分离不仅使功能逻辑更加清晰,还能更有效的利用做事器资源。
4)减少数据库I/O,合理利用缓存:
我们的系统没有将持久化,用户信息持久化到mysql中去。在业务当中,如果要对做持久化储存,就要考虑数据库I/O的优化,大略讲:合并数据库的写次数、优化数据库的读操作、合理的利用缓存。
上边是便是笔者想到的一些代码优化和架构升级的方案。
6、本文结语不知道大家有没有创造,利用Go搭建一个IM系统比利用其他措辞要大略很多,而且具备更好的拓展性和性能(并没有吹嘘Go的意思)。
在当今这个时期,5G将要遍及,流量不再昂贵,IM系统已经广泛渗入到了用户日常生活中。对付程序员来说,搭建一个IM系统不再是困难的事情。
如果读者根据本文的思路,理解Websocket,Copy代码,运行程序,该当用不了半天的韶光就能上手这样一个IM系统。
IM系统是一个时期,从QQ、微信到现在的人工智能,都广泛运用了即时通信,环绕即时通信,又可以做更多产品布局。
笔者写本文的目的便是想要帮助更多人理解IM,帮助一些开拓者快速的搭建一个运用,燃起大家学习网络编程知识的兴趣,希望的读者能有所收成,能将IM系统运用到更多的产品布局中。
7、完全源码下载请自行从github下载:
主地址:https://github.com/GuoZhaoran/fastIM
备地址:https://github.com/52im/fastIM
附录:更多IM开拓文章[1] IM代码实践(适宜新手):
《自已开拓IM有那么难吗?手把手教你自撸一个Andriod版大略单纯IM (有源码)》
《一种Android端IM智能心跳算法的设计与实现磋商(含样例代码)》
《手把手教你用Netty实现网络通信程序的心跳机制、断线重连机制》
《详解Netty的安全性:事理先容、代码演示(上篇)》
《详解Netty的安全性:事理先容、代码演示(下篇)》
《微信本地数据库破解版(含iOS、Android),仅供学习研究 [附件下载]》
《Java NIO根本视频教程、MINA视频教程、Netty快速入门视频 [有源码]》
《轻量级即时通讯框架MobileIMSDK的iOS源码(开源版)[附件下载]》
《开源IM工程“蘑菇街TeamTalk”2015年5月前未删减版完全代码 [附件下载]》
《微信本地数据库破解版(含iOS、Android),仅供学习研究 [附件下载]》
《NIO框架入门(一):做事端基于Netty4的UDP双向通信Demo演示 [附件下载]》
《NIO框架入门(二):做事端基于MINA2的UDP双向通信Demo演示 [附件下载]》
《NIO框架入门(三):iOS与MINA2、Netty4的跨平台UDP双向通信实战 [附件下载]》
《NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战 [附件下载]》
《用于IM中图片压缩的Android工具类源码,效果可媲美微信 [附件下载]》
《高仿Android版手机QQ可拖拽未读数小气泡源码 [附件下载]》
《一个WebSocket实时谈天室Demo:基于node.js+socket.io [附件下载]》
《Android谈天界面源码:实现了聊景象泡、表情图标(可翻页) [附件下载]》
《高仿Android版手机QQ首页侧滑菜单源码 [附件下载]》
《开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石 [源码下载]》
《分享java AMR音频文件合并源码,全网最全》
《微信团队原创Android资源稠浊工具:AndResGuard [有源码]》
《一个基于MQTT通信协议的完全Android推送Demo [附件下载]》
《Android版高仿微信谈天界面源码 [附件下载]》
《高仿手机QQ的Android版锁屏谈天提醒功能 [附件下载]》
《高仿iOS版手机QQ录音及振幅动画完全实现 [源码下载]》
《Android端社走运用中的评论和回答功能实战分享[图文+源码]》
《Android端IM运用中的@人功能实现:仿微博、QQ、微信,零入侵、高可扩展[图文+源码]》
《仿微信的IM谈天韶光显示格式(含iOS/Android/Web实现)[图文+源码]》
《Android版仿微信朋友圈图片拖拽返回效果 [源码下载]》
《适宜新手:从零开拓一个IM做事端(基于Netty,有完全源码)》
《拿起键盘便是干:跟我一起徒手开拓一套分布式IM系统》
《精确理解IM长连接的心跳及重连机制,并动手实现(有完全IM源码)》
《适宜新手:手把手教你用Go快速搭建高性能、可扩展的IM系统(有源码)》
>> 更多同类文章 ……
欢迎关注我的“即时通讯技能圈”"大众号:
(本文同步发布自:http://www.52im.net/thread-2988-1-1.html)