30分鐘學會反向Ajax

場景1:當有新郵件的時候,網頁自動彈出提示信息而無需用戶手動的刷新收件箱。javascript

場景2:當用戶的手機掃描完成頁面中的二維碼之後,頁面會自動跳轉。html

場景3:在相似聊天室的環境中有任何人發言,全部登陸用戶均可以即時看見信息。java

與傳統的MVC模型請求必須從客戶端發起由服務器響應相比,使用反向Ajax可以模擬服務器端主動向客戶端推送事件從而提升用戶體驗。本文將分兩個部分討論反向Ajax技術,包括:Comet和WebSocket。文章旨在演示如何實現以上兩種技術手段,Struts2或SpringMVC中的應用並未涉及。此外,Servlet的配置也採用註解的方式,相關知識你們能夠參考其它資料。react

1、Comet(最佳的兼容手段)jquery

Comet本質上則是這樣的一種概念:可以從服務器端向客戶端發送數據。在一個標準的 HTTP Ajax 請求中,數據是發送給服務器端的,反向 Ajax 以某些特定的方式來模擬發出一個 Ajax 請求,這樣的話,服務器就能夠儘量快地向客戶端發送事件。因爲普通HTTP請求每每會伴隨頁面的跳轉,而推送事件則須要瀏覽器停留在同一個頁面或者框架下,所以Comet的實現只可以經過Ajax來完成。web

它的實現過程以下:頁面加載的時候隨即向服務器發送一條Ajax請求,服務器端獲取請求並將它保存在一個線程安全的容器中(一般爲隊列)。同時服務器端仍然能夠正常響應其餘請求。當須要推送的事件到來的時候,服務器遍歷容器中的請求在返回應答後刪除。因而全部停留在頁面中的瀏覽器都會得到該應答,並再次發送Ajax請求,重複上述過程。ajax

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
            + path + "/";
%>
<!DOCTYPE html>
<html lang="en">
<base href="<%=basePath%>">
<head>
<title>WebSocket</title>
<script type="text/javascript" src="static/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
    $(function() {
        connect();
        $("#btn").click(function() {
            var value = $("#message").val();
            $.ajax({
                url : "longpolling?method=onMessage&msg=" + value,
                cache : false,
                dataType : "text",
                success : function(data) {

                }
            });
        });
    });
    function connect() {
        $.ajax({
            url : "longpolling?method=onOpen",
            cache : false,
            dataType : "text",
            success : function(data) {
                connect();
                alert(data);
            }
        });
    }
</script>
</head>
<body>
    <h1>LongPolling</h1>
    <input type="text" id="message" />
    <input type="button" id="btn" value="發送" />
</body>
</html>

咱們注意到,由btn發送的請求其實並不須要獲取應答。整個過程的關鍵是須要客戶端始終讓服務器保持connect()的請求。而服務器端首先須要支持這種異步的響應方式,幸運的是目前爲止絕大部分的Servlet容器都已經提供了良好的支持。下面以Tomcat爲例:瀏覽器

package servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(value="/longpolling", asyncSupported=true)
public class Comet extends HttpServlet {
    private static final Queue<AsyncContext> CONNECTIONS = new ConcurrentLinkedQueue<AsyncContext>();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getParameter("method");
        if (method.equals("onOpen")) {
            onOpen(req, resp);
        } else if (method.equals("onMessage")) {
            onMessage(req, resp);
        }
    }

    private void onOpen(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        AsyncContext context = req.startAsync();
        context.setTimeout(0);
        CONNECTIONS.offer(context);
    }

    private void onMessage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String msg = req.getParameter("msg");
        broadcast(msg);
    }

    private synchronized void broadcast(String msg) {
        for (AsyncContext context : CONNECTIONS) {
            HttpServletResponse response = (HttpServletResponse) context.getResponse();
            try {
                PrintWriter out = response.getWriter();
                out.print(msg);
                out.flush();
                out.close();
                context.complete();
                CONNECTIONS.remove(context);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

ConcurrentLinkedQueue是Queue隊列的一個線程安全實現,這裏使用它來做爲保存請求的容器。AsyncContext是Tomcat支持的異步環境,不一樣的服務器使用的對象也略有不一樣。Jetty支持的對象是Continuation。完成了廣播的請求須要經過context.complete()將相關請求結束,並使用CONNECTIONS.remove(context)刪除隊列。安全

2、WebSocket(來自HTML5的支持)服務器

 使用 HTTP 長輪詢的 Comet 是可靠地實現反向 Ajax 的最佳方式,由於如今全部瀏覽器都提供了這方面的支持。

WebSockets 在 HTML5 中出現,是比 Comet 更新的反向 Ajax 技術。WebSockets 支持雙向、全雙工通訊信道,並且許多瀏覽器(Firefox、Google Chrome 和 Safari)也支持它。鏈接經過 HTTP 請求(也稱爲 WebSockets 握手)和一些特殊的標頭 (header)。鏈接一直處於激活狀態,您能夠用 JavaScript 編寫和接收數據,正如您使用原始 TCP 套接字同樣。

經過輸入 ws:// 或 wss://(在 SSL 上)啓動 WebSocket URL。如圖:

首先:WebSockets並不是在全部瀏覽器上都能得到良好的支持,顯然IE又拖了後腿。所以當你打算使用這項技術以前必須考慮到用戶的使用環境,若是你的項目面向的是互聯網或者包括手機端用戶,奉勸你們三思。

其次:WebSockets提供的請求區別於普通的HTTP請求,它是一種全雙工通訊且始終處於激活狀態(若是你不去關閉它的話)。這就意味着你不用每次得到應答後再次向服務器發送請求,這樣能夠節約大量的資源。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
            + path + "/";
    String ws = "ws://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<!DOCTYPE html>
<html lang="en">
<base href="<%=basePath%>">
<head>
<title>WebSocket</title>
<script type="text/javascript" src="static/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
    $(function() {
        var websocket = null;
        if ("WebSocket" in window){
            websocket = new WebSocket("<%=ws%>websocket");
        } else {
            alert("not support");
        }
        websocket.onopen = function(evt) {
        }
        
        websocket.onmessage = function(evt) {
            alert(evt.data);
        }
        
        websocket.onclose = function(evt) {
        }
        
        $("#btn").click(function() {
            var text = $("#message").val();
            websocket.send(text);
        });
    });
</script>
</head>
<body>
    <h1>WebSocket</h1>
    <input type="text" id="message" />
    <input type="button" id="btn" value="發送"/>
</body>
</html>

JQuery對WebSocket還未提供更良好的支持,所以咱們必須使用Javascript來編寫部分代碼(好在並不複雜)。而且打部分常見的服務器均可以支持ws請求,以Tomcat爲例。在6.0版本中WebSocketServlet對象已經被標註爲@java.lang.Deprecated,7.0之後的版本支持jsr365提供的實現,所以你必須使用註解來完成相關配置。

package servlet;

import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/websocket")
public class WebSocket {
    private static final Queue<WebSocket> CONNECTIONS = new ConcurrentLinkedQueue<WebSocket>();
    private Session session;

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        CONNECTIONS.offer(this);
    }

    @OnMessage
    public void onMessage(String message) {
        broadcast(message);
    }

    @OnClose
    public void onClose() {
        CONNECTIONS.remove(this);
    }

    private synchronized void broadcast(String msg) {
        for (WebSocket point : CONNECTIONS) {
            try {
                point.session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
                CONNECTIONS.remove(point);
                try {
                    point.session.close();
                } catch (IOException e1) {

                }
            }
        }
    }
}

3、總結(從請求到推送)

在傳統通訊方案中,若是系統 A 須要系統 B 中的信息,它會向系統 B 發送一個請求。系統 B 將處理請求,而系統 A 會等待響應。處理完成後,會將響應發送回系統 A。在同步 通訊模式下,資源使用效率比較低,這是由於等待響應時會浪費處理時間。

異步 模式下,系統 A 將訂閱它想從系統 B 中獲取的信息。而後,系統 A 能夠向系統 B 發送一個通知,也能夠當即返回信息,與此同時,系統 A 能夠處理其餘事務。這個步驟是可選的。在事件驅動應用程序中,一般沒必要請求其餘系統發送事件,由於您不知道這些事件是什麼。在系統 B 發佈響應以後,系統 A 會當即收到該響應。

Web 框架過去一般依賴傳統 「請求-響應」 模式,該模式會致使頁面刷新。隨着 Ajax、Reverse Ajax 以及 WebSocket 的出現,如今能夠將事件驅動架構的概念輕鬆應用於 Web,得到去耦合、可伸縮性和反應性 (reactivity) 等好處。更良好的用戶體驗也會帶來新的商業契機。

相關文章
相關標籤/搜索