分享

三周,用长轮询实现Chat并迁移到Azure测试

 quasiceo 2014-02-03

三周,用长轮询实现Chat并迁移到Azure测试

公司的OA从零开始进行开发,继简单的单点登陆、角色与权限、消息中间件之后,轮到在线即时通信的模块需要我独立去完成。这三周除了逛网店见爱*看动漫接兼职,基本上都花在这上面了。简单地说就是用MVC4基于长轮询实现(伪)即时通信,利用BootMetro搭建即时聊天系统,同时跨域组件化之后今晚移植到了Azure上方便周末进行第一次迭代的公网测试,地址在http://indreamchat./。有兴趣的朋友可以上去送测试数据,剥离了认证登陆,简单地伪装了一个...一个...怎么说,反正能用就好了...

一大早要坐客车回家,所以现在睡也不是不睡也不是,就分享一下实现方式。由于已经回到租房了,所以代码不在手上,写得如何,有待指正。

最后,它长这个样子:

 

首先介绍下用户情况。

系统的用户除了1/4的内网用户外,基本上都是全国各地办事处的外网用户,而且出差尤多,特别是海外,这是网络状况。

也就是我们的用户遍布世界各地,有不同的网络状况,而且世界上大部分可以想得到的设备都可能会是接入端,所以一开始就有较高兼容需求。

 

回到即时通信,其实只是其难点而已,总体来说是一个Chat和消息推送模块,允许各个子系统按照需求用群组会话组织管理用户,并推送系统消息,同时允许用户间的通信交流,并且满足移植性,使得不同子系统能直接引用。

下文先交代架构,然后最后交流下关键技术吧。

那么直接用文字说下架构吧,从上到下是从底层(数据库)到顶层(UI):

  • SQL Server 2012
  • Data Access / Entity Framework 5——数据链路以及数据缓存,利用EF实现
  • ChatManager + ListenQueue——会话的管理对象,监听列表提供监听服务和持有监听对象,在新消息和相关监听存在的时候通过callback推送消息
  • CometManager——长轮询的管理对象,负责向MVC提供轮询服务,同时向上层监听消息,可以视为一个服务的Adapter
  • Service / MVC 4——提供UI和JSONP API的(工程意义上的)UI/Service层
  • ChatDataManager.js——与Service进行数据通信并维护本地数据对象的管理对象
  • chatUiEngine.js——利用ChatDataManager.js向UI提供动态UI服务的引擎
  • UI——提供了配置和界面容器后,只执行了一个chatUiEngine.Start()方法启动的页面

以下逐层详细说明:

SQL Server 2012

Data Access / Entity Framework 5

即时通信有一个很特别的地方,就是对集中的数据进行频繁读写。你可以站在数据的角度来看,基本上所有用户都在访问并且添加最近最新的那群数据,所以作为数据链路层的数据对象,只需要将尽量新的数据缓存起来即可。另外可以保证的是先写后读(发了消息其他人才可以看见),写完马上要(发完了消息其他人马上要读取),基本上就是个对写方法加了信号量(同步锁)的数据栈。

那么,是用带有缓存的ORM就十分合适,比如Entity Framework。

从业务方面考虑,这是个频繁修改需求的项目,有Model First的Entity Framework是个不错的选择。

至于为什么用那么新,其实只是用Nuget更新的,不过还是很喜欢它的Convert To Enum功能。只是用EF的话要十分小心数据库的结构和Model并不完全同步,比如1 to 1/0在生成数据库再另外生成Data Model的时候会变成1 to *,因为只有一个外键约束。

ChatManager + ListenQueue

这是会话管理与消息管理的核心,会话管理其实也就是增删查改的问题,主要功能实现在于消息部分,也就是ListenQueue。

ListenQueue是一个监听队列,可以添加监听,由ChatManager作为其Fascade,对外提供监听和停止监听服务。

业务实现的方法就是,向ChatManager提出监听某用户/会话的最新信息,在ChatManager有最新信息的时候通过Callback将有最新消息的消息返回给监听者(怎么说着那么别扭呢),由监听者决定是获取新消息还是执行什么业务。

所以,这一层实现了消息的发送获取管理、会话的管理、监听列表的管理,而它们各自有业务相关。

在这里,得感慨一下delegate闭包的强大和便利。

CometManager

一个监听实现者和一个长轮询服务者,通过长轮询实现监听到最新消息后即时推送回客户端。长轮询是怎么回事呢?应该可以搜到不少资源,放在后面讲吧。

