最近项目中要用到pushlet,做一个基于长连接的报警提醒。
之前项目中也有类似的提醒,但是是用轮询解决的,5分钟请求一次,这次的需求提醒及时性要求比较高,通过提高轮询频率的方法显然不合适,网上基于长连接的解决方案也有不少,因为pushlet属于轻量,故选择它。
废话不多先上配置
后端jar包,页面引js
web.xml
<servlet>
<servlet-name>pushlet</servlet-name>
<servlet-class>nl.justobjects.pushlet.servlet.Pushlet</servlet-class>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>pushlet</servlet-name>
<url-pattern>/pushlet.srv</url-pattern>
</servlet-mapping>
消息推送方法
public static void multicast(String eventKey, String fieldKey, String jsonMsg) {
Event event = Event.createDataEvent(eventKey);
try {
event.setField(fieldKey, URLEncoder.encode(jsonMsg, "UTF-8"));
Dispatcher.getInstance().multicast(event);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
页面
PL._init();
PL.joinListen('2333');
function onData(event) {
result=JSON.parse(decodeURIComponent(event.get('6666')));
console.log(result);
}
2333对应后端方法里的eventKey,6666对应fieldKey,result输出的就是jsonMsg
如果页面是jsp的话不用调init方法,其他的话是要调的
js里面的init方法里会有这么一段
PL.pushletURL = PL._getWebRoot() + 'pushlet.srv';
_getWebRoot: function() {
/** Return directory of this relative to document URL. */
if (PL.webRoot != null) {
return PL.webRoot;
}
//derive the baseDir value by looking for the script tag that loaded this file
var head = document.getElementsByTagName('head')[0];
var nodes = head.childNodes;
for (var i = 0; i < nodes.length; ++i) {
var src = nodes.item(i).src;
if (src) {
var index = src.indexOf("ajax-pushlet-client.js");
if (index >= 0) {
index = src.indexOf("lib");
PL.webRoot = src.substring(0, index);
break;
}
}
}
return PL.webRoot;
}
所以最后的url会是一个很纠结的url(因为存放js的路径是千奇百怪的),我们要做的就是干脆在init方法里写死,对应web.xml里的路径,即/pushlet.srv
这样的话一个后端往页面推数据的模型就完成了,pushlet还能做到前端向后端推数据(基于长连接,为啥我不用ajax交互。。。)等,业务不涉及也没做深入了解
开发环境ok的,可到了线上就不行了,因为线上部署了不同的节点
举个例子,线上部署着1,2两个节点,用户a长连接着1,此时用户b,向节点2发送了一个请求,触发了pushlet广播消息,2向所有长连接的用户推送了消息,显然此时的用户a是收不到任何消息推送的,因为用户a长连接着节点1而不是节点1和节点2。
知道问题所在接下来的就好办了,首先去寻找pushlet的集群解决方案,很可惜,网上并没有现成的。
时间缘故并没有深入了解pushlet源码,利用jms来解决
大致思路讲一下,就是在原本触发pushlet消息推送的地方改成用activemq向所有应用节点发消息,消息内容包括pushlet所要发的内容,应用对应的消费者消费时再用pushlet进行消息推送。中间加了一层mq的话,虽然时效性有所下降,但还是能满足业务需求的。
配置的话就是普通的spring activemq配置。
要注意的是用广播模式(就是订阅模式),生产者跟消费者写在一起。
下面上配置跟代码
application配置,生产者跟消费者
<!-- 本级任务监听 -->
<bean id="pushletTaskConnectionFactory" class="org.apache.activemq.spring.ActiveMQConnectionFactory">
<property name="brokerURL" value="${pushletTaskMQAddress}" />
</bean>
<bean id="pushletTaskQueueDestination" class="org.apache.activemq.command.ActiveMQTopic">
<!-- 设置消息队列的名字 -->
<constructor-arg index="0" value="pushletTaskQueue" />
</bean>
<bean id="pushletTaskMessageConvert"
class="com.hahaha.PushletTaskMsgConventer" />
<bean id="pushletTaskQueueTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="pushletTaskConnectionFactory" />
<property name="defaultDestination" ref="pushletTaskQueueDestination" />
<property name="messageConverter" ref="pushletTaskMessageConvert" />
<property name="pubSubDomain" value="true" />
</bean>
<bean id="pushletTaskMessageListener"
class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="com.hahaha.PushletTaskMessageListener" />
</constructor-arg>
<property name="defaultListenerMethod" value="onMessage" />
<property name="messageConverter" ref="pushletTaskMessageConvert" />
</bean>
<bean id="pushletTaskQueueListenerContainer" lazy-init="false"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="pushletTaskConnectionFactory" />
<property name="destination" ref="pushletTaskQueueDestination" />
<property name="messageListener" ref="pushletTaskMessageListener" />
<property name="pubSubDomain" value="true"/>
</bean>
生产者代码的话就是发送个消息
消费者监听代码
public class PushletTaskMessageListener implements MessageListener {
private static final Logger logger = LoggerFactory.getLogger(PushletTaskMessageListener.class);
@Override
public void onMessage(Message message) {
MapMessage mapMsg = (MapMessage) message;
try {
String evenKey = mapMsg.getString("evenKey");
String fieldKey = mapMsg.getString("fieldKey");
String jsonMsg = mapMsg.getString("jsonMsg");
PushLetUtil.multicast(evenKey, fieldKey, jsonMsg);
} catch (JMSException e) {
logger.error("消息推送失败:" + mapMsg.toString(), e);
}
}
}
写完收工~