本文转自博客园:http://www.cnblogs.com/indream/p/3187540.html
公司的OA从零开始进行开发,继简单的单点登陆、角色与权限、消息中间件之后,轮到在线即时通信的模块需要我独立去完成。这三周除了逛网店见爱*看动漫接兼职,基本上都花在这上面了。简单地说就是用MVC4基于长轮询实现(伪)即时通信,利用BootMetro搭建即时聊天系统,同时跨域组件化之后今晚移植到了Azure上方便周末进行第一次迭代的公网测试,地址在http://indreamchat.cloudapp.net/。有兴趣的朋友可以上去送测试数据,剥离了认证登陆,简单地伪装了一个...一个...怎么说,反正能用就好了...
一大早要坐客车回家,所以现在睡也不是不睡也不是,就分享一下实现方式。由于已经回到租房了,所以代码不在手上,写得如何,有待指正。
最后,它长这个样子:
首先介绍下用户情况。
系统的用户除了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的方法(不大懂插入代码...):
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
这些是在Html中提供给chatUiEngine.js的容器,chatUiEngine.js利用它们生成合适的界面元素,将数据渲染上去后展示到容器中,而容器在上面的配置中进行描述。
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
剩下的就是UI中大致的容器了,用一个简单的table搭建出来,然后chatUiEngine就会将界面元素动态导入。
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
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.cloudapp.net/进入这个破站点哦,账号快过期,时间有限!时间有限哦!!!
最最后,关于360浏览器和IE6
最最后,作为一个要涉足前端,并且涉足兼容的开发人员,允许我再一次表达对360垃圾浏览器最深刻最深沉最深入的鄙视,以及对IE6最悲痛最悲剧最悲哀的叹息。
(通宵脑子已经很不清醒了,写得怎样就怎样吧,回头再补救...)