在这里不是用ChatManager直接提供轮询服务是因为需要扩充性,将来必然有其他形式的客户端和其连接方式需要获取最新消息,比如Web Socket、WCF、Hessian。到时候这些方式的接收者只要实现符合delegate约束的监听方法,即可将消息以自己的通信方式发送回自己所服务的客户端了。

Service / MVC 4

这就是为浏览器提供最终页面和数据的项目层面上的UI层。选用MVC 4是因为其可以同时提供轻量级的跨域Web API的JSONP服务,如何实现JSONP后面简述吧。

这一层主要的任务就是界面和数据服务,并没有什么特别的。当然,依赖注入由这里启动,我用的是StructureMap2.6.4。

这里使用JSONP服务的一个目的就是为了能让其他系统跨域调用Chat。

ChatDataManager.js

这个在站点可以直接看到,没有做编码,所以可以从页面源代码处看到源代码。

这是利用jQuery.Ajax与上一层进行数据交流以及本地数据管理的管理对象。它主要的功能就是获取数据、将数据格式化并持久化、同步更新数据,在数据更新时用回调通知监听对象,让其对数据更新作出反应。

用大写字母开头而不用JS常用的命名法就是不希望一般用户直接使用。

chatUiEngine.js

利用ChatDataManager所持久化的数据和数据更变让界面持续工作的“引擎”,从一个Start(settings)方法开始启动。

它启动后的第一步就是启动ChatDataManager.js,然后用获取到的数据构建Chat的整个页面界面,然后一直维持界面运转。比如在有新内容的时候刷新或者更改界面,用户操作时控制界面作出反应,用户发送消息时将消息通过ChatDataManager.js推送回服务器,等等。

将所有UI操作的方法封装成API的目的就是让其他系统可以通过调用两个JS而在自己系统打开Chat,并且使用;而将数据与UI的持久化控制分成两层,是为了让客户端在有需要的时候获取部分数据,而不需交互。

UI

UI所作的就是提供容器(显示Chat以及相关内容的地方)和配置(告诉chatUiEngine.js有什么具体UI需求)。

这里尝试展示下打开chatUiEngine.js的方法(不大懂插入代码...):

启动chatUiEngine.js

这些是在Html中提供给chatUiEngine.js的容器,chatUiEngine.js利用它们生成合适的界面元素,将数据渲染上去后展示到容器中,而容器在上面的配置中进行描述。

Html模板

剩下的就是UI中大致的容器了,用一个简单的table搭建出来,然后chatUiEngine就会将界面元素动态导入。

贴了也没什么意义的代码

UI部分已经尽量简化了,目的就是希望对原有的系统可以实现无痛人流植入,尽量少造成更改,同时可以让它们实现自己特殊的界面需求。

当然,至此只是我打了半个星期酱油,敲了两个星期多一点代码的第一次迭代的发布,所以必定很不完善。另外代码只在上周末重构过一次,这周测试和需求频繁发生也造成了新的代码乱搞基冗余,近期需要再次重构。

 

 

 

 

下面就分享下一些技术理解吧:

关于长轮询Long-Polling

详细的许多内容应该挺容易搜索到得,我也是从找到@dudu的谋篇博文开始知道MVC是具体怎么实现的,就用我的方式和实现方法笼统地分享下吧。

首先是原理。

原理很简单,HTTP是个异步转同步的协议,客户端发送了一个请求后,保持了与服务端的一条TCP连接,然后服务端通过这条连接将网页以及相关内容发送回客户端。而原来的Web只允许这一种通信方式,也是出于安全方面考虑(现在有Web Socket了)。

那服务端有消息要马上推送给客户端要怎么办呢?所以有了长轮询。

服务端将那条连接Hold住,直到有消息了再将数据通过那条连接返回给用户,然后用户再继续请求新的连接,然后服务端继续Hold住......

体现在我的开发过程里面就是,一开始我想用自旋锁锁住那条连接的线程的(没那么神秘就是一直While(true) {sleep();}而已),后来发现了MVC可以通过实现AsyncController,然后用AsyncManager来实现异步返回,从而节约了CPU资源。

然后效果就来了:

用户请求连接,然后等啊等,等啊等,等到我有新消息了,然后就断线了(返回了结果),然后发现,唉妈呀(粤语Diu,英语Oh, f**k),断了,有新消息了。然后主动去请求了新消息,这时EF就把刚刚存进去新鲜滚热辣的最新消息返回给该用户。用户拿到后在继续请求连接,然后等啊等,等啊等,等啊等,等啊等,等啊等,按后就断线了,唉妈呀,......

然后,实时通信就这么实现了,虽然我觉得是很聪明,但是却很恶心的技术...

