服务器推送技术

服务器推送技术(Server Push)是最近Web技术中最热门的一个流行术语,它的别名叫Comet(彗星)。它是继AJAX之后又一个倍受追捧的Web技术。服务器推送技术最近的流行与AJAX有着密切的关系。本文详细介绍了服务器推送技术,希望对你有帮助。

AD:

推送技术Server Push的基础思想是将浏览器主动查询信息改为服务器主动发送信息。服务器发送一批数据,浏览器显示这些数据,同时保证与服务器的连接。当服务器需要再次发送一批数据时,浏览器显示数据并保持连接。以后,服务器仍然可以发送批量数据,浏览器继续显示数据,依次类推。

客户端拉曳(Client Pull)

在客户端拖曳技术中,服务器发送一批数据,在HTTP响应或文档头标记中插入指令,让浏览器“在5秒内再次装入这些数据”或“10秒内前往某URL装入数据”。当指定的时间达到时,客户端就按照服务器的指示去做,或者刷新当前数据,或者调入新的数据。

其实push 和 pull 这两种技术手段非常不同,但目的几乎一致,都是为了给最终用户方便的提供最新信息。

在服务器推送技术中,HTTP 连接一直保持着,直到服务器知道自己已结束发送数据并发送一个结束信号,或者客户端中断连接。而在客户端拖曳技术中,并不保持HTTP连接,相反,客户端被告知合时建立新连接,以及建立连接是获取什么数据。

在服务器推送中,奇妙之处在于“multipart/mixed”格式的MIME,它能够使一个报文(或HTTP响应)包含许多数据项、在客户端拖曳中,奇妙之处在于HTTP响应头标(或等效的HTML元素),它能告知客户端在指定的延时时间后执行何种动作。

服务器推送通常效率要比客户端拖曳效率高,因为它不必为后续数据建立新的连接。由于始终保持连接,即使没有数据传输时也是这样,因此服务器必须愿意分配这些TCP/IP端口,对于TCP/IP端口数有限的服务器这将是一个严重的问题。

客户端拖曳效率低,因为这必须每次为传送数据建立新的连接。但是它不必始终保持连接。

在实际情况中,建立HTTP连接通常需要花费相当多的时间,多达一秒甚至更多。因此从性能上考虑,服务器推送对于最终用户更有吸引力,特别是对于需要经常更新信息的情况下。

服务器推送相对客户端拖曳的另一点优势是,服务器推送相对比较容易控制。例如,服务器每一次推送时都保持一个连接,但它又随时可以关闭其中的任何连 接,而不需要在服务器上设置特殊的算法。而客户端拖曳在同样的情况下要麻烦许多,它每次要与服务器建立连接,服务器为了处理将客户端拖曳请求与特定的最终 用户匹配等情况,需要使用相当麻烦的算法。

如果实现服务器推送的CGI程序是使用Shell脚本语言编写的,有时会存在一些问题。例如,客户端最终用户中断连接,Shell程序通常不能注意 到,这将使资源毫无用处的浪费掉,解决这一问题的办法是用Perl或者C来编写这类CGI程序,以使用户中断连接时能够结束运行。

如上所述,在服务器推送中,多个响应中连接始终保持,使服务器可在任何时间发送更多的数据。一个明显的好处是服务器完全能够控制更新数据的时间和频率。另外,这种方法效率高,因为始终保持连接。缺点是保持连接状态会浪费服务器端的资源。服务器推送还比较容易中断。

接下来就大概说说服务器推送技术

服务器在响应请求时,HTTP使用MIME报文格式来封装数据。通常一个HTTP响应只能包含一个数据块。但MIME有一种机制可用一个报文(或 HTTP响应)表示将多个数据块,这种机制就是成为“multipart/mixed”的标准MIME类型。multipart/mixed报文大体格式 如下:
Content-type:multipart/mixed;boundary=ThisRandomString
--ThisRandomString
Content-type:text/plain
第一个对象的数据。
--ThisRandomString
Content-type:text/plain
第二个对象的数据。
--ThisRandomString-- 

