ActiveMQ之Ajax調用
前言
ActiveMQ支持Ajax,這是應用在實時web應用中的一種異步的Javascript和Xml機制。這意味著你可以利用ActiveMQ的“發布/訂閱”的天性,來創建高度實時的web應用。
Ajax允許一個常見的DHTML客戶端(使用JavaScript和一個第5版及更高版本的現代瀏覽器)通過互聯網收發信息。ActiveMQ對Ajax的支持建立在與REST的ActiveMQ連接器相同的基礎上,該連接器允許任意可聯網的設備通過JMS收發消息。
如果想看一下Ajax是怎么用的,跑一下 官方的例子 就行了。
Servlet
首先要在Web應用中安裝AMQ的AjaxServlet,以此來支持基于Ajax的JMS:
... <servlet> <servlet-name>AjaxServlet</servlet-name> <servlet-class>org.apache.activemq.web.AjaxServlet</servlet-class> </servlet> ... <servlet-mapping> <servlet-name>AjaxServlet</servlet-name> <url-pattern>/amq/*</url-pattern> </servlet-mapping>
這個servlet既可以為需要的js文件提供服務,又可以處理JMS的請求和響應。
Javascript API
ActiveMQ的ajax特性是由客戶端的 amq.js腳本提供的。從ActiveMQ 5.4開始,該腳本利用三個不同的適配器中的一個來支持與服務器的通信。流行的jQuery、Prototype以及Dojo都是被支持的,并且這三個庫的比較新的版本都隨ActiveMQ發布了。
<script type="text/javascript" src="js/jquery-1.4.2.min.js"></script> <script type="text/javascript" src="js/amq_jquery_adapter.js"></script> <script type="text/javascript" src="js/amq.js"></script> <script type="text/javascript"> var amq = org.activemq.Amq; amq.init({ uri: 'amq', logging: true, timeout: 20 }); </script>
包含這些腳本的結果就是創建了一個名叫amq的javascript對象,它提供了發送信息以及訂閱頻道和主題的API。
發送一條消息
要從javascript客戶端發送一條JMS消息,需要做的僅僅是調用這個方法:
amq.sendMessage(myDestination,myMessage);
這里的myDestination是目的地URL字符串地址(例如:"topic://MY.NAME" 或者 "channel://MY.NAME"),myMessage是任意格式化好的XML或者被編碼為XML內容的純文本。
接收消息
要接收消息,客戶端必須定義一個消息處理函數,并且將其注冊到amq對象中。例如:
var myHandler = { rcvMessage: function(message) { alert("received "+message); } }; amq.addListener(myId,myDestination,myHandler.rcvMessage);
這里的myId是一個字符串標識符,在之后調用amq.removeHandler(myId)函數的時候會用到。myDestination是目的地URL字符串地址(例如:"topic://MY.NAME" 或者 "channel://MY.NAME")。接收到消息的時候,回調函數myHandler.rcvMessage會把消息傳遞到你的處理代碼塊。
該“消息”其實是文本消息的正文,或者對象消息的字符串(toString())表示形式。
注意,默認情況下,通過Stomp發布的消息如果包含了content-length消息頭,則它將會被ActiveMQ轉換為二進制消息,并且會對web客戶端不可見。從ActiveMQ 5.4.0開始,你可以通過將amq-msg-type消息頭 設置設為“text”,以使消息可以被web客戶端消費。
對選擇器的支持
默認情況下,一個ajax客戶端會接收到其訂閱的主題或隊列上的所有消息。在ActiveMQ 5.4.1中amq.js支持了JSM選擇器,因為很多時候僅接收這些消息中的一個子集非常有用。選擇器被作為調用amq.addListener函數的第四個可選參數。
amq.addListener( myId, myDestination, myHandler.rcvMessage, { selector:"identifier='TEST'" } );
這樣用的時候,Javascript客戶端只會收到包含了被設置為“TEST”的“identifier”消息頭的消息。
在多瀏覽器窗口中使用AMQ Ajax
單一瀏覽器的所有窗口或者tab頁在ActiveMQ服務器中共享同一個JSESSIONID。除非服務器可以分辨多窗口的監聽器,否則發送給一個窗口的消息可能被傳遞到另一個窗口。實際上,這意味著amq.js在任意時刻只能在一個瀏覽器窗口保持活躍。從ActiveMQ 5.4.2開始,這個問題被解決了,解決方法是允許對amq.init的每一次調用指定一個唯一的clientId。這樣做之后,同一個瀏覽器的多個窗口就可以快樂地共存了。它們分別在代理上有單獨的消息訂閱者集合,并且不互相影響。
這個例子中,我們使用當前時間(web頁面被加載的時間)作為唯一標識。只要兩個瀏覽器窗口不是在同一毫秒打開的,這種方法就是有效的,并且包含在ActiveMQ發行版的例子 chat.html 中使用的就是這種方法。其他保證clientId唯一性的方法也會很容易設計出來。注意clientId只需要在一個session中唯一即可。(同一毫秒在不同瀏覽器中打開的窗口不會互相影響,因為他們屬于不同的session。)
org.activemq.Amq.init({ uri: 'amq', logging: true, timeout: 45, clientId:(new Date()).getTime().toString() });
注意在一個tab頁或者窗口中,clientId對所有的消息訂閱者是公用的,并且它和作為amq.addListener函數的第一個參數的clientId是完全不同的。
- 在amq.init中,clientId的作用是區分共享同樣的JSESSIONID的不同的web客戶端。在調用amq.init時,同一個瀏覽器的不同窗口需要一個唯一的clientId。
- 在amq.addListener中,clientId用于將一個消息訂閱與回調函數相關聯,當訂閱收到一條消息的時候會出發這個函數。這些clientId值是每一個web頁面內部的,不需要在多窗口或tab頁中保持唯一性。
它是如何工作的
AjaxServlet 與 MessageListenerServlet
在服務器端,amq的ajaxt特性是被繼承自MessageListenerServlet的AjaxServlet處理的。這個servlet負責追蹤既有的客戶端(使用一個HttpSession),并慢慢地構造客戶端收發消息需要的AMQ和javax.jms對象(例如:目的地、消息消費者、消息可用監聽器,即Destination, MessageConsumer和MessageAVailableListener)。這個servlet應當被映射到服務于Ajax客戶端的web應用上下文中的“/amq/*”路徑下(這是可以改變的,但客戶端javascript的amq.uri域需要做相應的修改。)
客戶端發送消息
客戶端發送一條消息的時候,它被編碼為POST請求的內容,使用的是所支持的幾個XmlHttpRequest連接適配器之一(jQuery、Prototype或Dojo)中的API。amq對象可能會將若干發送消息的請求合并到一個單獨的POST中,當然這樣做的前提是能做到不增加額外的延遲(看下面的“輪詢”部分)。
當接MessageListenerServlet 收到一個POST請求,消息會被作為application/x-www-form-urlencoded參數解碼,并帶有類型“type”(在此情況下是“send”,而不是下面所說的“lisen”或“unlisten”)和目的地。 如果一個目的地通道或者主題不存在,它將會被創建。消息將會作為一個文本消息(TextMessage)被發送到目的地。
監聽消息
當客戶端注冊了一個監聽器,消息訂閱請求在一次POST請求中從客戶端發送到服務器端,就像發送消息一樣,只不過其類型“type”為“listen”。當MessageListenerServlet接收到“listen”消息的時候,它就慢慢地創建一個MessageAvailableConsumer并為其注冊一個監聽器。
等待輪詢消息
當一個由MessageListenerServlet 創建的監聽器被調用,表明一條消息可用時,由于HTTP“客戶端-服務器”模式的限制,不可能直接把這條消息發送到ajax客戶端。相反客戶端必須對消息實施一種特殊類型的“輪詢”。輪詢通常意味著周期性的發送請求去查看時候有消息可用,這樣的話就有一個折中的考慮:如果輪詢的頻率比較高,當系統空閑的時候就會產生過多的負載;反之如果輪詢頻率低,探測新消息的延遲就會變高。
為解決負載和延遲的折中問題,AMQ使用一種等待輪詢機制。一旦amq.js腳本被加載,客戶端就開始從服務器輪詢可用消息。一個輪詢請求可以作為一個GET請求發送,如果有其他準備從客戶端發送到服務器端的消息,也可以作為一個POST請求發送。當MessageListenerServlet接收到一次輪詢的時候,它將會:
- 如果輪詢請求是POST,所有的“send”、“listen”、和“unlisten”消息都被處理。
- 如果所有被訂閱的通道或主題都沒有對客戶端可用的消息,該servlet將暫停請求的處理,直至:
- 一個MessageAvailableConsumer監聽器被調用,表明現在有一條消息可用;或者
- 超時過期(通常是大約30秒,比所有常見的TCP/IP、代理和瀏覽器超時都少)。
- 一個HTTP響應被返回到客戶端,包含被封裝成text/xml的所有可用消息。
當amq.js javascript接收到輪詢請求的響應時把所有消息傳遞到注冊的處理函數,以此來處理它們。一旦處理完了所有的消息,它立即向服務器發送另一次輪詢。
所以amq ajax特性的空閑狀態是服務器上的一次輪詢請求“空檔”,等待消息被發送到客戶端。這個“空擋”請求被一個超時設定周期性地刷新,防止任何 TCP/IP、代理或者瀏覽器的超時關閉連接。所以服務器能夠通過喚醒“空檔”請求并允許發送響應,來異步地向客戶端發送一條消息。
客戶端可以通過創建(或使用現有的)第二條連接,異步地向服務器發送一條消息。然而,在處理輪詢響應的過程中,正常的客戶端消息發送被暫停,因此所有要發送的消息進入隊列,在處理過程結束時連同將要發送的輪詢請求(無延遲),作為一個單獨的POST進行發送。這確保了客戶端和服務器之間只需要兩個連接(對大多數瀏覽器來說)。
無線程(threadless)等待
上面描述的等待輪詢是使用Jetty 6的Continuations機制實現的。這允許與請求關聯的線程在等待期間被釋放,所以容器就不需要為每個客戶端維護一個線程了(這可能是一個很大的數字)。如果使用了另一個servlet容器,Continuation機制退回到使用等待,并且線程不被釋放。
與“服務器推送”對比
首先我們可以很容易地為ActiveMQ增加服務器推送支持。然而由于多種原因,我們更喜歡Ajax方式:
- 使用Ajax意味著我們對每一次發送/接收使用一個清晰的HTTP請求,而不是維持一個無限長的GET,這樣對web基礎設施(防火墻,代理服務器,緩存等等)更加友好。
- 我們仍然可以利用HTTP 1.1的長連接套接字和管道處理,來達到將單個套接字用于客戶端和服務器之間通訊的功效;雖然是以一種對任意有HTTP功能的基礎設施都有效的方法。
- 服務器是純REST的,所以對任意客戶端有效(而不是被束縛到頁面上使用的自定義JavaScript函數調用,這正是服務器推送方法需要的)。所以“服務器推送”將服務器約束到了web頁面之上;而使用Ajax我們可以有一個對任意頁面都有效的通用服務。
- 客戶端可以通過輪詢和超時的頻率控制。例如,通過使用20秒超時的HTTP GET,可以避免“服務器推送”在某些瀏覽器中出現的內存問題。或者對輪詢隊列使用零超時GET。
- 更容易充分利用消息的HTTP編碼,而不是使用JavaScript函數調用作為傳輸協議。
- 服務器推送假定服務器知道客戶端在使用什么函數,因為服務器主要負責通過套接字下發JavaScript函數調用 —— 對我們來說更好的方法是發送通用的XML數據包(或者字符串或者其他任意格式的消息),并且讓JavaScript客戶端完全從服務器端解耦。
- Ajax提供完全的XML支持,允許富消息的完整XML文檔被傳送到客戶端,通過標準的JavaScript DOM支持可以很容易地處理這種消息。
Introduction
ActiveMQ supports Ajax which is an Asychronous Javascript And Xml mechanism for real time web applications. This means you can create highly real time web applications taking full advantage of the publish/subscribe nature of ActiveMQ
Ajax allows a regular DHTML client (with JavaScript and a modern version 5 or later web browser) to send and receive messages over the web. Ajax support in ActiveMQ builds on the same basis as the REST connector for ActiveMQ which allows any web capable device to send or receive messages over JMS.
To see Ajax in action, try running the examples
The Servlet
The AMQ AjaxServlet needs to be installed in your webapplications to support JMS over Ajax:
... <servlet> <servlet-name>AjaxServlet</servlet-name> <servlet-class>org.apache.activemq.web.AjaxServlet</servlet-class> </servlet> ... <servlet-mapping> <servlet-name>AjaxServlet</servlet-name> <url-pattern>/amq/*</url-pattern> </servlet-mapping>
The servlet both serves the required js files and handles the JMS requests and responses.
Javascript API
The ajax featues of amq are provided on the client side by the amq.js script. Beginning with ActiveMQ 5.4, this script utilizes one of three different adapters to support ajax communication with the server. Current jQuery, Prototype, and Dojo are supported, and recent versions of all three libraries are shipped with ActiveMQ.
<script type="text/javascript" src="js/jquery-1.4.2.min.js"></script> <script type="text/javascript" src="js/amq_jquery_adapter.js"></script> <script type="text/javascript" src="js/amq.js"></script> <script type="text/javascript"> var amq = org.activemq.Amq; amq.init({ uri: 'amq', logging: true, timeout: 20 }); </script>
Including these scripts results in the creation of a javascript object called amq, which provides the API to send messages and to subscribe to channels and topics.
Sending a message
All that is required to send a JMS message from the javascript client, is to call the method:
amq.sendMessage(myDestination,myMessage);
where myDestination is the URL string address of the destination (e.g. "topic://MY.NAME" or "channel://MY.NAME") and myMessage is any well formed XML or plain text encoded as XML content.
Receiving messages
To receive messages, the client must define a message handling function and register it with the amq object. For example:
var myHandler = { rcvMessage: function(message) { alert("received "+message); } }; amq.addListener(myId,myDestination,myHandler.rcvMessage);
where myId is a string identifier that can be used for a later call to amq.removeHandler(myId) and myDestination is a URL string address of the destination (e.g. "topic://MY.NAME" or "channel://MY.NAME"). When a message is received, a call back to the myHandler.rcvMessage function passes the message to your handling code.
The "message" is actually a text of the Text message or a String representation (toString()) in case of Object messages.
Be aware that, by default, messages published via Stomp which include a content-length header will be converted by ActiveMQ to binary messages, and will not be visible to your web clients. Beginning with ActiveMQ 5.4.0, you can resolve this problem by always setting the amq-msg-type header to text in messages which will may be consumed by web clients.
Selector support
By default, an ajax client will receive all messages on a topic or queue it is subscribed to. In ActiveMQ 5.4.1 amq.js supports JMS selectorssince it is frequently useful to receive only a subset of these messages. Selectors are supplied to an amq.addListener call by way of an optional 4th parameter.
amq.addListener( myId, myDestination, myHandler.rcvMessage, { selector:"identifier='TEST'" } );
When used in this way, the Javascript client will receive only messages containing an identifier header set to the value TEST.
Using AMQ Ajax in Multiple Browser Windows
All windows or tabs in a single browser share the same JSESSIONID on the ActiveMQ server. Unless the server can distinguish listeners from multiple windows, messages which were intended for 1 window will be delivered to another one instead. Effectively, this means that amq.js could be active in only a single browser window at any given time. Beginning in ActiveMQ 5.4.2, this is resolved by allowing each call to amq.initto specify a unique clientId. When this is done, multiple windows in the same browser can happily co-exist. Each can have a separate set of message subscriptions on the broker with no interactions between them.
In this example, we use the current time (at the time the web page is loaded) as a unique identifier. This is effective as long as two browser windows are not opened within the same millisecond, and is the approach used by the example chat.html included with ActiveMQ. Other schemes to ensure the uniqueness of clientId can easily be devised. Note that this clientId need only be unique within a single session. (Browser windows opened in the same millisecond in separate browsers will not interact, since they are in different sessions.)
org.activemq.Amq.init({ uri: 'amq', logging: true, timeout: 45, clientId:(new Date()).getTime().toString() });
Note that this clientId is common to all message subscriptions in a single tab or window, and is entirely different from the clientId which is supplied as a first argument in amq.addListener calls.
- In amq.init, clientId serves to distinguish different web clients sharing the same JSESSIONID. All windows in a single browser need a unique clientId when they call amq.init.
- In amq.addListener, clientId is used to associate a message subscription with the callback function which should be invoked when a message is received for that subscription. These clientId values are internal to each web page, and do not need to be unique across multiple windows or tabs.
How it works
AjaxServlet and MessageListenerServlet
The ajax featues of amq are handled on the server side by the AjaxServlet which extends the MessageListenerServlet. This servlet is responsible for tracking the existing clients (using a HttpSesssion) and lazily creating the AMQ and javax.jms objects required by the client to send and receive messages (eg. Destination, MessageConsumer, MessageAVailableListener). This servlet should be mapped to /amq/* in the web application context serving the Ajax client (this can be changed, but the client javascript amq.uri field needs to be updated to match.)
Client Sending messages
When a message is sent from the client it is encoded as the content of a POST request, using the API of one of the supported connection adapters (jQuery, Prototype, or Dojo) for XmlHttpRequest. The amq object may combine several sendMessage calls into a single POST if it can do so without adding additional delays (see polling below).
When the MessageListenerServlet receives a POST, the messages are decoded as application/x-www-form-urlencoded parameters with their type (in this case send as opposed to listen or unlisten see below) and destination. If a destination channel or topic do not exist, it is created. The message is sent to the destination as a TextMessage.
Listening for messages
When a client registers a listener, a message subscription request is sent from the client to the server in a POST in the same way as a message, but with a type of listen. When the MessageListenerServlet receives a listen message, it lazily creates a MessageAvailableConsumer and registers a Listener on it.
Waiting Poll for messages
When a Listener created by the MessageListenerServlet is called to indicate that a message is available, due to the limitations of the HTTP client-server model, it is not possible to send that message directly to the ajax client. Instead the client must perform a special type of Poll for messages. Polling normally means periodically making a request to see if there are messages available and there is a trade off: either the poll frequency is high and excessive load is generated when the system is idle; or the frequency is low and the latency for detecting new messages is high.
To avoid the load vs latency tradeoff, AMQ uses a waiting poll mechanism. As soon as the amq.js script is loaded, the client begins polling the server for available messages. A poll request can be sent as a GET request or as a POST if there are other messages ready to be delivered from the client to the server. When the MessageListenerServlet receives a poll it:
- if the poll request is a POST, all send, listen and unlisten messages are processed
- if there are no messages available for the client on any of the subscribed channels or topic, the servlet suspends the request handling until:
- A MessageAvailableConsumer Listener is called to indicate that a message is now available; or
- A timeout expires (normally around 30 seconds, which is less than all common TCP/IP, proxy and browser timeouts).
- A HTTP response is returned to the client containing all available messages encapsulated as text/xml.
When the amq.js javascipt receives the response to the poll, it processes all the messages by passing them to the registered handler functions. Once it has processed all the messages, it immediately sends another poll to the server.
Thus the idle state of the amq ajax feature is a poll request "parked" in the server, waiting for messages to be sent to the client. Periodically this "parked" request is refreshed by a timeout that prevents any TCP/IP, proxy or browser timeout closing the connection. The server is thus able to asynchronously send a message to the client by waking up the "parked" request and allowing the response to be sent.
The client is able to asynchronously send a message to the server by creating (or using an existing) second connection to the server. However, during the processing of the poll response, normal client message sending is suspended, so that all messages to be sent are queued and sent as a single POST with the poll that will be sent (with no delay) at the end of the processing. This ensures that only two connections are required between client and server (the normal for most browsers).
Threadless Waiting
The waiting poll described above is implemented using the Jetty 6 Continuations mechanism. This allows the thread associated with the request to be released during the wait, so that the container does not need to have a thread per client (which may be a large number). If another servlet container is used, the Continuation mechanism falls back to use a wait and the thread is not released.
Comparison to Pushlets
Firstly we could easily add support for pushlets to ActiveMQ. However we prefer the Ajax approach for various reasons
- using Ajax means that we use a distinct HTTP request for each send/receive which is much more friendly to web infrastructure (firewalls, proxies, caches and so forth) rather than having an infinitely-long GET.
- we can still take advantage of HTTP 1.1 keep-alive sockets and pipeline processing to gain the efficiency of a single socket used for communication between the client and server side; though in a way that works with any HTTP-capable infrastructure
- the server is pure REST and so will work with any client side (rather than being tied to custom JavaScript function calls used on the page which the Pushlet approach requires). So Pushlets tie the server to the web page; with Ajax we can have a generic service which works with any page.
- the client can be in control over frequency of polling & timeouts. e.g. it can avoid the memory issues of Pushlets in some browsers by using a 20-second timeout HTTP GET. Or using a zero timeout GET to poll queues.
- its easier to take full advantage of HTTP encoding of messages, rather than using JavaScript function calls as the transfer protocol.
- pushlets assume the server knows what functions are used on the client side as the server basically writes JavaScript function calls down the scoket - it's better for us to send generic XML packets (or strings or whatever the message format is) and let the JavaScript client side be totally decoupled from the server side
- Ajax supports clean XML support allowing full XML documents to be streamed to the client for rich messages which are easy to process via standard JavaScript DOM support
轉自:http://blog.csdn.net/neareast/article/details/7588527