Ajax
简介
ActiveMQ Classic 支持 Ajax,它是一种异步 Javascript 和 Xml 机制,用于实时 Web 应用程序。这意味着您可以创建高度实时的 Web 应用程序,充分利用 ActiveMQ Classic 的发布/订阅特性。
Ajax 允许常规 DHTML 客户端(使用 JavaScript 和现代版本 5 或更高版本的 Web 浏览器)通过网络发送和接收消息。ActiveMQ Classic 中的 Ajax 支持建立在与 ActiveMQ Classic 的 REST 连接器相同的基础之上,该连接器允许任何支持 Web 的设备通过 JMS 发送或接收消息。
要查看 Ajax 的实际效果,请尝试 运行示例
Servlet
AMQ AjaxServlet 需要安装在您的 Web 应用程序中,以支持通过 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
amq 的 ajax 特性在客户端由 amq.js 脚本提供。从 ActiveMQ Classic 5.4 开始,此脚本利用三种不同的适配器之一来支持与服务器的 ajax 通信。当前支持 jQuery、Prototype 和 Dojo,ActiveMQ Classic 附带了所有三个库的最新版本。
<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 Classic 转换为二进制消息,并且您的 Web 客户端将看不到这些消息。从 ActiveMQ Classic 5.4.0 开始,您可以通过始终将 amq-msg-type
标头 设置为 text
来解决此问题,该标头可能被 Web 客户端使用。
选择器支持
默认情况下,ajax 客户端将接收其订阅的主题或队列上的所有消息。在 ActiveMQ Classic 5.4.1 中,amq.js 支持 JMS 选择器,因为它经常用于仅接收这些消息的子集。选择器通过可选的第 4 个参数提供给 amq.addListener
调用。
amq.addListener( myId, myDestination, myHandler.rcvMessage, { selector:"identifier='TEST'" } );
当以这种方式使用时,Javascript 客户端将仅接收包含设置为值 TEST
的 identifier
标头的消息。
在多个浏览器窗口中使用 AMQ Ajax
单个浏览器中的所有窗口或选项卡在 ActiveMQ Classic 服务器上共享相同的 JSESSIONID
。除非服务器能够区分来自多个窗口的侦听器,否则原本打算发送到一个窗口的消息反而会被发送到另一个窗口。实际上,这意味着 amq.js 只能在任何给定时间在一个浏览器窗口中处于活动状态。从 ActiveMQ Classic 5.4.2 开始,通过允许对 amq.init
的每次调用都指定唯一的 clientId
来解决此问题。这样,同一浏览器中的多个窗口可以愉快地共存。每个窗口都可以拥有代理上独立的消息订阅集,这些订阅集之间没有相互作用。
在此示例中,我们使用当前时间(加载网页时)作为唯一标识符。只要两个浏览器窗口不是在同一毫秒内打开的,这就会很有效,并且是 ActiveMQ Classic 附带的示例 chat.md 使用的方法。可以轻松设计其他方案来确保 clientId
的唯一性。请注意,此 clientId
只需在一个会话内唯一。(在同一毫秒内在不同浏览器中打开的浏览器窗口不会相互作用,因为它们位于不同的会话中。)
org.activemq.Amq.init({
uri: 'amq',
logging: true,
timeout: 45,
clientId:(new Date()).getTime().toString()
});
请注意,此 clientId
对于单个选项卡或窗口中的所有消息订阅来说是通用的,并且与作为 amq.addListener
调用中的第一个参数提供的 clientId
完全不同。
- 在
amq.init
中,clientId
用于区分共享相同JSESSIONID
的不同 Web 客户端。当单个浏览器中的所有窗口调用amq.init
时,它们需要唯一的clientId
。 - 在
amq.addListener
中,clientId
用于将消息订阅与回调函数关联起来,当收到该订阅的消息时,应调用该函数。这些clientId
值是每个网页内部的,不需要在多个窗口或选项卡中唯一。
工作原理
AjaxServlet 和 MessageListenerServlet
amq 的 ajax 特性在服务器端由 AjaxServlet 处理,该 Servlet 扩展了 MessageListenerServlet。此 Servlet 负责跟踪现有客户端(使用 HttpSesssion)并延迟创建客户端发送和接收消息所需的 AMQ 和 javax.jms 对象(例如,Destination、MessageConsumer、MessageAVailableListener)。此 Servlet 应映射到为 Ajax 客户端提供服务的 Web 应用程序上下文中的 /amq/*
(可以更改,但客户端 javascript amq.uri
字段需要更新以匹配。)
客户端发送消息
当从客户端发送消息时,它被编码为 POST 请求的内容,使用支持的连接适配器之一(jQuery、Prototype 或 Dojo)的 API 用于 XmlHttpRequest。如果 amq 对象可以在不添加额外延迟的情况下将多个 sendMessage 调用组合到单个 POST 中(请参阅下面的轮询),它可能会这样做。
当 MessageListenerServlet 接收 POST 时,消息会被解码为 application/x-www-form-urlencoded
参数,并包含其类型(在这种情况下为 send
,而不是 listen
或 unlisten
,请参见下文)和目标。如果目标频道或主题不存在,则会创建它。该消息将作为 TextMessage 发送到目标。
侦听消息
当客户端注册侦听器时,消息订阅请求会以与消息相同的方式从客户端以 POST 的方式发送到服务器,但类型为 listen
。当 MessageListenerServlet 接收 listen
消息时,它会延迟创建一个 MessageAvailableConsumer 并为它注册一个侦听器。
等待消息轮询
当由 MessageListenerServlet 创建的侦听器被调用以指示消息可用时,由于 HTTP 客户端-服务器模型的限制,无法将该消息直接发送到 ajax 客户端。相反,客户端必须执行一种特殊的 轮询 来获取消息。轮询通常意味着定期发出请求以查看是否有可用消息,并且存在权衡:要么轮询频率很高,当系统空闲时会产生过多的负载;要么频率很低,检测新消息的延迟很高。
为了避免负载与延迟之间的权衡,AMQ 使用了等待轮询机制。amq.js 脚本加载后,客户端开始轮询服务器以获取可用消息。轮询请求可以作为 GET 请求发送,也可以作为 POST 请求发送(如果有其他消息要从客户端传递到服务器)。当 MessageListenerServlet 接收轮询请求时,它会
- 如果轮询请求是 POST,则处理所有
send
、listen
和unlisten
消息 - 如果没有消息可用于客户端在任何订阅的频道或主题上,则 Servlet 会暂停请求处理,直到
- 调用 MessageAvailableConsumer 侦听器以指示现在有消息可用;或者
- 超时到期(通常大约 30 秒,小于所有常见的 TCP/IP、代理和浏览器超时)。
- 将包含所有可用消息(封装为
text/xml
)的 HTTP 响应返回给客户端。
当 amq.js javascript 接收轮询的响应时,它会通过将它们传递给注册的处理函数来处理所有消息。处理完所有消息后,它会立即向服务器发送另一个轮询请求。
因此,amq ajax 特性的空闲状态是在服务器中“停放”的轮询请求,等待将消息发送到客户端。此“停放”的请求会定期由超时刷新,以防止任何 TCP/IP、代理或浏览器超时关闭连接。因此,服务器能够通过唤醒“停放”的请求并允许发送响应,以异步方式将消息发送到客户端。
客户端能够通过创建(或使用现有的)第二个连接到服务器来异步地将消息发送到服务器。但是,在处理轮询响应期间,正常的客户端消息发送会暂停,因此所有要发送的消息都会被排队并作为单个 POST 与轮询一起发送(不会延迟),并在处理结束时立即发送。这样可以确保客户端和服务器之间只需要两个连接(大多数浏览器的正常情况)。
无线程等待
上面描述的等待轮询是使用 Jetty 6 Continuations 机制实现的。这允许在等待期间释放与请求关联的线程,因此容器不需要为每个客户端都拥有一个线程(这可能是一个很大的数量)。如果使用其他 servlet 容器,则 Continuation 机制会回退到使用等待,并且线程不会被释放。
与 Pushlets 的比较
首先,我们可以轻松地为 ActiveMQ Classic 添加对 pushlets 的支持。但是,出于各种原因,我们更喜欢 Ajax 方式
- 使用 Ajax 意味着我们为每次发送/接收使用一个独立的 HTTP 请求,这比拥有一个无限长的 GET 更友善于 Web 基础设施(防火墙、代理、缓存等)。
- 我们仍然可以利用 HTTP 1.1 保持活动套接字和管道处理来获得用于客户端和服务器端之间通信的单个套接字的效率;尽管以一种与任何支持 HTTP 的基础设施兼容的方式。
- 服务器是纯 REST,因此将与任何客户端一起工作(而不是绑定到 Pushlet 方式要求的页面上使用的自定义 JavaScript 函数调用)。因此,Pushlets 将服务器绑定到网页;使用 Ajax,我们可以拥有一个与任何页面都兼容的通用服务。
- 客户端可以控制轮询频率和超时。例如,它可以使用 20 秒超时 HTTP GET 来避免 Pushlets 在某些浏览器中的内存问题。或者使用零超时 GET 来轮询队列。
- 它更容易充分利用 HTTP 消息编码,而不是使用 JavaScript 函数调用作为传输协议。
- pushlets 假设服务器知道客户端使用了哪些函数,因为服务器基本上是通过套接字写入 JavaScript 函数调用 - 我们最好发送通用 XML 数据包(或字符串或任何消息格式)并让 JavaScript 客户端完全从服务器端解耦。
- Ajax 支持干净的 XML 支持,允许将完整的 XML 文档流式传输到客户端,以实现丰富的消息,这些消息可以通过标准 JavaScript DOM 支持轻松处理。