上述报文包括两上数据块,二者的类型都是“text/plain”。最后一个“ThisRandomString”后的两条短线(--)表示报文结束,后面没有数据。

对于服务器推送,使用一个“multipart/mixed”类型的变种--multipart/x-mixed-replace。这里,“x-” 表示属于实验类型。“replace”表示每一个新数据块都会代替前一个数据块。也就是说,新数据不是附加到旧数据之后,而是替代它。

下面是实际使用的“multipart/x-mixed-replace”类型:
Content-type:multipart/x-mixed-replace;boundary=ThisRandomString
--ThisRandomString
Content-type:text/plain
第一个对象的数据
--ThisRandomString
Content-type:text/plain
第二个(最后一个)对象的数据。
--ThisRandomString-- 

使用这一技术的关键是,服务器并不是推送整个“multipart/x-mixed-replace”报文,而是每次发送后数据块。

HTTP连接始终保持,因而服务器可以按自己需要的速度和频率推送新数据,两个数据块之间浏览器仅需在当前窗口等候,用户甚至可以到其他窗口做别的 事情,当服务器需要发送新数据时,它只是源(ABC输入法没那个字*&^$#)传输管道发送数据块,客户端相应的窗口进行自我更新。

在服务器推送技术中,“multipart/x-mixed-replace”类型的报文由唯一的边界线组成,这些边界线分割每个数据块。每个数据 块都有自己的头标,因而能够指定对象相关的内容类型和其他信息。由于“multipart/x-mixed-replace”的特性是每一新数据块取代前 一数据对象,因而浏览器中总是显示最新的数据对象。  

“multipart/x-mixed-replace”报文没有结尾。也就是说,服务器可以永远保持连接,并发送所需的数据。如果用户不再在浏览 器窗口中显示数据流,或者浏览器到服务器间的连接中间(例如用户按“STOP”按钮),服务器的推送才会中断。这是人们使用服务器推送的典型方式。

当浏览器发现“Content-type”头标或到达头标结束处时,浏览器窗口中的前一个文档被清除,并开始显示下一个文档。发现下一个报文边界时,就认为当前数据块(文档)已经结束。 

总之,服务器推送的数据由一组头标(通常包括“Content-type”)、数据本身和分割符(报文边界)三部分组成。浏览器看到分割符时,它保持状态不变,直到下一个数据块到达。

将以上概念进行用编程方法实现,就可以得到实际的服务器推送程序。例如,下面的Unix shell程序将使浏览器每5秒显示一次服务器上的进程列表:
#!/bin/sh
echo "HTTP/1.1 200"
echo "Content-type: multipart/x-mixed-replace;boundary=--ThisRandomString--"
echo ""
echo "--ThisRandomString--"
while true
do
echo "Content-type: text/html"
echo ""
echo "h2Processes on this machine updated every 5 seconds/h2"
echo "time:"
date
echo "p"
echo "plaintext"
ps -el
echo "--ThisRandomString--"
sleep 5
done  

注意到,边界设置在sleep语句之前发送,这能够确保浏览器清除其缓冲区,并显示所接收到的最新数据。
NCSA HTTPD用户在内容类型中不能使用空格,包括边界参数。NCSA HTTPD只能将不带空格字符的字符串作为内容类型。如果在内容类型行中存在空格(冒号后面的空格除外),空格后的任何文本都会被删除。

下面的示例是正确的:
Content-type: multipart/x-mixed-replace;boundary=ThisRandomString
而下例则不能正常工作,因为它在中间有空格:
Content-type: multipart/x-mixed-replace; boundary=ThisRandomString

服务器推送的另一个优点是它可以针对单个内联图象进行。包括图象的文档可以由服务器定时或定周期进行更新。而实现这一点非常简单:只需使IMG元素的SRC属性指向推送一系列图象的URL即可。

如果服务器推送用于单个内联图象,文档中的图象就会一次次被新推送来的图象所代替,而文档本身不需变化(假设文档没有进行服务器推送)。这样,WEB页面中有限的动画就可以为静态画面所代替。

___________________________________________________________________________________________________

下面介绍在ARP之上的一个非常热门的技术实现:服务器推送技术。

服务器推送技术(Server Push)是最近Web技术中最热门的一个流行术语,它的别名叫Comet(彗星)。它是继AJAX之后又一个倍受追捧的Web技术。服务器推送技术最近的流行与AJAX有着密切的关系。

随着Web技术的流行,越来越多的应用从原有的 C/S模式转变为B/S模式,享受着Web技术所带来的各种优势(例如跨平台、免客户端维护、跨越防火墙、扩展性好等)。但是基于浏览器的应用,也有它不 足的地方。主要在于界面的友好性和交互性。由于浏览器中的页面每次需要全部刷新才能从服务器端获得最新的数据或向服务器传送数据,这样产生的延迟所带来的 视觉感受非常糟糕。因此很多的桌面应用为了获得更友好的界面放弃了Web技术,或者采用浏览器的插件技术(ActiveX、Applet、Flash 等)。但是浏览器插件技术本身又有许多问题,例如跨平台问题和插件版本兼容性问题。

随着 AJAX技术的兴起,让广大开发人员又一次看到了使用浏览器来替代桌面应用的机会,并且这次机会非常大。AJAX将整个页面的刷新变成页面局部的刷新,并 且数据的传送是以异步方式进行,这使得网络延迟带来的视觉差异将会消失。AJAX还利用DHTML和丰富的JavasSript语言来模拟桌面系统的各种 事件和响应过程,以及平滑滚动和拖拽的效果。还不止这些,更有一些IT巨头(Google、Sun、Oracle等)提供了非常丰富的AJAX开发工具, 使得开发和调试AJAX应用变得简单高效,并且开发的AJAX应用还可以跨越各种浏览器和操作系统。在这种情况下基于AJAX的Web应用迅速涌起,吞噬 着原有桌面系统的份额。聊天工具、邮件阅读器、博客编辑器,甚至是Office办公软件和文字处理软件在浏览器中都有着美丽的外观和几乎可以与桌面系统媲 美的交互界面。Google更是提出“有了浏览器和Google,就不需要微软”的口号和策略。在AJAX的世界中,除了传统的CAD设计软件和大型游戏 软件等因为对系统硬件的苛刻需求,还离不开桌面系统以外,似乎其他所有的应用都可以变成Web应用了。

但是,在浏 览器中的AJAX应用中存在一个致命的缺陷无法满足传统桌面系统的需求。那就是“服务器发起的消息传递(Server-Initiated Message Delivery)”。在很多的应用当中,服务器软件需要向客户端主动发送消息或信息。因为服务器掌握着系统的主要资源,能够最先获得系统的状态变化和事 件的发生。当这些变化发生的时候,服务器需要主动地向客户端实时地发送消息。例如股票的变化。在传统的桌面系统中,这种需求没有任何问题,因为客户端和服 务器之间通常存在着持久的连接,这个连接可以双向传递各种数据。而基于HTTP协议的Web应用却不行。上节中也提到过,在Web世界中,服务器永远是被 动地发送数据,前提是客户端必须先发送请求。浏览器其实并不知道服务器的信息什么时候会有改变,为了模拟实时的交流,或者不想错过某些信息,只能通过轮询 (Polling)技术不断刷新页面来获得最新的数据(见图18-5)。这种方式不但浪费服务器的资源,最重要的是每次建立(或关闭)新的HTTP连接都 有一定的延迟,这种延迟使得频繁信息传递的应用无法忍受。于是就产生了“服务器推送技术”。

图18-5  Web请求的轮询技术

“服务器推送技术”在很久以前就出现过。例如Netscape曾经推出适用于Push技术的专用浏览器和经过修改的HTML语言。但是这仅仅在特定的浏览器中才能使用,其他流行的浏览器(IE等)就不兼容这种技术。

现在的“服务器推送技术”是保持原有的HTTP协议 不变,在服务器端改变处理方式,使得服务器能够使用浏览器已经打开的HTTP连接,主动向浏览器发送消息(见图18-6)。这里关键的技术是要保持原有的 HTTP连接不断。一旦拥有持久的连接,服务器就可以根据自己的数据更新,随时地向客户端发送最新的信息。

图18-6  服务器数据推送技术

在GlassFish中,Grizzly通过NIO 的技术实现了异步请求服务(ARP),并在ARP之上扩展了服务器推送技术的实现,将其也命名为“Comet”。因为使用了NIO,Grizzly才可以 在保持HTTP连接的同时,并不会绑定固定的线程,使得GlassFish具有很好的扩展性,可以很好地同时支持大量的Comet请求。下面我们来分析 Grizzly中对Comet的实现。

18.2.1  Comet实现的分析

如图 18-7所示,Comet的实现是基于ARP之上的,因此整个框架结构仍然符合ARP的模式。读者可以与“新邮件提醒功能”做一个比较,大部分的代码都相 类似。最大的不同就是“新邮件提醒功能”是Grizzly的一个扩展,而Comet却已经是Grizzly的一部分,它与其他Grizzly的核心 Java包位于同样重要的位置。所有的Comet的实现都在com.sun. enterprise.web.connector.grizzly.comet包中。

因为是 ARP的扩展,所以它的入口仍然是AsyncFilter接口的实现。Comet对AsyncFilter接口的实现是CometAsyncFilter 类。这个类的注册比“新邮件提醒功能”要简单,只需要在GlassFish的启动配置文件(domain.xml)中加上<property name="cometSupport" value="true"/>就行了,在SelectorThreadConfig类中就会读取到(见例18.12),并且调用 SelectorThread中的enableCometSupport方法(见例18.13)将CometAsyncFilter类注册到系统。

图18-7  Comet实现类结构图

【例18.12】在SelectorThreadConfig类中打开Comet功能:

if (System.getProperty(ENABLE_COMET_SUPPORT) != null){

    selectorThread.enableCometSupport(

    Boolean.valueOf(System.getProperty(ENABLE_COMET_SUPPORT)).booleanValue());

}

【例18.13】SelectorThread中的enableCometSupport方法:

protected void enableCometSupport(boolean enableComet){

    if ( enableComet ){

        asyncExecution = true;

        setBufferResponse(false);   

        isFileCacheEnabled = false;

        isLargeFileCacheEnabled = false;

        asyncHandler = new DefaultAsyncHandler();

        asyncHandler.addAsyncFilter(new CometAsyncFilter());

        SelectorThread.logger()

.log(Level.INFO,"Enabling Grizzly ARP Comet support.");

    } else {

        asyncExecution = false;

    }

}

在CometAsyncFilter类中,最重要的方法就是doFilter,它是Comet与异步请求处理(ARP)框架的接口。

【例18.14】CometAsyncFilter中的doFilter方法:

public boolean doFilter(AsyncExecutor asyncExecutor) {

AsyncProcessorTask apt =

(AsyncProcessorTask) asyncExecutor.getAsyncTask();

    CometEngine cometEngine = CometEngine.getEngine();               

    try{

        if (!cometEngine.handle(apt)) {

            return true;

        }

    } catch (IOException ex){

        logger.log(Level.SEVERE,"CometAsyncFilter",ex);

    }

    return false;

}

从例18.14可以看出,在doFilter方法之中,所有的操作都交给CometEngine的handle方法。

CometEngine是Comet应用中最先接触的类。如果一个Servlet或JSP页面要想成为Comet请求,那么在编程的时候需要经过以下几步。

(1)   获得CometEngine的实例对象,并将需要成为Comet请求的路径注册:

CometEngine cometEngine = CometEngine.getEngine();
CometContext cometContext = cometEngine.register(contextPath);

(2)   注册一个CometHandler:

cometContext.addCometHandler(handler);

(3)   最后,如果有消息发送,可以通过下面的方法通知所有注册的通道:

cometContext.notify(handler);

有关CometContext和CometHandler类,在下面的内容会进行稍微详细的描述。

当请求处理交给CometEngine对象以 后,CometEngine以及其他几个类(CometContext和CometHandler等)就会对这个请求的生命周期负起全部的责任。 Comet请求和其他的请求不一样,它需要长时间地保持HTTP连接,来保证服务器端能够利用这些连接主动发送消息给浏览器客户端。因此 CometEngine并没有使用主线程的Selector(在SelectorThread中运行的Selector,而是使用了自己的 Selector对象:CometSelector,而让主线程的Selector负责其他类型的请求读取和处理。CometSelector的主要职责 是负责已经注册的Comet请求的生命周期:哪些Comet请求的连接被用户关闭或异常关闭,哪些Comet请求根据配置已经超时。在这些情况下,需要系 统释放相应的资源,使得系统更加稳定和健壮。

而CometContext的作用则是应用程序和 Comet实现之间的桥梁。CometHandler可以利用它来注册,因此CometContext掌握了当前Comet应用中所有注册了的频道。这样 当其中有一个频道利用CometContext来发送消息时,CometContext能够将消息主动发送给所有注册的Handler。这些对象的关系, 可以通过一个典型的例子的讲解更加清楚的展现出来。

18.2.2  Comet实例讲解——“聊天室”应用

“聊天室”是一个非常典型的Comet应用。通常的 “聊天室”至少需要包含两个基本的功能:发送本人的消息和接受显示别人的消息。这里的Comet应用主要是指接受别人的消息。因为别人什么时候发送了消息 浏览器是不会知道的,只有聊天服务器本身知道,如果想要将各种消息实时地通知各个客户端,就需要服务器推送技术。

现有的很多“聊天室”大多使用轮询 (Polling)技术,来使得浏览器不断自动刷新以获得最新的消息。这种实现方法在并发用户不太多的情况下还能接受。如果并发用户非常多,服务器的负担 就会大大地增加。另外每次重新建立连接所带来的延迟也使得用户不能非常及时地获得最新的消息。综合这些因此,对“聊天室”的最佳实现应该使用Comet技 术,也就是“服务器推送技术”。

下面来讲解一个使用GlassFish的Comet来实现的“聊天室”。在本书所附的CD中有详细的代码和步骤来部署和运行“聊天室”应用。

在“聊天室”中,只有一个Servlet和几个JSP页面文件。JSP页面非常简单,只是简单的HTML。所有的请求处理都在Servlet中。

【例18.15】Servlet中的init方法:

...

public void init(ServletConfig config) throws ServletException {

    super.init(config);

    contextPath = config.getServletContext().getContextPath() + "/chat";

    CometEngine cometEngine = CometEngine.getEngine();                // [1]

    CometContext context = cometEngine.register(contextPath);        // [2]

    context.setExpirationDelay(20*1000);       // [3]
}
...

从例18.15的代码可以看出,Servlet在初始化的时候做了以下三件事情。

(1)   获得了一个CometEngine的实例对象。上文已经解释过,CometEngine对象是Comet应用的入口。任何Comet应用都需要CometEngine对象来注册Comet请求的路径。

(2)   将当前的路径向CometEngine进行注册。显然,当Comet功能打开的时候,GlassFish不会将所有的请求都认为是Comet请求,而是仅 仅当请求的路径和将注册的路径相匹配的时候才会进行Comet处理。注册成功的结果是返回一个CometContext对象。上文已经解释 过,CometContext是每个用户之间交流的桥梁。

(3)   设置当前Comet应用的超时的阀值。

【例18.16】Servlet的doPost方法中的部分代码(一):

...

public void doPost(HttpServletRequest request,

            HttpServletResponse response)

            throws ServletException, IOException

{

    String action = request.getParameter("action");

    CometEngine cometEngine = CometEngine.getEngine();

    CometContext cometContext = cometEngine.getCometContext(contextPath);

...

}

从例18.16的代码中可以看出,在处理Comet 请求,与其他用户交互的时候,是需要先获得CometContext的。需要指出的是,CometEngine对象是一个单例对象 (Singleton),只会存在一个实例,因此任何时候调用getEngine的方法都会获得同一个实例。

【例18.17】Servlet的doPost方法中的部分代码(二):

...

if (action != null) {

if ("login".equals(action)) {

    String username = request.getParameter("username");

    request.getSession(true).setAttribute("username", username);

    if (firstServlet != -1){

        cometContext.notify("User " + username

            + " from " + request.getRemoteAddr()

            + " is joining the chat.<br/>",CometEvent.NOTIFY,firstServlet);

    }

...

从例18.17的代码可以看出,当用户登录成功后,除了将用户信息保存到session中之外,还会通过cometContext向所有其他用户发出“新用户登录”的信息。

【例18.18】Servlet的doPost方法中的部分代码(三):

...

else if ("post".equals(action)){

String username = (String) request.getSession(true)

                .getAttribute("username");

String message = request.getParameter("message");

cometContext.notify("[ " + username + " ]  " + message + "<br/>");

response.sendRedirect("post.jsp");

return;

...

例18.18的代码是在处理用户“说话”的情况。如果用户在自己的发送消息框中向其他在线的用户发送了一些消息,Servlet在处理的时候就是通过cometContext来通知所有的在线用户。

【例18.19】Servlet的doPost方法中的部分代码(四):

...
else if ("openchat".equals(action)) {

    response.setContentType("text/html");

    String username = (String) request.getSession(true)

                                   .getAttribute("username");

    response.getWriter().println("<h2>Welcome "+ username + " </h2>");

    CometRequestHandler handler = new CometRequestHandler();

    handler.clientIP = request.getRemoteAddr();

    handler.attach(response.getWriter());

    cometContext.addCometHandler(handler);        

    return;

...

例18.19的代码演示的是“聊天消息显示”的功 能,这才是真正Comet的请求,这个请求的连接是一直保持打开着的,等待着服务器主动将最新的信息发送到浏览器。这段代码中最主要的内容就是向 CometContext注册了一个CometHandler。注册之后,这个Handler就会等待服务器端的回调,来完成向浏览器输出的功能。

【例18.20】CometRequestHandler类的onEvent方法:

public class CometRequestHandler implements CometHandler<PrintWriter>{

public void onEvent(CometEvent event) throws IOException{  

        try{

            if (firstServlet != -1 && this.hashCode() != firstServlet){

                event.getCometContext().notify("User " + clientIP

                + " is getting a new message.<br/>",CometEvent.NOTIFY,

                firstServlet);

            }

            if (event.getType() != CometEvent.READ){

                printWriter.println(event.attachment());

                printWriter.flush();

            }

        } catch (Throwable t){

            t.printStackTrace();

        

}

...

         

例18.19的代码解释了CometRequestHandler类在收到了系统的函数回调之后,进入到onEvent方法。在onEvent方法的处理中,仅仅是简单地将系统传递过来的消息通过一直保持的HTTP连接向客户传过去。

当“聊天室”应用运行的时候,用户界面如图18-8所示。其中下半部分是发送消息的部分,它的处理代码对应于例18-18。上半部分是对话消息显示的部分,它的处理代码对应于例18.19。

chat

图18-8  “聊天室”应用的用户界面

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值