JSONP

一般的Web不许跨域请求消息,但是有一个例外,就是引用文件,比如图片、JS文件、CSS,所以就可以把所需要跨域请求的东西通过文件,动态地引用进本页面。

而JSONP就是用这种方式实现用JSON通信的。

实现起来并不神奇,就是客户度先新建一个function,比如叫做callback1234(),并且把方法名同时和请求一起发送回服务端,然后服务端把数据准备好后,包装成callback1234([数据内容]);,并打包进一个.js文件,发送回客户端。客户端收到那个文件后将其添加进引用,然后因为callback1234是本地已有的一个function,所以就执行了callback1234(data),以此将数据推进了你已经定义好了的代码的深渊......

移植到Windows Azure

就是Microsoft的公有云,一开始并没有这个打算。不过为了方便回家能测试,同时上个月正好去了Azure广州的Live to Code(吃喝玩乐,还发了篇博客,就懒得翻出来了),拿了一个还有两天到期的试用账号,所以今晚...呃...好吧,刚才就挂上去了。

挂上去还算比较简单,首先在Azure建立自己的数据库,然后用SQL Server Management Studio连接上,并执行了EF Model First生成的SQL代码就把数据库在上面生成了。

在这里,我做错的就是用DB First生成了Azure用的那个Container,导致1 to 1/0的约束变成了1 to *,烦了我半天。原来直接吧connetionString改了就可以了,不用新建的...

其他代码都是从原有项目复制黏贴上去的,唯一修改的地方就是Web层的Global文件已经失效了,因为不是用IIS启动的,不会被执行。所以添加了一个WebRole(其实是自动添加的),用上面的OnStart()等方法代替了Application_Start()等方法,仅此而已。

在Visual Studio 2012里,右键创建的那个Windows Azure项目,点击publish,然后第一次下一步下一步下一步地设置好,比如用多少个CPU、多少个实例等等,然后就会推送到Azure了。推送完成后马上可以通过自己设置的二级域名打开网站。

我第一次用,不大熟悉,用了cloudapp.com的一级域名,另外建了个windowsazure.com的站点。不过,暂时来说,能挂上去能跑就是好事了,我也太累赶着下班了(其实还不是通宵没睡)。

最后再说一下http://indreamchat./进入这个站点哦,账号快过期,时间有限!时间有限哦!!!

最最后,关于360浏览器和IE6

最最后,作为一个要涉足前端,并且涉足兼容的开发人员,允许我再一次表达对360垃圾浏览器最深刻最深沉最深入的鄙视,以及对IE6最悲痛最悲剧最悲哀的叹息。

(通宵脑子已经很不清醒了,写得怎样就怎样吧,回头再补救...)

posted @ 2013-07-13 06:53 Indream Luo 阅读(1843) 评论(39) 编辑 收藏

评论列表
  
#1楼 2013-07-13 07:28 Yong Zhang  
这玩意不是SignalR干的活吗?用SignalR 10分钟搞定。
  
#2楼 2013-07-13 07:40 Lin.Zheng  
用广播方式,还是群聊天!
  
#3楼 2013-07-13 07:41 Lin.Zheng  
我也通宵加班...现在开始回家!!
  
#4楼 2013-07-13 09:01 迷惘小子  
表示图片挂了
  
#5楼 2013-07-13 09:16 二德子  
360浏览器 6.0 以后的版本用的是chromium内核了,不要鄙视了。
还有在安装360浏览器的电脑上打开ie6会弹出 浏览器版本太低的提示,360也在为消灭ie6做工作。
  
#6楼 2013-07-13 09:25 博客园团队  
图片不能显示,麻烦博主上传一下图片
  
#7楼[楼主] 2013-07-13 10:04 Luo Indream  
@博客园团队
手机收到自动右键,丫在大巴上开机刷手机流量上传图片啊...楼上那位更绝,回复就有图片啊,我的流量啊......
  
#8楼[楼主] 2013-07-13 10:05 Luo Indream  
@Yong Zhang
有空当研究一下,不过主要目的不是实现,是一个可云化+可随时植入任意应用和页面的API+可以被我们自己掌控开发的应用。10分钟:15天,确实开发成本有待商榷。
  
#9楼[楼主] 2013-07-13 10:05 Luo Indream  
@Lin.Zheng
因为不是自己的服务器,所以不心痛资源...
  
#10楼[楼主] 2013-07-13 10:06 Luo Indream  
@Lin.Zheng
哎呀,我还开了个通宵加班组...
  
