框架簡介:DWR(Direct Web Remoting)
是一個用於改善web頁面與Java類交互的遠程服務器端Ajax開源框架,可以幫助開發人員開發包含AJAX技術的網站。它可以允許在瀏覽器裏的代碼使用運行在WEB服務器上的JAVA函數,就像它就在瀏覽器裏一樣。
本Demo實現的基本功能:
點擊jsp界面的按鈕,通過DWR調用到服務器端的java代碼,在控制檯打印出jsp輸入框中的值
Demo構建流程:
1.新建Web工程
2.導入jar包:commons-logging-x.x.x.jar和dwr3.0.jar
3.在web.xml中加入DWR使用能力:
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<servlet-class>uk.ltd.getahead.dwr.DWRServlet</servlet-class >
<init-param>
<param-name> debug</param-name >
<param-value> true</param-value >
</init-param>
<init-param>
<param-name>crossDomainSessionSecurity</param-name >
<param-value>false</param-value >
</init-param>
<init-param>
<param-name>allowScriptTagRemoting</param-name >
<param-value>true</param-value >
</init-param>
</servlet>
<servlet-mapping>
<servlet-name> dwr-invoker</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>
4.在src中新建類MessagePush:
package sugar.dwr;
public class MessagePush {
public void send(String str){
System. out.println(str);
}
}
5.與web.xml同級目錄下創建dwr.xml,用來配置js函數與java代碼的映射關係:
<?xml version="1.0" encoding= "UTF-8"?>
<!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 3.0//EN" "http://getahead.org/dwr/dwr30.dtd" >
<dwr>
<allow>
<create creator="new" javascript= "messagePush">
<param name="class" >sugar.dwr.MessagePush</ param>
</create>
</allow>
</dwr>
6.在index.jsp中寫入js邏輯(該處使用到jquery,請自行添加):
<%@ page language= "java" import ="java.util.*" pageEncoding="UTF-8" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head >
<title >DWR</ title>
<script type= "text/javascript" src ="js/jquery-1.8.3.js"></ script>
<script type= "text/javascript" src ="dwr/util.js"></ script>
<script type="text/javascript" src= "dwr/engine.js"></script >
<script type="text/javascript" src= "dwr/interface/messagePush.js" ></script>
</head >
<body >
<table border="0" >
<tr>
<td>< input id ="content" type="text" /></td>
<td>< input id ="send" type="button" value= "send"/></td >
</tr>
</table>
<script type="text/javascript" >
$( "#send").click(function(){
var content = $("#content" ).val();
messagePush.send(content);
});
</script>
</body >
</html>
說明:jsp文件中必須引入幾個js,它們都是隱含存在的,不用考慮它們在哪兒。其中engine.js和util.js是固定的。另外的一個js的名稱就是dwr.xml中配置的類名。
1.服務器推送技術和DWR的推送方式
傳統模式的 Web 系統以客戶端發出請求、服務器端響應的方式工作,服務端不能主動發送請求(消息)給客戶端。
這種方式並不能滿足很多現實應用的需求,譬如:
監控系統:後臺硬件熱插拔、LED、溫度、電壓發生變化;
即時通信系統:其它用戶登錄、發送信息;
即時報價系統:後臺數據庫內容發生變化;
這些應用都需要服務器能實時地將更新的信息傳送到客戶端,而無須客戶端發出請求。“服務器推”技術在現實應用中有一些解決方案,這些解決方案大致可以分爲兩類:
一類需要在瀏覽器端安裝插件,基於套接口(Socket)傳送信息,或是使用 RMI、CORBA 進行遠程調用;而另一類則無須瀏覽器安裝任何插件、基於 HTTP 長連接。
而基於客戶端套接口的“服務器推”技術大體可以分爲:
1.傳統輪詢:在 Web 早期,這一點常使用 meta 刷新實現。這將自動指示瀏覽器在指定秒數之後重新裝載頁面,從而支持簡陋的輪詢( polling )。例如在 HTML 文件中加入 ,實際上就是 HTTP 頭標告知瀏覽器每 12 秒更新一次文檔。
優點:不需要服務器端配置
缺點:用戶體驗度差
對服務器的壓力很大,帶寬流失嚴重
2.ajax輪詢:Ajax 隔一段時間(通常使用 JavaScript 的 setTimeout 函數)就去服務器查詢是否有改變,從而進行增量式的更新。但是間隔多長時間去查詢成了問題,因爲性能和即時性造成了嚴重的反比關係。間隔太短,連續不斷的請求會沖垮服務器,間隔太長,務器上的新數據就需要越多的時間才能到達客戶機。
優點:不需要太多的服務器端配置
降低了帶寬的負荷(因爲返回的不是完整的頁面)
缺點:對服務器的壓力並沒有減少
實時性差,有一定的延遲
應用:這種技術很常見,很多webmail應用程序就是通過這種技術在電子郵件到達時顯示電子郵件的。
3.comet:Comet 方式通俗的說就是一種長連接機制 (long lived http) 。同樣是由 Browser 端主動發起請求,但是 Server 端以一種似乎非常慢的響應方式給出回答。這樣在這個期間內,服務器端可以使用同一個 connection 把要更新的數據主動發送給 Browser 。因此請求可能等待較長的時間,期間沒有任何數據返回,但是一旦有了新的數據,它將立即被髮送到客戶機。Comet 又有很多種實現方式,但是總的來說對 Server 端的負載都會有增加 . 雖然對於單位操作來說,每次只需要建議一次 connection, 但是由於 connection 是保持較長時間的 , 對於 server 端的資源的佔用要有所增加。
優點:實時性好(消息延時小);性能好(能支持大量用戶)
缺點:長期佔用連接,喪失了無狀態高併發的特點。
應用:股票系統。實時通訊。
4.flash xml socket:這種方案實現的基礎是:一、 Flash 提供了 XMLSocket 類。二、 JavaScript 和 Flash 的緊密結合:在 JavaScript 可以直接調用 Flash 程序提供的接口。
優點:實時性好(消息延時小);性能好(能支持大量用戶)
缺點:因爲 XMLSocket 沒有 HTTP 隧道功能, XMLSocket 類不能自動穿過防火牆;
因爲是使用套接口,需要設置一個通信端口,防火牆、代理服務器也可能對非 HTTP 通道端口進行限制;
應用:網絡聊天室,網絡互動遊戲。
5.java appet:在客戶端使用 Java Applet ,通過 java.net.Socket 或 java.net.DatagramSocket 或 java.net.MulticastSocket 建立與服務器端的套接口連接,從而實現 “ 服務器推送 ” 。
DWR採用的是長連接機制。
1、長連接技術通過客戶端發出請求獲取服務器端數據的方式通常稱爲”拉”技術,很形象說明客戶端在拉取服務器端數據,而有時候需要服務器端主動向客戶端”推”數據,比如監測聊天上線人數主動向上線發送消息,後臺數據庫發生變化是主動更新所有客戶端展示。
2、Reverse Ajax實現服務器推技術DWR2.x的推技術也叫DWR Reverse Ajax(逆向Ajax)主要是在BS架構中,從服務器端向多個瀏覽器主動推數據的一種技術。DWR的逆向Ajax主要包括兩種模式:主動模式和被動模式。其中主動模式包括polling和comet兩種,被動模式只有piggyback這一種:
(1)piggyback方式,是默認的方式。如果後臺有什麼內容需要推送到前臺,是要等到那個頁面進行下一次ajax請求的時候,將需要推送的內容附加在該次請求之後,傳回到頁面。只有等到下次請求頁面主動發起了,中間的變化內容才傳遞迴頁面。
(2)comet方式當服務端建立和瀏覽器的連接,將頁面內容發送到瀏覽器之後,對應的連接並不關閉,只是暫時掛起。如果後面有什麼新的內容需要推送到客戶端的時候直接通過前面掛起的連接再次傳送數據。服務器所能提供的連接數目是一定的,在大量的掛起的連接沒有關閉的情況下,可能造成新的連接請求不能接入,從而影響到服務質量。
(3)polling方式由瀏覽器定時向服務端發送ajax請求,詢問後臺是否有什麼內容需要推送,有的話就會由服務端返回推送內容。這種方式和我們直接在頁面通過定時器發送ajax請求,然後查詢後臺是否有變化內容的實現是類似的。
2.ScriptSession的生命週期
客戶端每次請求(刷新)都會生成一個新的ScriptSession,當客戶端請求時或者客戶端退出時將會銷燬ScriptSession。
通過ScriptSession我們可以得到客戶端(瀏覽器)的腳本執行權。即我們可以直接調用瀏覽器的js代碼。
本節Demo實現的基本功能:
在上節( DWR3.0框架入門(1) —— 實現ajax )的代碼基礎上修改,實現將index.jsp輸入框中的推送文本顯示在其它打開的jsp頁面上
3.Demo修改的流程
1.修改MessagePush中的send方法:
package sugar.dwr;
import java.util.Collection;
import org.directwebremoting.Browser;
import org.directwebremoting.ScriptBuffer;
import org.directwebremoting.ScriptSession;
public class MessagePush {
public void send(final String content){
Runnable run = new Runnable(){
private ScriptBuffer script = new ScriptBuffer();
public void run() {
//設置要調用的 js及參數
script.appendCall("show" , content);
//得到所有ScriptSession
Collection<ScriptSession> sessions = Browser.getTargetSessions();
//遍歷每一個ScriptSession
for (ScriptSession scriptSession : sessions){
scriptSession.addScript( script);
}
}
};
//執行推送
Browser. withAllSessions(run);
}
}
2.新建receiver.jsp用來接收來自服務器的消息:
<%@ page language= "java" import ="java.util.*" pageEncoding="UTF-8" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head >
<title >dwr接收</title >
<script src="js/jquery-1.8.3.js" ></script>
<script type="text/javascript" src= "dwr/util.js"></script >
<script type="text/javascript" src= "dwr/engine.js"></script >
<script type="text/javascript" src= "dwr/interface/messagePush.js" ></script>
</head >
<body >
dwr接收<br />
<div id="content" style=" width: 200px ;height: 30px;border : 1px solid ; text-align: center ; padding: 5px;"></ div>
<script type="text/javascript" >
//這個方法用來啓動該頁面的ReverseAjax功能
dwr.engine.setActiveReverseAjax( true);
//設置在頁面關閉時,通知服務端銷燬會話
dwr.engine.setNotifyServerOnPageUnload( true);
//這個函數是提供給後臺推送的時候 調用的
function show(content){
$( "#content" ).text(content);
}
</script>
</body >
</html>
1.ScriptSession使用中存在的問題
在上一節實現了服務器的推送功能,但是根據 ScriptSession的生命週期我們可以得出以下幾點的問題:
(1)ScriptSession不會與HttpSession同時創建
當我們訪問一個頁面的時候,如果是第一次訪問,就會創建一個新的HttpSession,之後再訪問的時候,就會保持當前的Session,即使是刷新,也能保持當前的HttpSession。
但是,ScriptSession不同,第一次訪問,會創建一個ScriptSession,但是,如果你刷新,就會創建一個新的ScriptSession.
(2)如何得到ScriptSession
在DWR中,我們可以通過WebContextFactory.get()來取得一個WebContext對象,進而通過WebContext的getScriptSession()取得ScriptSession對象。
但是要注意,在我們自定義的Servlet中,我們也可以通過WebContextFactory.get()來取得一個WebContext,但是這種方法卻不能取得ScriptSession對象。因爲,此WebContext對象其實不是通過DWR的上下文環境得到的,所以,就根本沒有創建ScriptSession對象。
假設這種方式也能得到ScriptSession的話,那麼我們實現“推”也就可以不侷限在DWR的上下文環境中了,那麼其靈活性就會大很多了。所以,這就是我們不能在Servlet中實現推的原因。
(3) 關於刷新就創建一個新的ScriptSession問題
在我們需要推送的頁面中,如果你刷新一下,那麼就提交一個Http的request,此時,如果是第一次,那麼就會創建一個httpSession對象,同時,請求由DwrServlet來處理後,就會創建一個ScriptSession.這個ScriptSession會和你的request請求的URI綁定放在一個由ScriptSessionManager維護的Map裏面(這裏面其實是一個URI對應的Set,在Set裏面放置的是URI綁定的所有ScriptSession)。
當你刷新的時候,同樣的一個HttpSession,卻會創建一個新的ScriptSession,然後綁定到對應的URI上。
(4)向所有的頁面訪問者推送
當我們想向所有的頁面訪問者推送的時候,我們只需要,取得所有的頁面訪問者,就可以“推”了。
如何取得所有的頁面訪問者?
DWR3.0可以通過
//得到所有ScriptSession
Collection sessions = Browser.getTargetSessions();
DWR2.x可以通過
Collection pages = webContext.getScriptSessionsByPage(“/yourPage.jsp”);
通過此方法,就可以實現調用客戶端的javascript函數,實現對客戶端的操作。
(5) 在上面的推送中產生的問題
上面的方法已經可以實現向所有的訪問者推送。但是問題是,在客戶端,如果用戶刷新一次或多次,那麼,Collection裏面可能就保存了很多的無用的ScriptSession,所以不僅僅會影響性能問題,更重要的是,可能就不能實現你想要的功能。
2.如何管理有效的ScriptSession
由於上面的問題,我們就需要自己管理ScriptSession。其實,有效地HttpSession,就是那個和當前的HttpSession匹配的ScriptSession。
所以,我們就可以自己維護一個Map,在這個Map裏面,我們定義key就是HttpSession的Id,其值就是ScriptSession對象。
在每一次頁面載入的時候,都去註冊此ScriptSession,那麼就會把新的ScriptSession綁定到httpSession上面了。
在DWR3.0中推出了 ScriptSessionListener用來監聽ScriptSession的創建及銷燬事件。我們可以使用該監聽器來維護我們自己的Map。
在上一節的代碼上修改Demo:
1.新建一個類實現 ScriptSessionListener接口
package sugar.dwr;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.directwebremoting.ScriptSession;
import org.directwebremoting.WebContext;
import org.directwebremoting.WebContextFactory;
import org.directwebremoting.event.ScriptSessionEvent;
import org.directwebremoting.event.ScriptSessionListener;
public class DWRScriptSessionListener implements ScriptSessionListener {
//維護一個Map key爲session的Id, value爲ScriptSession對象
public static final Map<String, ScriptSession> scriptSessionMap = new HashMap<String, ScriptSession>();
/**
* ScriptSession創建事件
*/
public void sessionCreated(ScriptSessionEvent event) {
WebContext webContext = WebContextFactory. get();
HttpSession session = webContext.getSession();
ScriptSession scriptSession = event.getSession();
scriptSessionMap.put(session.getId(), scriptSession); //添加scriptSession
System. out.println( "session: " + session.getId() + " scriptSession: " + scriptSession.getId() + "is created!");
}
/**
* ScriptSession銷燬事件
*/
public void sessionDestroyed(ScriptSessionEvent event) {
WebContext webContext = WebContextFactory. get();
HttpSession session = webContext.getSession();
ScriptSession scriptSession = scriptSessionMap.remove(session.getId()); //移除scriptSession
System. out.println( "session: " + session.getId() + " scriptSession: " + scriptSession.getId() + "is destroyed!");
}
/**
* 獲取所有ScriptSession
*/
public static Collection<ScriptSession> getScriptSessions(){
return scriptSessionMap.values();
}
}
2.新建一個類繼承 DefaultScriptSessionManager,用來綁定 DWRScriptSessionListener
package sugar.dwr;
import org.directwebremoting.impl.DefaultScriptSessionManager;
public class DWRScriptSessionManager extends DefaultScriptSessionManager {
public DWRScriptSessionManager(){
//綁定一個ScriptSession增加銷燬事件的監聽器
this.addScriptSessionListener( new DWRScriptSessionListener());
System. out.println( "bind DWRScriptSessionListener");
}
}
3.在web.xml中將 DWRScriptSessionManager 配置在 dwr-invoker servlet中
<init-param>
<param-name >org.directwebremoting.extend.ScriptSessionManager </param-name>
<param-value >sugar.dwr.DWRScriptSessionManager </param-value>
</init-param>
4.通過以下方法獲取所有的 ScriptSession
//得到所有ScriptSession
Collection<ScriptSession> sessions = DWRScriptSessionListener.getScriptSessions();
3.使用 ScriptSessionFilter過濾
如果我們不想要給所有的客戶端 推送消息,只想給特定的客戶端推送,那麼我們可以使用 ScriptSessionFilter來實現。在filter中去判定session中的Attribute值是不是我們給定的。
1.使用以下方法推送消息
//執行推送
Browser.withAllSessionsFiltered(filter, run); //注意這裏調用了有filter功能的方法
2.通過參數可知我們需要一個 ScriptSessionFilter對象,此時send的方法改寫成如下:
public void send(final String content){
//過濾器
ScriptSessionFilter filter = new ScriptSessionFilter() {
public boolean match(ScriptSession scriptSession) {
String tag = (String)scriptSession.getAttribute("tag" );
System. out.println(tag);
return "receiverTag" .equals(tag);
}
};
Runnable run = new Runnable(){
private ScriptBuffer script = new ScriptBuffer();
public void run() {
//設置要調用的 js及參數
script.appendCall( "show", content);
//得到所有ScriptSession
Collection<ScriptSession> sessions = DWRScriptSessionListener.getScriptSessions();
//遍歷每一個ScriptSession
for (ScriptSession scriptSession : sessions){
scriptSession.addScript( script);
}
}
};
//執行推送
Browser. withAllSessionsFiltered(filter, run); //注意這裏調用了有filter功能的方法
3.在打開jsp頁面時需要在 ScriptSession 中注入設定的attribute,MessagePush中的方法
public void onPageLoad(final String tag){
//獲取當前的ScriptSession
ScriptSession scriptSession = WebContextFactory.get().getScriptSession();
scriptSession.setAttribute( "tag", tag);
System. out.println( "setAttribute");
}
4.在jsp中調用該方法
<script type= "text/javascript">
//這個方法用來啓動該頁面的ReverseAjax功能
dwr.engine.setActiveReverseAjax( true);
//設置在頁面關閉時,通知服務端銷燬會話
dwr.engine.setNotifyServerOnPageUnload( true);
var tag = "receiverTag"; //自定義一個標籤
messagePush.onPageLoad(tag);
//這個函數是提供給後臺推送的時候 調用的
function show(content){
$( "#content").text(content);
}
</script >
這樣我們可以給不同客戶端的jsp中導入不同的tag值,過濾推送的客戶端