web实时通信应用解决方案:WebSocket模拟库-SockJS
SockJS简介:
SockJS是一个浏览器的JavaScript库,它提供了一个类似的WebSocket对象。 SockJS为您提供了一个连贯的,跨浏览器的JavaScript API创建一个低延迟,全双工,浏览器和Web服务器之间的跨域通信通道。
git项目网址:https://github.com/sockjs/sockjs-client
SockJS family:
- SockJS-client JavaScript client library
- SockJS-node Node.js server
- SockJS-erlang Erlang server
- SockJS-cyclone Python/Cyclone/Twisted server
- SockJS-tornado Python/Tornado server
- SockJS-twisted Python/Twisted server
- SockJS-aiohttp Python/Aiohttp server
- Spring Framework Java client & server
- vert.x Java/vert.x server
- Xitrum Scala server
- Atmosphere Framework JavaEE Server, Play Framework, Netty, Vert.x
Work in progress:
- SockJS-ruby
- SockJS-netty
- SockJS-gevent (SockJS-gevent fork)
- pyramid-SockJS
- wildcloud-websockets
- wai-SockJS
- SockJS-perl
- SockJS-go
SockJS特点:
客户端和服务器端api尽可能简洁,尽量靠近websocket api
支持服务端扩展和负载均衡技术
传输层应该全面支持跨域通信
如果受到代理服务器的限制,传输层能优雅地从一种方式回退到另一种方式
尽可能快地建立连接
客户端只是纯粹的JavaScript,不需要flash
客户端JavaScript必须经过严格的测试
服务器端代码尽可能简单,降低用另一种语言重写server的代价
实际上sockjs的目标也就是sockjs具有的特点。
在SockJS: WebSocket emulation done right一文中对sockjs的特点进行了具体阐述。
sockjs几个特点,非常值得一提
跨域通信
sockjs服务器端支持跨域通信,意味着我们可以将sockjs server独立出来,把它放在与web主站点不同的域名之上,实际上这是比较合理的部署策略。关于跨域,也是一个比较大的话题,其中有一个机制叫cors(跨域资源共享)主要解决JavaScript不能跨域请求的问题。sockjs服务器应该支持这种机制。
负载均衡
无论server端优化得再好,一个sockjs server的处理能力都是有限的,我们更需要的是一种可扩展的解决方案。一种非常简单的方法是把每一个sockjs server放到一个不同的域名之下,如sockjs1.example.com和sockjs2.example.com,允许客户端随机选择一个server。
Prefix-based sticky sessions
在sockjs中,一个典型的url如下:
http://localhost:8000/chat/<serverid>/<sessionid>/
url中的第二个参数sessionid,必须是一个随机字符串,唯一标识一个session。第一个参数serverid,主要应用于负载均衡目的。负载均衡器可以利用这个serverid参数作为一个线索,进行负载均衡分流。具体使用方面,参考HAProxy的一个配置参考文件,其中关键的配置在于
balance uri depth 2
JSESSIONID cookie sticky sessions
另外一种负载均衡配置方案,主要利用含有jsessionid的cookie。这个cookie由socketjs server进行设置,当response到达负载均衡器的时候,jessionid会被加上一个额外的前缀或者后缀,具体原理方面可以参考阅读
LOAD BALANCING, AFFINITY, PERSISTENCE, STICKY SESSIONS: WHAT YOU NEED TO KNOW一文。
健壮的传输协议
我们知道HTML5 的websocket协议定义了websocket api使得网页可以利用websocket协议和远端主机进行全双工通信。websocket协议应该是最快,最好的web通信协议,已被大多数的浏览器所支持。那么为什么还需要sockjs进行封装?
在真实的网络世界中,实际上有着非常复杂的网络拓扑结构,在浏览器和server之间,含有很多的中间节点,包括路由器,代理服务器,反向代理服务器,负载均衡器等等。即使Html5 websocket协议已经成为了标准,但是这些中间节点并不一定会遵守这些标准,还有很大可能会阻止websocket handshake的过程,结果无法建立websocket连接。
How HTML5 Web Sockets Interact With Proxy Servers一文中提到了websocket协议和代理服务器的“不友善关系”,源于代理服务器对websocket handshake的阻挠和对长连接,空闲连接的关闭处理,让我们看到了如果只是直接利用websocket协议,实在是困难重重。
sockjs的出现,实际是为了解决这个问题,使得人们可以建立健壮的web实时通信程序。
sockjs服务器传输协议不仅提供了websocket协议的支持,还提供了流传输Streaming和轮询Polling ,其中又包括多种底层传输方案,如:
xhr,xhr_streaming,jsonp,eventsource,htmlfile。每一种传输方案,其实都值得开辟一个章节来大写特写。
如果浏览器客户端js,采用websocket连接不上服务器,它可以回退选择其他传输方案,那么确保总可以利用一种传输协议,连接到服务器。那么开发者就不需要理睬那些可恶的中间节点了。
使用方法
SockJS模拟WebSockets API,而且是WebSocket SockJS Javascript对象。首先,您需要加载SockJS JavaScript库。例如,您可以把它放在你的HTML头:
<script src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
脚本加载后可以与SockJS服务器建立一个连接。这是一个简单的例子:
var sock = new SockJS('https://mydomain.com/my_prefix');
sock.onopen = function() {
console.log('open');
};
sock.onmessage = function(e) {
console.log('message', e.data);
};
sock.onclose = function() {
console.log('close');
};sock.send('test');
sock.close();
SockJS-client API
SockJS类
相似的WebSocket API,SockJS的构造函数接受一个,或多个参数:
var sockjs = new SockJS(url, _reserved, options);
url可能包含一个查询字符串,如果你需要的话。
实践代码片段:
//注:这里的消息提醒需要引入iNotifyJS库,协议处理需要引入stompJS库 var iN = new iNotify().init({ effect: 'scroll', message:"有消息拉!", audio:{ file: 'static/dist/others/4331.mp3'//可以使用数组传多种格式的声音文件 }, notification:{ title:"通知!", icon:"", body:'您来了一条新消息' } }); iN.isPermission(); var client; var url = "/imapi/stomp"; var ws = new SockJS(url); client = Stomp.over(ws); var headers = { login: '123', passcode: '' }; client.connect(headers, function(success) { //iN.notify().player().setFavicon(10); var subscription = client.subscribe("/topic/site/"+main.userObject.siteId, function(message) { main.totalFunc(); // 文本格式uri解码 message = decodeURI(message); // 解码之后还会有一些残留的编码, 替换掉 message = message.replace(/%3A/g,':'); message = message.replace(/%2C/g,','); message = message.replace(/\+/g,' '); // 截取报文, 以及去掉最后一个结束符 '=' message = message.substring(message.indexOf("{"), message.lastIndexOf('=')); // 转换为json对象 message = JSON.parse(message); if(message && message.site) { iN.notify({ title: message.site.title, body: message.site.body }).player(); //console.info(message); } // 待处理订单数量 if(message && message.repair) { // msgType : 报修类型 // count_wait_for_process : 待受理的订单数量 // count_wait_for_check : 已申请验收的订单数量 if(message.repair.msgType == 'HOME_REPAIR'){ main.messageSend.count_wait_for_process_homeOrder = message.repair.count_wait_for_process; main.messageSend.count_wait_for_check_homeOrder = message.repair.count_wait_for_check; } if(message.repair.msgType == 'PUBLIC_REPAIR'){ main.messageSend.count_wait_for_process_publicOrder = message.repair.count_wait_for_process; main.messageSend.count_wait_for_check_publicOrder = message.repair.count_wait_for_check; } if(message.repair.msgType == 'COMMUNITY_COMPLAINT'){ main.messageSend.count_wait_for_process_complaintOrder = message.repair.count_wait_for_process; main.messageSend.count_wait_for_check_complaintOrder = message.repair.count_wait_for_check; } // 打印结构 //console.info(message.repair); } }); } ,function(error){ console.log('connect error:'+error);//连接出错或掉线时会回调这个方法。 util.showTip("实时推送消息连接未成功或连接掉线,请刷新网页重连"); } );
参考资料:
1.http://blog.csdn.net/wyx819/article/details/46592153