#11楼[楼主] 2013-07-13 10:06 Luo Indream  
@迷惘小子
你这张图片烧了我三次手机流量啊...上传了,不知道成功不成功
  
#12楼[楼主] 2013-07-13 10:09 Luo Indream  
@二德子
昨天就是一个360 6.2的环境,居然一个string[i]的方法给我undefined,无论是用序列号还是用key,主要是for(var i in string)和for(var i =0;***;i++)都不出结果,连IE6都给我结果了...不过现在360出到9.1了,我也难找其他测试机重现了,只能做了一个Hack。360这会都赶上IE6的大牌了,公司其他几个很多系统都死过在360手上。
  
#13楼 2013-07-13 10:45 sun8134  
@Luo Indream
引用@二德子
昨天就是一个360 6.2的环境,居然一个string[i]的方法给我undefined,无论是用序列号还是用key,主要是for(var i in string)和for(var i =0;***;i++)都不出结果,连IE6都给我结果了...不过现在360出到9.1了,我也难找其他测试机重现了,只能做了一个Hack。360这会都赶上IE6的大牌了,公司其他几个很多系统都死过在360手上。

几年前也被360黑过,关键是当时ie6-9(还没10呢),ff,chrome,op都过了,就**安全浏览器一个div飞了...
不知道数字公司咋做到的...
  
#14楼[楼主] 2013-07-13 11:24 Luo Indream  
@sun8134
引用@Luo Indream
引用引用@二德子
昨天就是一个360 6.2的环境,居然一个string[i]的方法给我undefined,无论是用序列号还是用key,主要是for(var i in string)和for(var i =0;***;i++)都不出结果,连IE6都给我结果了...不过现在360出到9.1了,我也难找其他测试机重现了,只能做了一个Hack。360这会都赶上IE6的大牌了,公司其他几个很多系统都死过在360手上。

几年前也被360黑过,关键是当时ie6-9(还没10呢),ff,chrome,op都过了,就**安全浏览器一个div飞了...
不知道...

所谓安全,或者就是在内核源代码上动手脚吧,当年IE换皮肤的年代,根本没任何安全性可言。现在难得有个开源的还不错的可以改,改得怎么样就不得而知了。好像很安全的感觉...那当然安全了,该有的功能都没了,不用参与自然就安全了
  
#15楼 2013-07-13 15:53 BangQ  
这几天也在做聊天模块,你的这个设计的感觉有些复杂了

我使用的是轮询,客户端每隔2s请求服务端,服务端有客户端的消息直接返回消息,ajax 异步操作。

服务端可以做一个消息队列,定时和到一定量的时候插入数据库。加上数据库设计,开发和测试,也就两天的时间。
当然了,网站的用户本来就不多,用网站聊天的人就更少了。所以,完全满足需求。

不知道和你的这么复杂的设计相比,有什么致命的缺点呢?
  
#16楼 2013-07-13 16:11 flysha  
碉堡了!
  
#17楼 2013-07-13 18:26 Qlin  
自己开发一个 成本高,可能还有好多bug,直接使用现成的SignalR,已经更新很多版本了。
  
#18楼 2013-07-13 18:27 sun8134  
@Luo Indream
引用@sun8134
引用引用@Luo Indream
引用引用@二德子
昨天就是一个360 6.2的环境,居然一个string[i]的方法给我undefined,无论是用序列号还是用key,主要是for(var i in string)和for(var i =0;***;i++)都不出结果,连IE6都给我结果了...不过现在360出到9.1了,我也难找其他测试机重现了,只能做了一个Hack。360这会都赶上IE6的大牌了,公司其他几个很多系统都死过在360手上。

几年前也被360黑过,关键是当时ie6-9(还没10呢),ff,chrome,op都过了,就**安全浏览器一...

说什么安全浏览器就是个名字而已...
  
#19楼[楼主] 2013-07-13 18:39 Luo Indream  
@BangQ
简单有简单的好处,复杂主要是需求。
主要是我们这边像文中所说,用户遍及各地,网络状况层次不齐,但是又有1/4的在服务器所在的内网,所以长轮询是个好(其实是折中,最好的当然是Web Socket直接发送了)方案。在内网的时候我们可以做到即时通信的效果,在外网的时候可以兼容不同的网络状况。比如你说2秒,其实如果慢的话,可能会卡10秒以上很难把控。而长轮询基本上是利用当时网络状况的最佳情况,网络多块消息就多快;其次是空载,长轮询可以由服务端控制断线时间,现在是30秒,也就是那些开着不关没消息的机子30秒刷新一次,最长可以去到4分钟多(移动网络的断线限制5分钟空载),那么就比普通的轮询节约数十倍的资源。
  
