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