俗话说的好,架构历来都不是一蹴而就的,没有什么架构一开始设计就是最终版本,其中须要通过不少步骤的变化,今天咱们就从一个最简单的例子来看看,究竟架构这个东西是怎么变的。
我将从一个最简单的聊天室的功能来实现,看看这样一个提及来好像很简单但的功能,咱们须要考虑哪些问题。html
我使用golang实现,从0开始实现,须要借助的是websocket来实现即时,基础知识本身补一下,这里不作过多赘述。git
即时聊天室包含功能(这里写出的功能假设就是产品经理告诉咱们的):
一、所用用户能链接聊天室
二、链接成功的用户能向聊天室发送消息
三、全部成功链接的用户能收到聊天室的消息github
为了简化,咱们暂定只有一个房间,由于即便要求须要多个房间和一个房间差很少;而后咱们简化消息存储,咱们默认也不持久化消息,由于消息的持久化就会涉及各类数据库操做还有分页查询,这里暂时不作考虑。golang
那么你必定奇怪了,这些都没了,那整个实现还有啥难度?你大能够本身先想想若是是你,你会怎么样去实现。web
下文中我会用C表明客户端,S表明服务端
(本文为了展现架构的演变,若是你能想到更好的架构或者一开始就直接想到最终版本,那么证实你已经有不少的经验积累了,给大佬递茶)数据库
各个版本和测试客户端全部的代码都已经上传github,若是有须要请查看,https://github.com/LinkinStars/simple-chatroom编程
第一个版本确定是最简单的版本,咱们就笔直朝着目标走。
咱们知道websocket能实现最基本的通讯。
客户端发送消息,服务端接收消息,C -> S
服务端发送消息,客户端接收消息,S -> C安全
那么聊天室就是:不少C发消息给S
S将全部收到的消息发给每个C
那么咱们的第一个架构就很容易想到是这样子的:服务器
咱们在服务端维护一个链接池,链接池中保存了链接的用户,每当服务端收到一个消息以后,就遍历一遍链接池,将这个消息发送给全部链接池中的人。流程图以下:
微信
那么下面,咱们用代码来实现一下
首先定义Room里面有一个链接池

而后咱们写一个处理websocket的方法

最后写一个群发消息,遍历链接池,发送消息

补全其余部分,就完成了,这就是咱们第一个版本,而后咱们用一个测试的html测试一下

嗯,完成啦~我真棒,真简单
固然不可能那么简单!!!还有不少问题!
针对于第一个版本,那么存在的问题还有
一、咱们发现,当用户断开链接的时候,链接池里面这个链接没有被移除,因此消息发送的时候会报错,并且链接池会一直变大。
二、用户不少,遍历发送消息是一个耗时的操做,不该该被阻塞
针对这两个问题改动以下:
一、当发送消息失败,证实链接已经断开,因此从链接池中移除链接
二、群发消息改成gorutinue
因此V1.1修改以下


到此为止,第一个版本就到这里了,由于聪明的你应该已经发现这样设计的架构存在一个巨大的问题...
若是你有必定的并发编程的经验就会发现,上面版本有一个很危险的并发操做,那就是链接池。
咱们假设一种状况,当一个协程正在遍历链接池发送消息的时候,另一个协程把其中一些链接删除了,还有一个协程把新的链接加进去了,这样的操做就是传说中的并发问题。
并且对于websocket来讲还有一个问题,就是若是并发去对同一个链接发送消息的话就会出现panic: concurrent write to websocket connection这样的异常,由于是panic因此问题就很是大了。
并发问题怎么解决?不少人会说,简单,加锁就完事了



加完了,搞定,这下没问题了吧。这就是版本2。由于加入了锁机制,因此并发安全保证了,可是
新的问题又出现了,咱们若是咱们在发送消息的方法中加入延时,模拟出发送消息网络不正常的状况
time.Sleep(time.Second * 2)
那么你就会发现,当新的用户加入的时候,由于当前还有消息正在发送,因此致使新加入的用户没有办法获取到锁,也就没法发送消息
那怎么办呢?
而后顺便说一下,由于锁的是room在必定并发的程度上仍是有可能出现异常

我在开发golang的时候有这样一个信念,有锁的地方必定能用channel优化,从而面向并发编程,虽然并不是绝对,可是golang提供的channel不少状况下都能将锁给替换掉,从而换取出性能的提高,具体怎么作呢?
首先咱们想一下有哪些地方能够利用channel进行解耦
一、第一次链接,咱们将链接扔进一个信道中去
二、断开链接,咱们将要删除的链接扔进一个信道中去
三、发送消息,咱们每一个链接对象都有一个信道,只须要将消息写入这个信道就能发送消息
因此咱们从新调整一下架构,图以下:

而后咱们看看代码上面如何实现:
首先定义一个客户端

包含一个链接和一个发送消息的专用信道
而后定义客户端的两个方法

当从websocket中获取到信息的时候,将消息丢到chatRoom的总发送信道中去,由chatRoom去群发。
当本身的send信道中有消息时,将消息经过websocket发送给客户端。
同时当发送或者接收消息出现异常,将本身发送给取消注册的信道,由chatRoom去移除注册信息。
而后定义聊天室

register用于处理注册
unregister用于处理移除注册
clientsPool这里更换为map,方便移除
send是总发送消息信道,用于群发消息
而后定义处理websocket方法