#20楼[楼主] 2013-07-13 18:50 Luo Indream  
@BangQ
其次就是消息队列,其实这里也是有消息队列的,利用了EF的二级缓存,可以让其自动控制最大的队列值。老实说,如果自己写消息队列,同时控制上十万个会话可不容易。如果几个几十个会话自己写还好。
我们最高负载是1000员工,我估计基本也就500个开着机的样子,他们打开着财务、HR、考勤、工作流等个个系统,每个人同时拥有工资通知、前台、各个系统的各种通知会话、各个部门的各个群、临时会话、人与人之间的私信会话,而且这些会话都是动态的,随着员工的状态变化,那样我很难控制哪些应该放在队列里,哪些应该删除。
其实现在我这个东西绝对满足不了这种需求,第一期我乐观地觉得能满足50~100个用户就可以了,后期肯定要重构,然后也像你说的那样逐步加入手动队列和缓存、算法优化来解决。
最后的目标就是架构上云化,实现伸缩性。用户多的时候就开多几个服务器,少的时候就关几个,所有服务器都“伪装”成一个服务,这就最好了。
啊...这要多少时间和精力啊...
  
#21楼 2013-07-13 18:51 devil0153  
  
#22楼[楼主] 2013-07-13 19:50 Luo Indream  
@devil0153
今天第二个提到,看了下文档,果然好东西
  
#23楼 2013-07-13 22:36 三度空间  
二哥,推荐个,求互粉,哈哈
  
#24楼 2013-07-13 22:37 Yong Zhang  
@Luo Indream 10分钟当然是开玩笑的。但是你确实可以考虑它,非常优秀,自己可以节约太多开发成本,SignalR已经被ms收编了,所以说各方面的支持也都好。
  
#25楼[楼主] 2013-07-13 23:06 Luo Indream  
@Yong Zhang
查找到了,果然好东西,简单方便
  
#26楼[楼主] 2013-07-13 23:07 Luo Indream  
@三度空间
已粉
  
#27楼 2013-07-14 13:12 麦田里的守望者  
@Luo Indream
文章没仔细看,但是我上次实验了一下,多构造几个假的ajax 请求服务就挂在那里了。
这种情景从下面几点应该会有改善:
ajax comet请求带凭证,服务端验证凭证,如果不合法直接返回
使用前端处理大量的comet请求,这一点有Nginx 的 Http Push Module http://pushmodule./
  
#28楼 2013-07-15 11:28 Care健康  
求源码 47145481#qq.com
  
#29楼[楼主] 2013-07-15 13:01 Luo Indream  
@Care健康
进入页面后查看源代码就可以找到了...
  
#30楼[楼主] 2013-07-15 13:04 Luo Indream  
@麦田里的守望者
其实有带凭证的,用了Form,在你进去的时候就写了。不过以后凭证会由另一个系统去做。
  
#31楼 2013-07-21 17:06 永远的阿哲  
不如用异步做
  
#32楼[楼主] 2013-07-21 17:21 Luo Indream  
@永远的阿哲
可惜需求的场景就是办公中的即时会话
  
#33楼 2013-07-26 13:12 游云  
页面打不开了,有没有相关的前端脚本分享下
  
#34楼[楼主] 2013-07-26 14:40 Luo Indream  
@游云
Azure的试用账号到期了被K掉了...
  
#35楼[楼主] 2013-07-26 14:42 Luo Indream  
@游云
这两周都在重构,把全量的数据方式全部改成增量,另外用jQuery重新封装了API方便他们调用,到时再整理篇东西出来吧。
没什么特别的技术,就是jQuery.Ajax和MVC联合实现的长轮询
  
#36楼 2013-08-14 15:22 游云  
@Luo Indream
恩,以前页面通过jsonp,长轮询的时候好像有进度条的问题,不知道你们有没遇到相关的问题??是怎么处理的??
  
#37楼[楼主] 2013-08-14 15:42 Luo Indream  
@游云
何谓进度条问题?反正详细的实现我发了另一篇文
  
#38楼 2013-11-25 11:36 domaom  
我看到了EF你说的EF二级缓存,不知道你是用什么实现的,AppFabric还是memcached?
  
#39楼[楼主] 2013-11-26 12:02 Luo Indream  
@domaom
没有用到二级缓存。小项目在本地开发的时候数据库和应用在同一个服务器上。而上到去后EF本身自有一个缓存机制,用到的都是小部分最新的数据,所以用不上二级缓存。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多