用dwr的comet(推)來實現簡單的無刷新多人聊天室,comet是長連接的一種。通常我們要實現無刷新,一般會使用到Ajax。Ajax 應用程序可以使用兩種基本的方法解決這一問題:一種方法是瀏覽器每隔若干秒時間向服務器發出輪詢以進行更新,另一種方法是服務器始終打開與瀏覽器的連接並在數據可用時發送給瀏覽器。第一種方法一般利用setTimeout或是setInterval定時請求,並返回最新數據,這無疑增加了服務器的負擔,浪費了大量的資源。而第二種方法也會浪費服務器資源,長期的建立連接;而相對第一種來說,第二種方式會更優於第一種方法;這裏有一個一對多和多對一的關係,而comet向多個客戶端推送數據就是一對多的關係。而具體使用哪種方式,要看你當前的需求而定,沒有絕對的。
爲什麼使用 Comet?
輪詢方法的主要缺點是:當擴展到更多客戶機時,將生成大量的通信量。每個客戶機必須定期訪問服務器以檢查更新,這爲服務器資源添加了更多負荷。最壞的一種情況是對不頻繁發生更新的應用程序使用輪詢,例如一種 Ajax 郵件 Inbox。在這種情況下,相當數量的客戶機輪詢是沒有必要的,服務器對這些輪詢的回答只會是 “沒有產生新數據”。雖然可以通過增加輪詢的時間間隔來減輕服務器負荷,但是這種方法會產生不良後果,即延遲客戶機對服務器事件的感知。當然,很多應用程序可以實現某種權衡,從而獲得可接受的輪詢方法。
儘管如此,吸引人們使用 Comet 策略的其中一個優點是其顯而易見的高效性。客戶機不會像使用輪詢方法那樣生成煩人的通信量,並且事件發生後可立即發佈給客戶機。但是保持長期連接處於打開狀態也會消耗服務器資源。當等待狀態的 servlet 持有一個持久性請求時,該 servlet 會獨佔一個線程。這將限制 Comet 對傳統 servlet 引擎的可伸縮性,因爲客戶機的數量會很快超過服務器棧能有效處理的線程數量。
如果本示例結合Jetty應用服務器效果會更好。
開發環境:
System:Windows
WebBrowser:IE6+、Firefox3+
JavaEE Server:tomcat5.0.2.8、tomcat6
IDE:eclipse、MyEclipse 8
開發依賴庫:
JavaEE5、Spring 3.0.5、dwr 3
Email:[email protected]
Blog:http://blog.csdn.net/IBM_hoojo
一、準備工作
1、 下載dwr的相關jar包
https://java.net/downloads/dwr/Development%20Builds/Build%20116/dwr.jar
程序中還需要spring的相關jar包
需要的jar包如下
2、 建立一個WebProject,名稱DWRComet
在web.xml中添加dwr、spring配置如下:
<-- 加載Spring容器配置 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<-- 設置Spring容器加載配置文件路徑 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.directwebremoting.servlet.DwrListener</listener-class>
</listener>
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
<-- dwr的comet控制 -->
<init-param>
<param-name>pollAndCometEnabled</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>
3、 在src目錄加入applicationContext-beans.xml配置,這個配置專門配置bean對象,用來配置需要注入的對象。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
</beans>
4、 在WEB-INF目錄添加dwr.xml文件,基本代碼如下
<?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>
</dwr>
以上的準備基本完畢,下面來完成無刷新聊天室代碼
二、聊天室相關業務實現
1、 聊天實體類Model
package com.hoo.entity;
import java.util.Date;
/**
* <b>function:</b>
* @author hoojo
* @createDate 2011-6-3 下午06:40:07
* @file Message.java
* @package com.hoo.entity
* @project DWRComet
* @blog http://blog.csdn.net/IBM_hoojo
* @email [email protected]
* @version 1.0
*/
public class Message {
private int id;
private String msg;
private Date time;
//getter、setter
}
2、 編寫聊天信息的事件
package com.hoo.chat;
import org.springframework.context.ApplicationEvent;
/**
* <b>function:</b>發送聊天信息事件
* @author hoojo
* @createDate 2011-6-7 上午11:24:21
* @file MessageEvent.java
* @package com.hoo.util
* @project DWRComet
* @blog http://blog.csdn.net/IBM_hoojo
* @email [email protected]
* @version 1.0
*/
public class ChatMessageEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
public ChatMessageEvent(Object source) {
super(source);
}
}
繼承ApplicationEvent,構造參數用於傳遞發送過來的消息。這個事件需要一個監聽器監聽,一旦觸發了這個事件,我們就可以向客戶端發送消息。
3、 發送消息服務類,用戶客戶端發送消息。dwr需要暴露這個類裏面的發送消息的方法
package com.hoo.chat;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import com.hoo.entity.Message;
/**
* <b>function:</b>客戶端發消息服務類業務
* @author hoojo
* @createDate 2011-6-7 下午02:12:47
* @file ChatService.java
* @package com.hoo.chat
* @project DWRComet
* @blog http://blog.csdn.net/IBM_hoojo
* @email [email protected]
* @version 1.0
*/
public class ChatService implements ApplicationContextAware {
private ApplicationContext ctx;
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.ctx = ctx;
}
/**
* <b>function:</b> 向服務器發送信息,服務器端監聽ChatMessageEvent事件,當有事件觸發就向所有客戶端發送信息
* @author hoojo
* @createDate 2011-6-8 下午12:37:24
* @param msg
*/
public void sendMessage(Message msg) {
//發佈事件
ctx.publishEvent(new ChatMessageEvent(msg));
}
}
上面的sendMessage需要瀏覽器客戶端調用此方法完成消息的發佈,傳遞一個Message對象,並且是觸發ChatMessageEvent事件。
4、 編寫監聽器監聽客戶端是否觸發ChatMessageEvent
package com.hoo.chat;
import java.util.Collection;
import java.util.Date;
import javax.servlet.ServletContext;
import org.directwebremoting.ScriptBuffer;
import org.directwebremoting.ScriptSession;
import org.directwebremoting.ServerContext;
import org.directwebremoting.ServerContextFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.web.context.ServletContextAware;
import com.hoo.entity.Message;
/**
* <b>function:</b>監聽客戶端事件,想客戶端推出消息
* @author hoojo
* @createDate 2011-6-7 上午11:33:08
* @file SendMessageClient.java
* @package com.hoo.util
* @project DWRComet
* @blog http://blog.csdn.net/IBM_hoojo
* @email [email protected]
* @version 1.0
*/
@SuppressWarnings("unchecked")
public class ChatMessageClient implements ApplicationListener, ServletContextAware {
private ServletContext ctx;
public void setServletContext(ServletContext ctx) {
this.ctx = ctx;
}
@SuppressWarnings("deprecation")
public void onApplicationEvent(ApplicationEvent event) {
//如果事件類型是ChatMessageEvent就執行下面操作
if (event instanceof ChatMessageEvent) {
Message msg = (Message) event.getSource();
ServerContext context = ServerContextFactory.get();
//獲得客戶端所有chat頁面script session連接數
Collection<ScriptSession> sessions = context.getScriptSessionsByPage(ctx.getContextPath() + "/chat.jsp");
for (ScriptSession session : sessions) {
ScriptBuffer sb = new ScriptBuffer();
Date time = msg.getTime();
String s = time.getYear() + "-" + (time.getMonth() + 1) + "-" + time.getDate() + " "
+ time.getHours() + ":" + time.getMinutes() + ":" + time.getSeconds();
//執行setMessage方法
sb.appendScript("showMessage({msg: '")
.appendScript(msg.getMsg())
.appendScript("', time: '")
.appendScript(s)
.appendScript("'})");
System.out.println(sb.toString());
//執行客戶端script session方法,相當於瀏覽器執行JavaScript代碼
//上面就會執行客戶端瀏覽器中的showMessage方法,並且傳遞一個對象過去
session.addScript(sb);
}
}
}
}
上面的代碼主要是監聽客戶端的事件,一旦客戶端有觸發ApplicationEvent事件或是其子類,就會執行onApplicationEvent方法。代碼中通過instanceof判斷對象實例,然後再執行。如果有觸發ChatMessageEvent事件,就獲取所有連接chat.jsp這個頁面的ScriptSession。然後像所有的ScriptSession中添加script。這樣被添加的ScriptSession就會在有連接chat.jsp的頁面中執行。
所以這就是客戶端爲什麼會執行服務器端的JavaScript代碼。但前提是需要在web.xml中添加dwrComet配置以及在chat頁面添加ajax反轉。
5、 下面開始在bean容器和dwr的配置中添加我們的配置
applicationContext-beans.xml配置
<bean id="chatService" class="com.hoo.chat.ChatService"/>
<bean id="chatMessageClient" class="com.hoo.chat.ChatMessageClient"/>
上面的chatService會在dwr配置中用到
dwr.xml配置
<allow>
<convert match="com.hoo.entity.Message" converter="bean">
<param name="include" value="msg,time" />
</convert>
<create creator="spring" javascript="ChatService">
<param name="beanName" value="chatService" />
</create>
</allow>
charService的sendMessage方法傳遞的是Message對象,所以要配置Message對象的convert配置。
上面的create的creator是spring,表示在spring容器中拿chatService對象。裏面的參數的beanName表示在spring容器中找name等於charService的bean對象。
6、 客戶端chat.jsp頁面代碼
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>Chat</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<script type="text/javascript" src="${pageContext.request.contextPath }/dwr/engine.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath }/dwr/util.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath }/dwr/interface/ChatService.js"></script>
<script type="text/javascript">
function send() {
var time = new Date();
var content = dwr.util.getValue("content");
var name = dwr.util.getValue("userName");
var info = encodeURI(encodeURI(name + " say:/n" + content));
var msg = {"msg": info, "time": time};
dwr.util.setValue("content", "");
if (!!content) {
ChatService.sendMessage(msg);
} else {
alert("發送的內容不能爲空!");
}
}
function showMessage(data) {
var message = decodeURI(decodeURI(data.msg));
var text = dwr.util.getValue("info");
if (!!text) {
dwr.util.setValue("info", text + "/n" + data.time + " " + message);
} else {
dwr.util.setValue("info", data.time + " " + message);
}
}
</script>
</head>
<body οnlοad="dwr.engine.setActiveReverseAjax(true);">
<textarea rows="20" cols="60" id="info" readonly="readonly"></textarea>
<hr/>
暱稱:<input type="text" id="userName"/><br/>
消息:<textarea rows="5" cols="30" id="content"></textarea>
<input type="button" value=" Send " οnclick="send()" style="height: 85px; width: 85px;"/>
</body>
</html>
首先,你需要導入dwr的engine.js文件,這個很重要,是dwr的引擎文件。其次你使用的那個類的方法,也需要在導入進來。一般是interface下的,並且在dwr.xml中配置過的create。
上面的js中調用的charService類中的sendMessage方法,所以在jsp頁面中導入的是ChatService.js。
在body的onload事件中,需要設置反轉Ajax,這個很重要。
showMessage是ChatMessageClient的onApplicationEvent方法中的appendScript中需要執行的方法。data參數也是在那裏傳遞過來的。
每當發送sendMessage方法後就會觸發ChatMessageEvent事件,然後監聽的地方就會執行onApplicationEvent方法,在這個方法中又會執行瀏覽器中的showMessage方法。