1. 服务器端推送组件
CometD、Pushlet、DWR、APE(Ajax Push Engine)
2. 软件环境
使用Pushlet完成服务器端的消息推送,实现群组IM。客户端模式为PULL,数据交换格式为XML-STRICT
3. 其中的坑
3.1 中文消息坑
现象
中文消息丢失后,似乎整个系统都瘫痪了
跟踪调查
结合源代码跟踪调试发现,所有要发送的消息都基于Event对象,Event对象最终被转换为XML格式的字符串发送至客户端。而在toXML过程中,当遇到中文时程序进入异常处理过程,错误消息为“非ISO-8859-1字符”,该消息因为异常不再发送至客户端,消息丢失了。可恶的是,后台日志中并没有明显错误。
补救方法
将待发送的对象作为Event的data属性,将待发送对象转换为JSON格式的字符串,并转码。我在项目中使用的是UTF-8编码。
new String(JSONObject.fromObject(obj).toString().getBytes("UTF-8"),"ISO-8859-1");
虽然在控制台中输出是乱码的,实际测试发现,浏览器端UTF-8编码能够正确接收并显示中文。
3.2 配置文件
根据Pushlet.properties中的注释,个人修改了部分配置,结果消息丢失率很高。考虑到jar包不大,代码相对较少就读了一些关键类,如SessionManager,Session,Controller,Subscriber,Subscription等。总结一下结论如下:
A .SessionManager删除Session
当如下情况发送时
1. 当发送数据时,出现异常
2. 当Session的timetoLive<0。
3. 当Session的两次Refresh请求间隔,超过了配置的refresh.timeout
B. 关于Session是否过期的检查
通过如下手段实现
1. 每次客户端发送refresh请求,就会将Session的timeToLi ve重置为session.timeout.mins
2. 服务器端有1个AgeTask来定时检查Session的timeToLive,周期为1分钟。所以,配置文件中session.timeout的单位为分钟
3. 每次客户端发送refresh请求,等待时间主要受queue.read.timeout.millis影响。如果没有新消息到达,请求是在读取消息队列超时之后返回
C. 基于B的推理
1. pull.session.timeout 不能设置为1,最小值为2。因为设置为1意味着Session的timeToLive为1分钟,考虑到实际线程调度的问题,AgeTask的执行间隔可能会超过1分钟。当AgeTask对每个Session的TimeToLive减去执行间隔delta时,Session纷纷超时。于是,刚刚发送的群组消息,仅有少数几个群组用户可以正确接收。说没法送成功吧,有人接收到了消息。说发送成功吧,莫非程序也检查用户人品。
2. queue.read.timeout+ 必须远小于 pull.refresh.timeout。因为如果有消息到达,则请求会立刻返回,而后发送下一次请求。如果没有消息到达,则要一直等到timeout,结合A.1,如果下一次请求没有如期到达,则该Session将会被stop掉,进而被remove掉。
3. 此外在完成本次请求,开启下一次请求之前的间隔受pull.refresh.wait.min.millis和pull.refresh.wait.max.millis两个参数影响。通过读取代码发现,客户端会生成1个属于[min,max]的随机值作为请求等待时间,也会对refresh是否如期到达造成影响。本人使用中将两个值全部设置为0,实现下次请求的立刻执行。
从推理得出如下内容:
pull.session.timeout>=2
queue.read.timeout + random(pull.refresh.wait.min.millis,pull.refresh.wait.max.millis)+网络传输时间 < pull.refresh.timeout
同时,也证明,曾经发送成功的消息,确实是人品大爆发。按照推理修改了配置文件值如下:
listen.force.pull.all=true
pull.refresh.timeout.millis=10000
queue.read.timeout.millis=5000
queue.write.timeout.millis=300
pull.refresh.wait.min.millis=0
pull.refresh.wait.min.millis=0
3.3 与HttpSession关联
项目中有针对某个人发送消息的需求。所以,我们需要把PushletSession与HttpSession关联起来。这里要注意的是,我是直接关联的PushletSession对象本身。但SessionManager种经常会对Session进行克隆。结果就出现一种现象,关联的PushletSession已过期,但SessionManager中该Session依然active。其实,仅有Session的id是不变的,其对应的Session可能被clone多次,而与HttpSession关联的这一个,早已被SessionManager丢弃了。所以,仅使用与HttpSession关联的PushletSession的id,而不直接使用当前关联的PushletSession。
3.4 断线重连
客户端提供onError回调函数,我们可以在该函数中,进行断线重连。当然,如果是浏览器关闭,就没有必要了。
3.5 消息重发
当Session被stop时,将Session中Subscriber中的EventQueue暂时恢复到HttpSession中。当新的Session重建OK时,优先发送这些缓存的消息。
不过由于前后两个Session是没有关联的,在前1个Session stop之后,新的Session start之前,服务器端将无法向该用户推送消息。这个希望与诸位小伙伴讨论,欢迎拍砖!