一、簡單例子直觀認識
1.1 模擬場景
假定項目中需要新增一個功能,管理員發布某些信息,這些信息需要推送到所有已經登錄的普通用戶頁面。
1.2 創建Web項目
簡單起見,復用上一篇博客的項目例子,【DWR系列】-DWR簡介及入門例子。即在原項目上直接新增測試。項目結構圖如下:
1.3 修改web.xml
修改web.xml
,使DWR
支持逆向Ajax
,為接收DWR
請求的servlet
簡單的增加一個參數即可:
<init-param> <param-name>activeReverseAjaxEnabled</param-name> <param-value>true</param-value> </init-param>
最終web.xml
如下:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"> <display-name>testweb</display-name> <servlet> <servlet-name>dwr-invoker</servlet-name> <!-- 接收js的Ajax請求的servlet --> <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class> <!-- 啟用逆向Ajax --> <init-param> <param-name>activeReverseAjaxEnabled</param-name> <param-value>true</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dwr-invoker</servlet-name> <!-- 攔截指定的URL --> <url-pattern>/dwr/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
org.directwebremoting.servlet.DwrServlet
可以設置為隨服務器啟動而加載。
1.4 新增被推送頁面
新增被推送頁面normal.jsp
:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>My JSP 'index.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <script type='text/javascript' src='dwr/engine.js'></script> <script type='text/javascript' src='dwr/util.js'></script> </head> <body> <h2>逆向Ajax頁面,服務器推送</h2> <span>推送信息:</span><span id="push"></span> </body> <script type="text/javascript"> window.onload = function(){ dwr.engine.setActiveReverseAjax(true); } </script> </html>
注意需要引入兩個js文件,engine.js
和util.js
,並在頁面加載完后聲明啟用逆向Ajax。
1.5 服務端推送代碼
復用上一篇博客的HelloWorld
,在有參無返回值方法內,將傳遞給后台的信息,推送給登錄的頁面,這也符合開始的需求:
/** * 有參無返回值 */ public void helloYN(final String name){ System.out.println(new Date().toLocaleString() + " js訪問helloYN方法,name=" + name); //將接收到的內容推送到所有的瀏覽器 Browser.withAllSessions(new Runnable(){ @Override public void run(){ Util.setValue("push",name); } }); }
這樣,就可以用第一篇博客的例子進行測試了。
1.6 測試
首先啟動項目,然后訪問index.jsp
模擬管理員登錄,再開兩個瀏覽器或者標簽頁,登錄normal.jsp
模擬普通用戶登錄,登錄如下:
第一個為上一個篇博客的js調用Java方法頁面,下面兩個模擬普通用戶登錄,然后在有參無返回值輸入框輸入文本,點擊按鈕發送觀察下面了個頁面,發現內容幾乎立即顯示出來:
二、逆向Ajax簡介
2.1 簡介
逆向Ajax
俗稱服務端推送
,但是實際意義上的服務端推送在現有條件下是實現不了的,可以設想一下,若服務端可以主動推送內容到客戶端,那么當訪問惡意網站的時候,會有可能被推送病毒或者木馬。所以一般所謂的服務器端推送都是通過其它方式來實現的,比如說輪詢或者長連接。
DWR的逆向Ajax(Reverse Ajax)有三種模式:
- Polling:輪詢模式,DWR會以一個固定時間為周期去服務器獲取數據,這種方式和自己編寫循環執行Ajax一樣。
- Comet:長連接模式,就是服務端持有請求,並不斷的發送數據信息。上面的例子即是Comet模式。
- Piggyback:捎帶模式,即當有推送需求時,等待下一次Ajax請求一並把數據發送過去。
2.2 各種模式選擇
實現簡單的對這三種模式進行比較:
- 響應速度:
Comet
(幾乎瞬時)>Polling
(可自由設置輪詢時間)>Piggyback
(因其不確定性) - 對服務器壓力(連接數較高時):
Comet
>Polling
>Piggyback
通過比較可以發現,高性能等價於高消耗,當系統的主要功能需要用推送來完成且實時性要求高連接數不大的情況下可以使用Comet,連接數較大且對實時性沒有較高要求(一分鍾或以上)可以使用Polling,不建議使用Piggyback。
三、逆向推送進階
不管使用哪種逆向推送都會面臨一個問題,那就是被推送客戶端的選擇問題,大多數情況消息需要被推送到指定的客戶端或指定角色的一系列客戶端。首先通過下面一個例子進行直觀認識。
3.1 創建Web項目
依然為了簡便起見,復用原有項目。在上面進行簡單修改。
3.2 設置不同屬性值
既然要選擇不同的客戶端進行推送,就要有選擇的依據,Web項目中常用的選擇依據就是根據用戶不同,進行區分。但是要有用戶就要有登錄模塊,再次通過其它方式進行模擬。
3.2.1 通過URL將用戶傳入
在訪問被推送頁面的時候,將用戶ID通過參數傳遞給JSP,例如:
http://localhost:8080/dwr/normal.jsp?userId=yiwangzhibujian
3.2.2 設置到session中
再將獲取到的userId
放到session
,至此就模擬完登錄過程:
String userId=request.getParameter("userId");
session.setAttribute("userId",userId);
正常系統的登錄操作一般都會講用戶ID放到session中,此處進行簡單模擬。
3.3 新增js
之前已經在normal.jsp
中開啟了逆向Ajax功能,現在則需要開啟關閉頁面提醒服務器功能:
dwr.engine.setNotifyServerOnPageUnload(true);
然后將此進行注冊(注冊說法有些不妥當,先這么理解,后續會對功能進行詳細解釋):
HelloWorld.regist();
注冊復用了HelloWorld
類,在里面新增了一個方法。
最終normal.jsp
如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>My JSP 'index.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <script type='text/javascript' src='dwr/engine.js'></script> <script type='text/javascript' src='dwr/util.js'></script> <script type='text/javascript' src='dwr/interface/HelloWorld.js'></script> <% //模擬登錄 //獲取登錄用戶 String userId=request.getParameter("userId"); //將登錄用戶放到session中 session.setAttribute("userId",userId); %> </head> <body> <h2>逆向Ajax頁面,服務器推送,當前用戶:${userId }</h2> <span>推送信息:</span><span id="push"></span> </body> <script type="text/javascript"> window.onload = function(){ //開啟逆向Ajax功能 dwr.engine.setActiveReverseAjax(true); //開啟關閉頁面提醒服務器功能 dwr.engine.setNotifyServerOnPageUnload(true); //對當前用戶進行注冊 HelloWorld.regist(); } </script> </html>
后續將對注釋內容進行詳解。
3.4 修改服務端類
依然復用HelloWorld
類,實際項目中最好不這么做:
3.4.1 新增注冊方法
/** * 當頁面開啟時注冊用戶 */ public void regist(){ // 獲取當前的scriptSession ScriptSession scriptSession=WebContextFactory.get().getScriptSession(); //獲取HttpSession 並獲得其中的userId HttpSession session=WebContextFactory.get().getSession(); String userId=(String) session.getAttribute("userId"); // 對當前scriptSession的key設置指定的值 scriptSession.setAttribute("key",userId); }
這個方法實際工作是將不同的屬性值放置到ScriptSession
中供過濾器使用,實際工作中可以使用監聽器ScriptSessionListener
來完成這個工作。
3.4.2 新增推送方法
推送方法我們復用HelloWorld
的有參有返回值方法,這樣可以將傳入的參數進行推送,傳入參數限定格式為,推送用戶 推送內容(忽略校驗,請按格式輸入):
/** * 有參有返回值 */ public String helloYY(final String name){ //獲得傳入的值進行分解,推送用戶 推送內容 final String[] param=name.split("[ ]{1,}"); System.out.println(new Date().toLocaleString() + " js訪問helloYY方法,name=" + name); //對符合條件的用戶進行推送 Browser.withAllSessionsFiltered(new ScriptSessionFilter(){ @Override public boolean match(ScriptSession session){ boolean isYou=param[0].equals(session.getAttribute("key")); return isYou; } },new Runnable(){ @Override public void run(){ Util.setValue("push",param[1]); } }); return "給" + param[0] + "成功推送一條消息"; }
最終的HelloWorld類內容如下:
package yiwangzhibujian; import javax.servlet.http.HttpSession; import org.directwebremoting.*; import org.directwebremoting.ui.dwr.Util; import java.util.Date; /** * @author yiwangzhibujian */ @SuppressWarnings("deprecation") public class HelloWorld{ /** * 無參無返回值 */ public void helloNN(){ System.out.println(new Date().toLocaleString() + " js訪問helloNN方法"); } /** * 有參無返回值 */ public void helloYN(final String name){ System.out.println(new Date().toLocaleString() + " js訪問helloYN方法,name=" + name); // 將接收到的內容推送到所有的瀏覽器 Browser.withAllSessions(new Runnable(){ @Override public void run(){ Util.setValue("push",name); } }); } /** * 無參有返回值 */ public String helloNY(){ System.out.println(new Date().toLocaleString() + " js訪問helloNY方法"); return "Hello World!"; } /** * 有參有返回值 */ public String helloYY(final String name){ // 獲得傳入的值進行分解,推送用戶 推送內容 final String[] param=name.split("[ ]{1,}"); System.out.println(new Date().toLocaleString() + " js訪問helloYY方法,name=" + name); // 對符合條件的用戶進行推送 Browser.withAllSessionsFiltered(new ScriptSessionFilter(){ @Override public boolean match(ScriptSession session){ boolean isYou=param[0].equals(session.getAttribute("key")); return isYou; } },new Runnable(){ @Override public void run(){ Util.setValue("push",param[1]); } }); return "給" + param[0] + "成功推送一條消息"; } /** * 當頁面開啟時注冊用戶 */ public void regist(){ // 獲取當前的scriptSession ScriptSession scriptSession=WebContextFactory.get().getScriptSession(); // 獲取HttpSession 並獲得其中的userId HttpSession session=WebContextFactory.get().getSession(); String userId=(String) session.getAttribute("userId"); // 對當前scriptSession的key設置指定的值 scriptSession.setAttribute("key",userId); } }
3.5 測試
依然和上面的測試一樣,登錄三個頁面,第一個為推送信息頁面,后兩個為用戶頁面,注意帶上用戶參數:
然后在第一個頁面的有參有返回值框進行輸入測試,測試結果如下:
測試結果通過,可以進行精准推送。