Slide1
随着互联网应用的蓬勃发展,WEB应用页面也变得复杂和功能强大,随之而来的是一些新技术的产生,在这次演讲中我会介绍一些比较新的WEB2.0技术和这些技术在J2EE框架下的实现和运用,主要包括BIG-PIPE(延迟页面区域加载),反向AJAX(COMET),DUST TEMPLATE(客户端渲染技术),对于每种技术,我会介绍它的运用场景和`一些技术细节,如架构和实现等,主要是抛砖引玉,希望和感兴趣的同学共同探讨
Slide2
首先给大家介绍的是BIG-PIPE(延迟页面区域加载技术),为了让大家对这种技术产生的背景有直观的了解,先介绍几种常见的CLIENT/SERVER交互模型,如图所示,这是一种最基本的模型,客户端发请求给SERVLET容器中的CONTROLLER,CONTROLLER依次调用多个SERVICE获取渲染页面每个部分需要的数据,当所有数据获取完成后转到JSP页面.这种模型有一个很明显的缺陷,在数据获取完成前用户将看到一个全白的页面,在图中的例子用户需要等待500+100+200 = 800毫秒才能看到渲染完成的页面,如果数据加载慢,那这对用户体验有非常大的影响
Slide3
随着AJAX技术的广泛应用,用户体验问题有了很大的改观,在第二张图中,CONTROLLER获得页面请求后直接转到JSP页面,这时页面大部分的静态元素已经可以展示给客户,页面再发起三个AJAX请求,分别获取三个SECTION的数据并且局部渲染三个SECTION,由于是并行加载,快速加载完的数据会先开始渲染,图中客户将在大约100毫秒后先看到SECTION2,200毫秒后看到SECTION3,500毫秒后看到SECTION1
Slide4
这种方式带来了一个新的问题,一个页面需要4个请求才能加载,如果数据块多的话请求也会随之增多,如果客户访问量大的话,会对服务器造成不小的负担. SCALABILITY是我们需要面对的下一挑战,在这张图的方案中,我们把三AJAX请求合成了一个,再增加一个参数告诉服务器那些SECTION需要渲染,服务器会在数据准备完成后将三个SECTION的渲染结果合并发给客户端,再由一段客户端脚本处理分割分别展示,在没有并行加载的情况下,这需要800毫秒,然后客户会同时看到三个SECTION一起出现
Slide5
那么有没有两全其美的方法能够同时兼顾用户体验和可扩展性呢?答案就是我所要介绍的BIGPIPE延迟页面区域加载技术(DEFERREDFRAGMENT),这是由FACEBOOK提出的一种技术,它的核心思想并不复杂,和AJAX技术类似,页面静态的部分会先被展示,再利用HTTP CHUNK传输的特性将渲染完的内容逐步推送到客户端,整个页面需要一个请求就可以加载完成
Slide6
我再用之前的例子给大家一个更直观的展示
首先整个页面会被加载,页面上预留了三个容器DIV,值得一提的是整个RESPONSE在这时并没有完成,这时服务器端开始用多线程并行加载所有的SECTIONS,SECTION2的数据在100毫秒后准备完毕并渲染,对应的HTML输出会被推送到客户端,这个SECTION会被放在一个不可见的DIV中,当SECTION推送完成后,服务器会再推送一段JS脚本,这个脚本的目的是将SECTION 2的OUTPUT移到容器DIV2中展示出来,同理200秒后渲染SECTION 3到页面的底部并移到容器DIV3,500秒后渲染SECTION1到页面底部并移到容器DIV1.这时整个RESPONSE才算COMMIT.通过这种机制页面上加载快或者重要的内容可以被先展示出来,整个页面的加载时间取决于最慢的块
Slide7
讲完了原理再来讲讲实现,EBAY用JSP标签实现了BIGPIPE的原理,在这个源代码例子中
JSP页面声明了两种标签,deferredFragment标签声明了一块需要延迟加载的区域,每个fragment有一个唯一的标识符,fragmentRenderer渲染页面所有声明的DeferredFragment,这个标签必须加在页面的末端.可见使用标签是非常简单的,而且对页面结构的侵入性很小
Slide8
除了在JSP中使用标签外,服务端获取数据的过程也有所改变
DependencyManager是一个核心类,需要用户继承它来实现数据加载和在数据加载完成后唤醒fragmentRender标签的渲染线程,这个类提供了两种数据加载的方式,inlineStart同步加载和start异步加载(可并行)在这段代码中展示的是异步加载,数据加载任务被提交给了一个线程池,数据加载完毕后调用了onDependencySatisfied方法唤醒fragmentRender数据已经准备就绪,可以开始渲染对应的fragment并将渲染结果推送给客户端,onDependencySatisifed方法的参数对应deferredFragment的ID
Slide9
如何使用DependencyManager?这里的代码示例是一个springmvc的controller,可以在controller的方法中获取一个dependencyManager的实例,并且通过DependencyManager.setDependencyManage在框架中注册,获取数据的业务逻辑被移到了dependencyManager中
Slide10
下面介绍一下整个框架中主要的组件和关系,首先JSP页面解析到deferredFragment标签的时候,并不直接解析中间包括的内容,而是将Fragment在dependencyManager中注册,dependencyManager会根据依赖关系构建一棵依赖关系树.依赖主要有两种,数据依赖FRAGMENT的渲染依赖于服务准备数据的完成,块依赖,FRAGMENT的渲染依赖别的FRAGMENT的渲染完成.当SERVICE准备数据完成后,会提示dependencyManager,dependencyManager会检查那些FRAGMENT的依赖以经全部满足,并且将这些FRAGMENT加入到一个内部的渲染队列中,fragmentRenderer标签解析的时候会不断地从内部的渲染队列中获取准备完毕的Fragment,并解析渲染Fragment中间包括的内容,写到JSP WRITER中,并且调用FLUSH方法推送
Slide11
STUBHUB已经运用了BIG PIPE技术对页面进行了优化,如图所示,这是我们的一个ticket detail页面,这个页面的动态信息并不多,但是之前的加载速度却很慢,主要是How do you get ticket这一块业务逻辑比较复杂,但是占页面的比例却相对很小,我们用这个页面做了BIGPIPE的POC,让几块红色的区域延迟加载,主要的信息能够很快地展现给客户.由于实现很简便,我们可以用很小的代价和很低的风险快速的提升用户体验,给相对比较耗时的后端优化提供了更多的时间缓冲,取得了很好的效果
Slide12
下面进入第二个议题
大家知道,传统的AJAX是由客户端发起的请求,但是在很多情况下,服务器会产生一些数据并需要主动推送给客户端,反向AJAX,顾名思义,正是提供了这种功能,这种技术可以在多种场景下运用,如网页聊天,实时弹出框提醒这类应用,客户端需要和服务器保持一个长时间的连接因为服务器返回数据的时间具有不确定性,另一类应用是实时性较强的系统,如监控图表,股票交易系统等
Slide13
反向AJAX技术的实现有很多种,POLLING和PIGGYBACK是比较简单的实现,COMET是基于长连接的实现,相对复杂但比较流行,这里会重点为大家介绍. WEBSOCKET是比较新的技术,目前HTML5的规范已经包括并且获得多种浏览器的支持. JAVA也出了自己的WEBSOCKET SPEC,从长远来看,这将成为一种标准,但是我对这种技术目前研究的不多,所以只是在这里提一下
Slide14
Polling是一种相当简单的反向AJAX实现,客户端以一定的频率向服务端发送请求,服务器如果没有数据生成则返回空响应,有数据生成则在响应中返回数据.它其实是模拟了反向推送的效果.值 得一提的是,这种技术虽然简单,但是在很多情况下却十分有效,如果服务端的数据更新很频繁,如实时监控等,通过减小轮询间隔轮询能达到较好的实时性,相反 如果服务端推送数据频率不高或具有不确定性,如网页聊天等,那么大部分带宽被浪费返回空响应,给服务器造成了不比要的压力.
这里是一段代码,客户端通过JAVASCRIPT函数setInterval进行轮询,很简单
Slide15
轮询的流程图,比较简单大家看一下就好
Slide16
Piggyback一定程度上弥补了Polling缺陷来减少带宽开销.客户端不再以固定的频率向服务端发送数据而是按照正常需要向服务端发送请求,如果在两次请求之间服务端有需要推送的数据生成了,服务端就会在第二次请求的响应中捎带上需要推送的数据.响应接收的脚本需要根据一个标识位判断是否有推送的数据,来进行不同的处理. Piggyback仍然有比较大的缺陷就是它的实时性不好,假如客户端两次请求的间隔时间很长,那么服务生成数据到推送完成需要等待较长的时间.
Slide17
Piggyback流程图,比较简单大家看一下就好
Slide18
Comet又称为长连接技术,它特别适用于网页聊天或实时提醒这样的应用,由于服务端推送的时间不可知,需要客户端保持和服务端的连接来等待推送,另外值得一提的是HTTP的一次连接一定是一个请求,一个响应(keep-alive除外),当收到第一次服务端推送的数据后连接会被关闭,那么服务器怎样再推送更多的数据呢?下面会介绍几种保持长连接的方法ForeverIframe,Multipart (comet streaming), Long polling
Slide19
ForeverIframe在页面嵌入了一个隐藏的IFRAME,SRC属性指向了返回事件的服务,这里又用到了我们前面提到的Http Chunked transfer来保证这个FRAME永远不会完全渲染完成,这就是Forever的由来,服务器推送的数据会放在一段SCRIPT脚本中在客户端被执行,它的优点是早期的浏览器也支持这种方式,缺点是无法检查连接的有效性,进行超时或异常的处理
Slide20
Streaming模式利用了HTTP MULTIPART响应,客户端发起AJAX请求,响应类型被标识为MULTI PART.在这种模式下,服务器推送多次数据是在同一个响应中进行的,响应并没有被COMMIT引起连接关闭,每次推送数据会引起客户端XHR对象的READY STATE的变化,客户端需要实现这一回调函数处理数据. STREAMING模式能够方便地进行超时和异常处理,但服务器端需要特别处理来挂起连接
Slide21
Longpolling技术与上面两种最大的不同是一次连接只能处理一次数据推送,客户端同样发起一个AJAX请求,这次HTTP连接被保持直到服务器产生第一次需要推送的数据或者连接超时.这时客户端AJAX的回调函数会重新创建一个连接,新建立的连接可以和之前的保持同样的连接ID,逻辑上来说,这仍然是同一个连接.另外值得一提的是超时的设置很重要,超时时间保证了服务端能及时释放空等的线程
Slide22
这里给大家演示一个COMET应用实例,源自STUBHUB网站实现了一个买家卖家自动匹配的功能,如果买家买票的时候恰好缺票,他可以设置一个提醒,当一个卖家卖出数量可匹配的票时,卖家会收到一个实时的弹出框提示,已有买家对他卖出的票感兴趣.这里用到了一个开源的COMET框架COMET4J,代码比较清晰简洁,不足之处是依赖于TOMCAT提供的COMET支持.
首先需要对COMET4J进行配置,使用COMET功能需要用到TOMCAT的NIO CONNECTOR,同时需要配置COMET4J的核心组件CometServlet
Slide23
配置完成后,需要实现一个WEB CONTEXTLISTENER来初始化COMET4JCONTEXT,这里定义了一个名为ticketMatchedEvent的通道,通道的概念非常类似于JMS的TOPIC,建立长连接的客户端可以订阅一个通道,服务端产生的数据可以推送给所有订阅此通道的客户端
同时在LISTENER中又启动了一根线程TicketMatchResultDispatcher,负责轮询匹配结果,如果有买家卖家匹配的结果,推送给客户端
Slide24
在网站页面初始化的时候,需要调用COMET4J的JAVASCRIPT函数来创建一个长连接,COMET4J支持LONG POLLING和STREAMING两种模式,这里使用了默认的LONG POLLING模式. /conn在WEB.XML中被映射到了CometServlet. CometServlet会把建立的长连接保存到COMMET引擎中
Slide25
买家卖家的请求会被提交给自动匹配引擎,如之前所说TicketMatchResultDispatcher线程,负责从匹配引擎轮询匹配结果,如果有匹配的结果,调用COMET引擎的SENDTOALL方法,注意指定的通道还是ticketMatchedEvent
Slide26
客户端为通道注册一个JAVASCRIPT回调函数,在函数中处理服务器推送的数据,显示一个弹出框提示卖家,之前提到的LONGPOLLING重连逻辑已经在COM4JCLIENT框架中实现,对开发者透明
Slide27
尽管反向AJAX技术能够做出许多很COOL的效果,但是使用时仍然有一些局限性,首先是兼容性的问题,MULTI PART RESPONSE在不同的浏览器下可能有不同的表现,另外COMET需要SERVLET容器提供相应的支持,每个容器TOMCAT,JETTY等都有自己的COMET API,今后可能WEBSOCKET将成为标准,但是主流SERVLET容器支持这一特性仍然需要一定的时间
可扩展性是我们需要考虑的又一问题,在传统的服务器IO模型中,一根主线程负责接收连接请求,然后为每个连接`请求分配一根线程负责读写,这种模型称为THREAD PER CONNECTION模型.当并发连接高的时候,THREAD数将随之增高,CONTEXT SWITCHING的代价将成为主要瓶颈,随着JAVANIO技术的出现,可以由一根或多根线程负责接收请求以及读写,再由一组线程负责后续操作,线程数量可以由线程池控制而并不随并发连接数的增加而增加,这种模型称为THREAD PERREQUEST,这种模型很适合处理反向AJAX技术带来的高并发的问题,所以我们看到之前COMET4J需要配置TOMCAT NIO的连接器.然而COMET又带来了一个新问题,长连接可能需要占用请求处理线程很长的一段时间,但是这段时间是在等待服务器产生数据,线程并没有进行有意义的操作而空占资源.最好能在这段时间内将线程挂起或释放,缓存请求状态,当服务器数据准备完成后再唤醒或重新从线程池中分配一根线程来负责推送数据,JETTY CONTINUATION和SERVLET3.0的ASYNC REQUEST都试图解决这样的问题,但是高并发下保存状态带来内存开销的问题仍然不可忽略.
Slide28
最后再简单介绍一下客户端模板技术,大家对于传统的模板技术如JSP,FREEMARKER, VELOCITY等都不陌生,这些模板的渲染依赖于JVM,或者j2EE容器,客户端模板技术和传统模板技术从概念上来说没有区别,只是它的渲染是在客户端浏览器进行的,WEB2.0页面通常具有复杂的JS脚本和样式,这使得前端测试变得重要.客户端渲染使得页面展示能够脱离容器,对于前端开发测试解耦有着十分积极的意义,服务器只负责提供数据,在测试的时候,这些数据可以很容易地被替换成JSON数据来模拟不同的业务场景,开发时也能做到所见即所得.同时客户端渲染也在一定程度上减少了服务器的处理压力
Slide29
再来看看客户端模板技术DUST的处理流程,DUST技术的核心是DUST.JS.首先模板源码会通过DUST.JS的编译函数被编译成一段JS源码,页面可以直接引用这段JS源码,也可以通过DUST.JS提供的LOADSOURCE函数将代码加载到引擎中,编译过的模板可以通过一个唯一的名字来引用,DUST使用JSON对象作为数据,在渲染的时候服务器返回的JSON对象和模板名字作为DUST RENDER函数的参数,RENDER完成后生成一段HTML片段,这个HTML片段可以被插入页面
Slide30
这是一个DUSTTEMPLATE的HELLO WORLD例子,对应前面描述的处理流程,大家可以看一下
Slide31
再来看看DUST模板本身的一些特性,DUST支持两种模式,预编译和实时编译,在预编译模式下,所有的模板会被编译成JS,在页面中直接引用编译后的JS,这种方式效率高,适合在生产环境使用.实时编译可以直接引用模板的源文件名,通过调用DUST.JS提供的函数完成编译和加载源码的操作,然后才渲染模板,这种方式使用简单,但是有额外开销,使页面加载明显变慢,通常可以在开发环境中使用.模板语言常见的特性基本上DUST都支持,如条件判断,循环等,模板嵌套和重载增加了可复用性,自定义函数增加了灵活性,大家可以去DUST官方网站了解详细,这里不做具体介绍了