關(guān)鍵字: Jetty, Cometd, Continuation, dojo
前言:
為了更容易的掌握Jetty cometd的使用方法,本筆記通過講解Jetty 6.0自帶的chat演示程序來輔助講解Jetty Cometd的實現(xiàn)。
環(huán)境配置方法:
服務(wù)器端:
類庫清單:WEB-INF/lib
jetty-6.1.9.jar
jetty-util-6.1.9.jar
servlet-api-2.5-6.1.9.jar
(以上Jetty服務(wù)器自帶)
cometd-api-0.9.20080221.jar
cometd-bayeux-6.1.9.jar
web.xml配置:
<servlet>
<servlet-name>cometd</servlet-name>
<servlet-class>org.mortbay.cometd.continuation.ContinuationCometdServlet</servlet-class>
<!-- 對隊列的內(nèi)容進行過濾 -->
<init-param>
<param-name>filters</param-name>
<param-value>/WEB-INF/filters.json</param-value>
</init-param>
<!-- 超時設(shè)置The server side poll timeout in milliseconds (default 250000). This is how long the server will
hold a reconnect request before responding. -->
<init-param>
<param-name>timeout</param-name>
<param-value>120000</param-value>
</init-param>
<!-- The client side poll timeout in milliseconds (default 0). How long a client will wait between
reconnects -->
<init-param>
<param-name>interval</param-name>
<param-value>0</param-value>
</init-param>
<!-- the client side poll timeout if multiple connections are detected from the same browser
(default 1500). -->
<init-param>
<param-name>multiFrameInterval</param-name>
<param-value>1500</param-value>
</init-param>
<!-- 0=none, 1=info, 2=debug -->
<init-param>
<param-name>logLevel</param-name>
<param-value>0</param-value>
</init-param>
<!-- If "true" then the server will accept JSON wrapped in a comment and will generate JSON wrapped
in a comment. This is a defence against Ajax Hijacking. -->
<init-param>
<param-name>JSONCommented</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>alwaysResumePoll</param-name>
<param-value>false</param-value> <!-- use true for x-site cometd -->
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>cometd</servlet-name>
<url-pattern>/cometd/*</url-pattern>
</servlet-mapping>
filters.json內(nèi)容如下:
格式如下:
{
"channels": "/**", --要過濾的隊列(支持通配符)
"filter":"org.mortbay.cometd.filter.NoMarkupFilter", --使用的過濾器,實現(xiàn)接口dojox.cometd.DataFilter
"init" : {} --初始化的值,調(diào)用 DataFilter.init方法傳入
}
示例內(nèi)容如下:
{
"channels": "/**",
"filter" : "org.mortbay.cometd.filter.NoMarkupFilter",
"init" : {}
},
{
"channels": "/chat/*",
"filter" : "org.mortbay.cometd.filter.RegexFilter",
"init" : [
[ "[fF].ck","dang" ],
[ "teh ","the "]
]
},
{
"channels": "/chat/**",
"filter" : "org.mortbay.cometd.filter.RegexFilter",
"init" : [
[ "[Mm]icrosoft", "Micro\\$oft" ],
[ ".*tomcat.*", null ]
]
}
]
這時,服務(wù)器端的配置就已經(jīng)完成的,基本的cometd功能就可以使用了。
客戶端通過dojox.cometd.init("http://127.0.0.2:8080/cometd");就可以進行連接。
代碼開發(fā):
接下來,我們要準(zhǔn)備客戶端(使用dojo來實現(xiàn))
一共三個文件
index.html
chat.js
chat.css(不是必須)
下面來看一下這兩個文件的內(nèi)容(加入注釋)
index.html
<head>
<title>Cometd chat</title>
<script type="text/javascript" src="../dojo/dojo/dojo.js"></script><!-- dojo類庫 -->
<script type="text/javascript" src="../dojo/dojox/cometd.js.uncompressed.js"></script><!-- dojo-cometd類庫 -->
<script type="text/javascript" src="chat.js"></script><!-- chat js文件,控制cometd的連接,消息的發(fā)送與接收 -->
<link rel="stylesheet" type="text/css" href="chat.css">
</head>
<body>
<h1>Cometd Chat</h1>
<div id="chatroom">
<div id="chat"></div>
<div id="input">
<div id="join" ><!-- 未登錄時,顯示的登錄名和登錄按鈕 -->
Username: <input id="username" type="text"/>
<input id="joinB" class="button" type="submit" name="join" value="Join"/>
</div>
<div id="joined" class="hidden"><!-- 登錄后,顯示的消息框和發(fā)送,退出按鈕(默認為隱藏) -->
Chat: <input id="phrase" type="text"></input>
<input id="sendB" class="button" type="submit" name="join" value="Send"/>
<input id="leaveB" class="button" type="submit" name="join" value="Leave"/>
</div>
</div>
</div>
</body>
chat.js文件
2 dojo.require("dojox.cometd");
3 dojo.require("dojox.cometd.timestamp");
4
5 //定義一個room類
6 var room = {
7 //定義屬性
8 _last: "", //最后發(fā)送消息的人員(如果不是本人,則顯示為空)
9 _username: null, //當(dāng)前的用戶名
10 _connected: true, //當(dāng)前的連接狀態(tài) true已經(jīng)連接, false表示未連接
11 groupName: "whimsical", //組名(未知)
12
13 //登錄操作
14 join: function(name){
15
16 if(name == null || name.length==0 ){
17 alert('Please enter a username!');
18 }else{
19
20 dojox.cometd.init(
new String(document.location).replace(/http:\/\/[^\/]*/,'').replace(/\/examples\/.*$/,'')+"/cometd");
21 // dojox.cometd.init("http://127.0.0.2:8080/cometd");
22 this._connected = true;
23
24 this._username = name;
25 dojo.byId('join').className='hidden';
26 dojo.byId('joined').className='';
27 dojo.byId('phrase').focus();
28
29 // subscribe and join
30 dojox.cometd.startBatch();
31 dojox.cometd.subscribe("/chat/demo", room, "_chat", { groupName: this.groupName});
32 dojox.cometd.publish("/chat/demo", {
33 user: room._username,
34 join: true,
35 chat : room._username+" has joined"
36 }, { groupName: this.groupName });
37 dojox.cometd.endBatch();
38
39 // handle cometd failures while in the room
40 room._meta = dojo.subscribe("/cometd/meta", this, function(event){
41 console.debug(event);
42 if(event.action == "handshake"){
43 room._chat({ data: {
44 join: true,
45 user:"SERVER",
46 chat:"reinitialized"
47 } });
48 dojox.cometd.subscribe("/chat/demo", room, "_chat", { groupName: this.groupName });
49 }else if(event.action == "connect"){
50 if(event.successful && !this._connected){
51 room._chat({ data: {
52 leave: true,
53 user: "SERVER",
54 chat: "reconnected!"
55 } });
56 }
57 if(!event.successful && this._connected){
58 room._chat({ data: {
59 leave: true,
60 user: "SERVER",
61 chat: "disconnected!"
62 } });
63 }
64 this._connected = event.successful;
65 }
66 }, {groupName: this.groupName });
67 }
68 },
69
70 //離開操作
71 leave: function(){
72 if(!room._username){
73 return;
74 }
75
76 if(room._meta){
77 dojo.unsubscribe(room._meta, null, null, { groupName: this.groupName });
78 }
79 room._meta=null;
80
81 dojox.cometd.startBatch();
82 dojox.cometd.unsubscribe("/chat/demo", room, "_chat", { groupName: this.groupName });
83 dojox.cometd.publish("/chat/demo", {
84 user: room._username,
85 leave: true,
86 chat : room._username+" has left"
87 }, { groupName: this.groupName });
88 dojox.cometd.endBatch();
89
90 // switch the input form
91 dojo.byId('join').className='';
92 dojo.byId('joined').className='hidden';
93 dojo.byId('username').focus();
94 room._username = null;
95 dojox.cometd.disconnect();
96 },
97
98 //發(fā)送消息
99 chat: function(text){
100 if(!text || !text.length){
101 return false;
102 }
103 dojox.cometd.publish("/chat/demo", { user: room._username, chat: text}, { groupName: this.groupName });
104 },
105
106 //從服務(wù)器收到消息后,回調(diào)的方法
107 _chat: function(message){
108 var chat=dojo.byId('chat');
109 if(!message.data){
110 console.debug("bad message format "+message);
111 return;
112 }
113 var from=message.data.user;
114 var special=message.data.join || message.data.leave;
115 var text=message.data.chat;
116 if(!text){ return; }
117
118 if( !special && from == room._last ){
119 from="

120 }else{
121 room._last=from;
122 from+=":";
123 }
124
125 if(special){
126 chat.innerHTML += "<span class=\"alert\"><span class=\"from\">"+from+"
</span><span class=\"text\">"+text+"</span></span><br/>";
127 room._last="";
128 }else{
129 chat.innerHTML += "<span class=\"from\">"+from+" </span><span class=\"text\">"+text+"</span><br/>";
130 }
131 chat.scrollTop = chat.scrollHeight - chat.clientHeight;
132 },
133
134 //初始操作
135 _init: function(){
136 dojo.byId('join').className='';
137 dojo.byId('joined').className='hidden';
138 dojo.byId('username').focus();
139
140 var element=dojo.byId('username');
141 element.setAttribute("autocomplete","OFF");
142 dojo.connect(element, "onkeyup", function(e){ //支持回車,登錄
143 if(e.keyCode == dojo.keys.ENTER){
144 room.join(dojo.byId('username').value);
145 return false;
146 }
147 return true;
148 });
149
150 dojo.connect(dojo.byId('joinB'), "onclick", function(e){ //綁定 room.join方法到 Join按扭
151 room.join(dojo.byId('username').value);
152 e.preventDefault();
153 });
154
155 element=dojo.byId('phrase');//取得消息框
156 element.setAttribute("autocomplete","OFF");
157 dojo.connect(element, "onkeyup", function(e){ //支持回車發(fā)送消息
158 if(e.keyCode == dojo.keys.ENTER){
159 room.chat(dojo.byId('phrase').value);
160 dojo.byId('phrase').value='';
161 e.preventDefault();
162 }
163 });
164
165 dojo.connect(dojo.byId('sendB'), "onclick", function(e){ //綁定 room.chat方法到 sendB按扭
166 room.chat(dojo.byId('phrase').value);
167 dojo.byId('phrase').value='';
168 });
169 dojo.connect(dojo.byId('leaveB'), "onclick", room, "leave"); //綁定 room.leave方法到 leaveB按扭
170 }
171 };
172
173 //頁面裝載時,調(diào)用room._init方法
174 dojo.addOnLoad(room, "_init");
175 //頁面關(guān)閉時,調(diào)用 room.leave方法
176 dojo.addOnUnload(room,"leave");
177
178 //vim:ts=4:noet:
補充:服務(wù)器端如何監(jiān)控消息隊列,以及進行訂閱,發(fā)送消息操作
要進行 監(jiān)控消息隊列,以及進行訂閱,發(fā)送消息操作的關(guān)鍵就是取得 Bayeux接口實現(xiàn)類 的實例
可以通過 ServletContextAttributeListener 這個監(jiān)聽器接口,通過attributeAdded方式加入
實現(xiàn)方法如下:
2 {
3 public void initialize(Bayeux bayeux)
4 {
5 synchronized(bayeux)
6 {
7 if (!bayeux.hasChannel("/service/echo"))
8 {
9 //取得 bayeux實例
10 }
11 }
12 }
13
14 public void attributeAdded(ServletContextAttributeEvent scab)
15 {
16 if (scab.getName().equals(Bayeux.DOJOX_COMETD_BAYEUX))
17 {
18 Bayeux bayeux=(Bayeux) scab.getValue();
19 initialize(bayeux);
20 }
21 }
22
23 public void attributeRemoved(ServletContextAttributeEvent scab)
24 {
25
26 }
27
28 public void attributeReplaced(ServletContextAttributeEvent scab)
29 {
30
31 }
32 }
取到 Bayeux實例后,就可以借助BayeuxService類幫我們實現(xiàn)消息隊列的監(jiān)聽,訂閱消息以及發(fā)送消息
2 {
3 synchronized(bayeux)
4 {
5 if (!bayeux.hasChannel("/service/echo"))
6 {
7 //取得 bayeux實例
8 new ChatService(bayeux);
9 }
10 }
11 }
具體方法請看下面這段代碼:
2 public static class ChatService extends BayeuxService {
3
4 ConcurrentMap<String,Set<String>> _members = new ConcurrentHashMap<String,Set<String>>();
5
6 public ChatService(Bayeux bayeux)
7 {
8 super(bayeux, "chat");//必須,把 Bayeux傳入到 BayeuxService對象中
9 subscribe("/chat/**", "trackMembers"); //訂閱隊列,收到消息后,會回調(diào)trackMembers方法
10 /*
11 subscribe支持回調(diào)的方法如下:
12 # myMethod(Client fromClient, Object data)
13 # myMethod(Client fromClient, Object data, String id)
14 # myMethod(Client fromClient, String channel, Object data,String id)
15 # myMethod(Client fromClient, Message message)
16
17 參數(shù):
18 Client fromClient 發(fā)送消息的客戶端
19 Object data 消息內(nèi)容
20 id The id of the message
21 channel 隊列名稱
22 Message message 消息對象。繼承于Map
23
24 */
25 }
26
27 //發(fā)布消息到隊列
28 public void sendMessage(String message) {
29 Map<String,Object> mydata = new HashMap<String, Object>();
30 mydata.put("chat", message);
31
32 Client sender = getBayeux().newClient("server");
33
34 getBayeux().getChannel("/chat/demo", false).publish(sender, mydata, "0"/*null*/);
35
36 }
37
38 //發(fā)送消息給指定的client(非廣播方式)
39 public void sendMessageToClient(Client joiner, String message) {
40 Map<String,Object> mydata = new HashMap<String, Object>();
41 mydata.put("chat", message);
42
43 send(joiner, "/chat/demo", mydata, "0"/*null*/);
44 }
45
46 //訂閱消息回調(diào)方法
47 public void trackMembers(Client joiner, String channel, Map<String,Object> data, String id)
48 {
49 //解釋消息內(nèi)容,如果消息內(nèi)容中 有 join這個字段且值為true
50 if (Boolean.TRUE.equals(data.get("join")))
51 {
52 //根據(jù)隊列,取得當(dāng)前登錄的人員
53 Set<String> m = _members.get(channel);
54 if (m==null)
55 {
56 //如果為空,則創(chuàng)建一個新的Set實現(xiàn)
57 Set<String> new_list=new CopyOnWriteArraySet<String>();
58 m=_members.putIfAbsent(channel,new_list);
59 if (m==null)
60 m=new_list;
61 }
62
63 final Set<String> members=m;
64 final String username=(String)data.get("user");
65
66 members.add(username);
67 //為該client增加事件,Remove事件。當(dāng)用戶退出時,觸發(fā)該方法。
68 joiner.addListener(new RemoveListener(){
69 public void removed(String clientId, boolean timeout)
70 {
71 members.remove(username);
72 }
73 });
74
75 //為該client增加事件,消息的發(fā)送和接收事件。當(dāng)用戶退出時,觸發(fā)該方法。
76 joiner.addListener(new MessageListener() {
77 public void deliver(Client fromClient, Client toClient, Message message) {
78 System.out.println("message from " + fromClient.getId() + " to "
79 + toClient.getId() + " message is " + message.getData());
80 }
81 });
82
83 Map<String,Object> mydata = new HashMap<String, Object>();
84 mydata.put("chat", "members=" + members);
85 //把已經(jīng)登錄的人員信息列表,發(fā)送回給消息發(fā)送者
86 send(joiner,channel,mydata,id);
87
88 }
89 }
90 }
91
附:部分使用到的API文檔
dojox.cometd.subscribe
dojox.cometd.subscribe()
handles all the hard work of telling
the server that we want to be notified when events are
published on a particular topic. subscribe
accepts a function
to handle messages and returns a dojo.Deferred
object which
has an extra property added to it which makes it suitable for
passing to dojox.cometd.unsubscribe()
as a “subscription
handle” (much like the handle object that dojo.connect()
produces and which dojo.disconnect()
expects).
Note that of a subscription is registered before a connection
with the server is established, events sent before the
connection is established will not be delivered to this client.
The deferred object which subscribe
returns will callback
when the server successfuly acknolwedges receipt of our
“subscribe” request.
Usage
parameter | type | description |
---|---|---|
channel | String | |
objOrFunc | Object | an object scope for funcName or the name or reference to a function to be called when messages are delivered to the |
funcName | String | the second half of the objOrFunc/funcName pair for identifying a callback function to notifiy upon channel message delivery |
props | Object | Optional. |
Examples
Example 1
Simple subscribe use-case
dojox.cometd.init("http://myserver.com:8080/cometd");
// log out all incoming messages on /foo/bar
dojox.cometd.subscribe("/foo/bar", console, "debug");
Example 2
Subscribe before connection is initialized
dojox.cometd.subscribe("/foo/bar", console, "debug");
dojox.cometd.init("http://myserver.com:8080/cometd");
Example 3
Subscribe an unsubscribe
dojox.cometd.init("http://myserver.com:8080/cometd");
var h = dojox.cometd.subscribe("/foo/bar", console, "debug");
dojox.cometd.unsubscribe(h);
Example 4
Listen for successful subscription:
dojox.cometd.init("http://myserver.com:8080/cometd");
var h = dojox.cometd.subscribe("/foo/bar", console, "debug");
h.addCallback(function(){
console.debug("subscription to /foo/bar established");
});
dojox.cometd.publish
parameter | type | description |
---|---|---|
channel | String | the destination channel for the message |
data | Object | a JSON object containing the message "payload" properties: Optional. Other meta-data to be mixed into the top-level of the message |
props | Object | Optional. |
Good Luck!
Yours Matthew!