当前第一次来的时候就建立客户端,而后启动客户端的读取和发送方法,而且将本身发给注册信道
最后最重要的就是如何去调度处理chatRoom中全部的管道,咱们使用select

当有注册的时候就注册,当有离开的时候就删除,当须要发送消息的时候,消息会发送给每个client各自的send信道由它们本身发送。
这样就成功实现了使用channel代替了原来的锁

当前群发消息和客户的加入退出就基本不受到影响了,随时能够加入和退出,一旦加入就会收到消息。
一切看似很完美吧,其实还有些bug,咱们建立一些客户端进行压测试试看。
编写压测代码以下,由于压测就是建立不少客户端发送消息,这里就很少作赘述了


而后会发现,测试的过程当中,若是你启动一个网页版本的客户端发现,你的消息发不出去了。这是为何呢?
原来咱们以前在处理全部管道中任务的时候当处理发送消息的时候有问题,虽然send是一个有缓冲的通道,可是当缓冲满的时候,那么就会阻塞,没法向里面再发送消息,须要等待send里面的消息被消费,可是若是send里面的消息要被消费,前提就是要轮到这个消息被发送,因而形成了循环等待,必定意义上的死锁。(有点绕,你须要理一理)

因此咱们须要修改一下代码,修复这个bug,当消息没法写入send信道的时候,那就直接将这个消息抛弃(虽然这样处理好像不太科学),由于要不就是这个用户已经断开链接,要不就是这个用户的缓冲信道已经占满了。以下:

其实在作的过程当中就发现了一些问题,一个问题同一个用户若是不停的发送消息,那么一方面是会对服务器形成压力,另外一方面对于别的用户来讲这是一种骚扰,因此咱们须要限制用户发送消息的频率。这里为了测试方便,针对于同一个用户1秒内只能发送一条消息,这样从必定程度上也减小了并发问题的出现。
改动很是简单,以下:

咱们启动多个客户端定时的发送一些消息进行测试,5个客户端下每1ms发送一条消息,本机测试下来没有问题。(固然这个版本)
那么到如今咱们已经实际了聊天室的基本功能,对于一个最简单的聊天来讲已经足够了,可是由于咱们简化了不少细节,因此存在不少优化的地方,下面列举几个地方能够作后续的优化和升级。
一、消息持久化,当前消息发送以后若是当时用户不在线就没法收到,这样对于用户来讲实际上是很难受的,因此消息须要进行持久化,而持久化就会有不少方案,保存消息的方式,以及保存消息的时间,不能由于保存消息而影响即时性。以及用户再次登陆以后须要将以后保存的消息返回给用户。
二、消息id,咱们如今发送消息的时候是不带消息id的,可是其实做为消息自己,消息的发送须要保证幂等性,相同的消息(消息id相同)不该该发送屡次,因此消息id的生成,如何保证消息不重复也是须要考虑的。
三、消息不丢失,消息持久化,网络异常都有可能致使消息丢失,如何保证消息不丢失呢?
四、密集型消息分发,当用户人数不少,当前会建立不少的协程去分发消息,人一多确定就不行了,并且人一多,一台机器确定不够,那么分布式维护链接池等等架构的调整就须要进行了。
五、心跳保活,链接一段时间以后,因为网络的缘由或者别的缘由,可能会致使链接中断的状况出现,因此通过一段时间就须要发送一些消息保持链接。相似PING\PONG
六、鉴权,这个简单,当前任何用户连上就能发送消息,理论上来讲,其实须要通过鉴权以后才能发送消息。
七、消息加密,如今消息都是明文传输的,这样传递消息实际上是不安全的,因此加密传输消息也是后期能够考虑的,同时消息的压缩也是。
这些后续的扩展就要你来思考一下了,如何去实现。设计的时候你也能够参考不少现实中已经存在的一些例子来帮助你思考。在咱们实现的时候也没有借助任何的中间件,因此你能够后期考虑使用一些中间件来完成分布式等要求,如mq等。
是否是看到这里发现只是简单的一个即时聊天后面的架构扩展都是很是可怕的,若是真的要作到像微信或者qq那样随意的单聊和群聊,而且解决各类并发问题还有不少路要走。
若是你有一些本身的想法,也欢迎在下面留言讨论。
这里其实想说明的并非如何去设计一个IM,想要真正说明的是一个架构师如何进行演变的,其中须要考虑到哪些问题,这些问题又是如何被解决的。其中须要经历不断的测试,调整,测试,调整。还想说明的是,架构没有好和坏,只有适合与否,对于一个小的项目来讲就没有必要用大架构,合适的才是最好的。
最后,也确定有人想了解一些大型的聊天im的架构,这里有几篇博客我认为写的很不错,能够参考一下。
下面这两篇是对一些大型架构的说明
https://alexstocks.github.io/html/pubsub.html
https://alexstocks.github.io/html/im.html
下面是一些github上的项目
https://github.com/alberliu/goim
这个项目比较简单,容易理解,文档介绍详细解释了不少概念,具体使用nsq来实现消息的转发
https://github.com/Terry-Mao/goim
这个项目相对复杂,运用到的东西就比较多,须要必定的理解,同时扩展性就相对不错
做者:LinkinStar