三周,用长轮询实现Chat并迁移到Azure测试公司的OA从零开始进行开发,继简单的单点登陆、角色与权限、消息中间件之后,轮到在线即时通信的模块需要我独立去完成。这三周除了逛网店见爱*看动漫接兼职,基本上都花在这上面了。简单地说就是用MVC4基于长轮询实现(伪)即时通信,利用BootMetro搭建即时聊天系统,同时跨域组件化之后今晚移植到了Azure上方便周末进行第一次迭代的公网测试,地址在http://indreamchat./。有兴趣的朋友可以上去送测试数据,剥离了认证登陆,简单地伪装了一个...一个...怎么说,反正能用就好了... 一大早要坐客车回家,所以现在睡也不是不睡也不是,就分享一下实现方式。由于已经回到租房了,所以代码不在手上,写得如何,有待指正。 最后,它长这个样子:
首先介绍下用户情况。 系统的用户除了1/4的内网用户外,基本上都是全国各地办事处的外网用户,而且出差尤多,特别是海外,这是网络状况。 也就是我们的用户遍布世界各地,有不同的网络状况,而且世界上大部分可以想得到的设备都可能会是接入端,所以一开始就有较高兼容需求。
回到即时通信,其实只是其难点而已,总体来说是一个Chat和消息推送模块,允许各个子系统按照需求用群组会话组织管理用户,并推送系统消息,同时允许用户间的通信交流,并且满足移植性,使得不同子系统能直接引用。 下文先交代架构,然后最后交流下关键技术吧。 那么直接用文字说下架构吧,从上到下是从底层(数据库)到顶层(UI):
以下逐层详细说明: 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的持久化控制分成两层,是为了让客户端在有需要的时候获取部分数据,而不需交互。 UIUI所作的就是提供容器(显示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最悲痛最悲剧最悲哀的叹息。 (通宵脑子已经很不清醒了,写得怎样就怎样吧,回头再补救...) 评论列表
@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都过了,就**安全浏览器一... 说什么安全浏览器就是个名字而已... @BangQ
简单有简单的好处,复杂主要是需求。 主要是我们这边像文中所说,用户遍及各地,网络状况层次不齐,但是又有1/4的在服务器所在的内网,所以长轮询是个好(其实是折中,最好的当然是Web Socket直接发送了)方案。在内网的时候我们可以做到即时通信的效果,在外网的时候可以兼容不同的网络状况。比如你说2秒,其实如果慢的话,可能会卡10秒以上很难把控。而长轮询基本上是利用当时网络状况的最佳情况,网络多块消息就多快;其次是空载,长轮询可以由服务端控制断线时间,现在是30秒,也就是那些开着不关没消息的机子30秒刷新一次,最长可以去到4分钟多(移动网络的断线限制5分钟空载),那么就比普通的轮询节约数十倍的资源。 @BangQ
其次就是消息队列,其实这里也是有消息队列的,利用了EF的二级缓存,可以让其自动控制最大的队列值。老实说,如果自己写消息队列,同时控制上十万个会话可不容易。如果几个几十个会话自己写还好。 我们最高负载是1000员工,我估计基本也就500个开着机的样子,他们打开着财务、HR、考勤、工作流等个个系统,每个人同时拥有工资通知、前台、各个系统的各种通知会话、各个部门的各个群、临时会话、人与人之间的私信会话,而且这些会话都是动态的,随着员工的状态变化,那样我很难控制哪些应该放在队列里,哪些应该删除。 其实现在我这个东西绝对满足不了这种需求,第一期我乐观地觉得能满足50~100个用户就可以了,后期肯定要重构,然后也像你说的那样逐步加入手动队列和缓存、算法优化来解决。 最后的目标就是架构上云化,实现伸缩性。用户多的时候就开多几个服务器,少的时候就关几个,所有服务器都“伪装”成一个服务,这就最好了。 啊...这要多少时间和精力啊... @Luo Indream
文章没仔细看,但是我上次实验了一下,多构造几个假的ajax 请求服务就挂在那里了。 这种情景从下面几点应该会有改善: ajax comet请求带凭证,服务端验证凭证,如果不合法直接返回 使用前端处理大量的comet请求,这一点有Nginx 的 Http Push Module http://pushmodule./ @domaom 没有用到二级缓存。小项目在本地开发的时候数据库和应用在同一个服务器上。而上到去后EF本身自有一个缓存机制,用到的都是小部分最新的数据,所以用不上二级缓存。 |
|