Pushlet
前些天有个同学叫我提供一个网页聊天工具给他,由于在学校期间我们关系还不错,现在虽然好久不联系,我还是一下子就答应了他。聊天工具需要实时,交谈的双方有一方发言,另一方能及时收到内容,一开始我想到ajax轮询(polling),感觉这种方式做不到实时,效率也差。我早听说有一种服务器推技术,由于没有实际需要,没有研究,现在也正好有个机会了,便下载了pushlet源码详细读了下。
什么是服务器推?
传统的B/S是一种请求/响应的模式,用户请求服务器响应,这一响应客户端页面就刷新了,为了提供更好的用户体验,出现了ajax,它通过类似后台线程的方式向服务器发送请求,服务器响应后使用javascript更新DOM,实现了不刷新。但是它获取服务器数据离不开请求,服务器推技术就是服务器在需要的时候,客户端没有请求也向客户端发送数据的技术。
实现原理——长链接
先看下pushlet的push-html-stream.jsp源文件
<%
try {
for (int i=1; i < 10; i++) {
out.print("<h1>"+i+"&【】lt;/h1>");
out.flush();
System.out.println("running...");//这句是我加的
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
out.print("<h1>"+e+"</h1>");
}
}
} catch (IOException e) {
out.print("<h1>"+e+"</h1>");
}
out.print("<h1>DONE</h1>");
%>
上面就是一个长链接,服务器使用一个循环,不断的向客户端输送数据,这和javascript的循环是不同的,因为它的数据都是在服务器端产生的。
这种运行方式有一些缺点,一是服务器无法感知客户端是否已经离开,即使客户端浏览器已经关闭,服务器端的for循环也不会中止。另一个是客户端浏览器状态会一直提示运行状态,直到循环结束。
pushlet是怎么解决这两个问题的呢?
第一个问题是JSP会“吞食”IOException,pushlet使用servlet,当用户关闭浏览器的时候,服务器在向客户端输出内容会产生异常,这个异常可以捕获,这个问题留在后面详细讨论。
第二个问题使用到不刷新技术,pushlet提供了两种解决方式:
1:使用隐藏的iframe,使用iframe请求服务器,获取数据
2:使用ajax
相关的事例可以看源码中的raw.html和raw-ajax.html
pushlet框架的设计
pushlet使用观察者模式,这种模式当主题发生变化时,会通知所有订阅了该主题的订阅者,正好符合服务器推的要求。
主要类讲解
1、事件源相关类
pushlet的事件源,也就是数据源,是服务器产生数据的地方。pushlet数据源使用后台线程
循环产生事件,再把产生的事件用路由器广播到各个订阅者。涉及的相关类如下:
Event:服务器事件,也可以说是数据。
EventQueue:事件队列,事件源产生的事件存放地
EventSource:数据源接口,定义了如何产生数据
EventPullSource: pull方式数据源抽象类,使用后台线程循环生产Event,
EventSourceManager:管理所有数据源,包括激活、中止和销毁
Dispatcher:路由器,数据源产生事件后,使用它把事件广播到合适的会话Session的订阅者Subscriber中。Subscriber再会把Event事件保存到EventQueue。
需要提醒的是,当不是所有的事件(数据)都是产生自数据源,比如在即时聊天中,只要用户发送的内容达服务器,服务器就把这个内容封装在一个响应事件中,把它加入到EventQueue,供Subscriber取出发送到客户端
2、处理用户请求相关类
pushlet接到一个用户请求后,会使用长链接(while循环),不断的从EventQueue中取数据,发现有数据就广播到客户端。
Pushlet
是一个servlet,代表中央处理器,负责协调各个接口处理用户请求
Session
代表用户订阅开始的会话,可以把它看作是观察者。
SessionManager
管理会话,包括产生和删除会话。它使用定时器定时监所有Session,如果超时就回收。
Subscription
代表订阅的单个主题
Subscriber
代表订阅者,它的作用管理用户订阅的所有主题,接收Dispatcher发送过来的Event
Controller
控制器,主要是执行用户请求,根据用户请求的Event,产生响应的Event发送到客户端。
Command
命令。包装会话、客户端适配器、请求和响应Event,用来让控制器Controller执行调用
ClientAdapter
客户端适配器接口。不同的适配器向客户端输送不同格式的内容,pushlet提供了它个实现,分别是:BrowserAdapter、SerializedAdapter、XMLAdapter。
BrowserAdapter
浏览器适配器,使用这个适配器向客户端发送javascript数据
XMLAdapter
XML适配器,如果使用ajax方式请求,pushlet会使用这种适配器向客户端返回XML数据
一些疑问
一、怎么把事件发送到适合的订阅者手中?
事件源产生的事件后,会向所有的会话Session广播,每个Session中包含了一个订阅者Subscriber,而订阅者又管理着它订阅的主题,如果它发现Event的主题而它订阅的不同,会放弃这个Event.
二、如果客户端已经离开,比如关闭浏览器,服务器如何终止工作?
对于jsp“吞食”IOException的问题,pushlet提供了两种解决方式:
1、使用servlet代替jsp,当客户端关闭时正常捕获IOException异常,并退出长链接。
pushlet的stream模式就是这种方式
2、不使用长链接
在poll或pull模式下,pushlet响应客户端的一个请求后,强迫客户端刷新。
在长链接中有这么一段代码:
while(isActive()) {
....略
if (mode.equals(MODE_PULL) || mode.equals(MODE_POLL)) {
sendRefresh(clientAdapter, refreshURL);
// Always leave loop in pull/poll mode
break;
}
}
上面说的是服务器发送一个命令叫客户端刷新请求服务器,并退出这个长链接,如果客户端关闭了,那么就不会有请求送到服务器,这个长链接就不会重新激活,服务器也就不能向客户端输送数据了。