过去我们想要实现一个实时Web应用通常会考虑采用ajax轮循或者是long polling技术,但是因为频繁的建立http连接会带来多余的请求以及消息精准性的问题,让我们在实现实时Web应用时头疼不已。现在,Html5提出了WebSocket协议来规范解决了这个问题。
ajax轮询非常简单了,就是在客户端设置一个定时器,频繁的去请求接口,看有没有数据返回,但是这样很明显会有很多的多余请求,导致服务器压力巨大。。
long polling技术,其实是在ajax轮询的基础上再做了一些优化,客户端请求服务端看有没有返回,如果暂时没有,服务端会把请求短暂的挂起,当有数据的时候再把数据返回给客户端,这时候客户端再另起一个请求,询问服务器。
为了保证连接的通道不断,我们通常会设置一个timeout,当过了这个timeout时还没有数据,服务端会把挂起的服务关闭,返回空的数据,然后客户端再进行请求。
这样做虽然比ajax轮询好很多,但是当消息量大的时候,请求数还是很多。而且轮询还极有可能在传输的过程中遇到消息丢失的情况,这时候需要服务端做消息缓存等处理。
WebSocket协议本质上是一个基于TCP的协议,它由通信协议和编程API组成,WebSocket能够在浏览器和服务器之间建立双向连接,以基于事件的方式,赋予浏览器实时通信能力。既然是双向通信,就意味着服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。简单来说,WebSocket就是一个长连接通道,在这个通道里客户端和服务端可以自由的发送消息给对方,而且不用管对方是否有返回,双方都有关闭这个通道的权利。
客户端:啦啦啦,我要建立Websocket协议,Websocket协议版本:17(HTTP Request)
服务端:ok,确认,已建立Websocket(HTTP Protocols Switched)
客户端:有消息的时候记得推给我哦。
服务端:ok,有的时候会告诉你的。
服务端:balabalabalabala
服务端:balabalabalabala
服务端:哈哈哈哈哈啊哈哈哈哈
客户端:哈哈哈哈哈哈哈,你可以不用返回
...
建立WebSocket连接
var ws = new WebSocket('ws://www.websocket.org')
为WebSocket 对象添加 4 个不同的事件:
代码示例:
// 当websocket连接建立成功时 ws.onopen = function() { console.log('websocket 打开成功'); }; // 当收到服务端的消息时 ws.onmessage = function(e) { // e.data 是服务端发来的数据 console.log(e.data); }; // 当websocket关闭时 ws.onclose = function() { console.log("websocket 连接关闭"); }; // 当出现错误时 ws.onerror = function() { console.log("出现错误"); };
WebSocket对象方法
代码示例:
// 发送消息 ws.send('blablabla') // 关闭socket ws.close()
HTML5提供的SocketAPI太过于简陋,并不能满足复杂环境的socket交互需要,API的调用也不太方便。
首先要和服务端约定互相可以识别的通信协议,假设我们约定的通信协议是
// 客户端发送给服务端 { method: 'xxx', request: {} } // 服务端返回给客户端 { data: {}, success: true, errorCode: 0, request: {} // 如果是服务端主动推消息给客户端,request会带有method参数 // 如果是服务端返回客户端请求,request就是客户端之前请求的数据 }
然后我们上代码:
/* * @example * var ws = new Socket('ws://www.websocket.org') * ws.on('ready',function() { * console.log('服务器连接成功'); * ws.on('message', function(json) { * console.log('一条新消息:'+json.session); * }); * ws.emit("send", { * session: "一条新消息" * }) * }) * ws.on("error",function(){ * console.log("连接报错") * }) * ws.on("close",function(){ * console.log("连接关闭"); * }) */ function Socket(url) { this.init(url) } Socket.prototype = { init: function(url) { this.initListeners() this.initSocket(url) this.bindSocketEvent() }, initSocket: function(url) { this.url = url this.socket = new WebSocket(url) return this }, initListeners: function() { this.listeners = {} return this }, bindSocketEvent: function() { var me = this me.socket.onopen = function() { me.stopHeartBeat() me.startHeartBeat() me.clearAll() me.trigger('ready') } me.socket.onerror = function(e) { me.trigger('close', e) me.close() } me.socket.onmessage = function(e) { me.refreshServerTimer(); var json = JSON.parse(e.data); me.trigger(json.request.method, json); } return this }, reConnect: function() { this.initSocket(this.url).bindSocketEvent() this.trigger('reconnect') }, isOffline: function() { return this.socket.readyState != WebSocket.OPEN }, on: function(evt, fn) { var me = this if(me.listeners[evt] && me.listeners[evt].length) { if(me.listeners[evt].indexOf(fn) == -1){ me.listeners[evt].push(fn) } }else { me.listeners[evt] = [fn] } return this }, off: function(evt, fn) { var me = this if(me.listeners[evt] && me.listeners[evt].length){ var index = me.listeners[evt].indexOf(fn) if(index != -1){ me.listeners[evt].splice(index,1) } } return this }, emit: function(method, info) { var me = this me.socket.send(JSON.stringify({ method: method, request: info || '' })) return this }, trigger: function(evt) { var me = this if(me.listeners[evt]) { for(var i=0; i<me.listeners[evt].length; i++) { me.listeners[evt][i].apply(me, [].slice.call(arguments,1)) } } return this }, startHeartBeat: function() { var me = this me.heartBeatTimer = setInterval(function() { me.emit("heartBeat") }, 5000) }, stopHeartBeat: function() { clearInterval(this.heartBeatTimer) }, //重新开始断线计时,20秒内没有收到任何正常消息或心跳就超时掉线 refreshServerTimer: function() { var me = this clearTimeout(me.serverHeartBeatTimer) me.serverHeartBeatTimer = setTimeout(function() { me.trigger("disconnect") me.close() me.reConnect() }, 20000) }, clearAll: function() { var tmp = this.listeners['ready'] this.listeners = {} this.listeners['ready'] = tmp return this }, close: function(options) { var me = this; clearTimeout(me.serverHeartBeatTimer); me.stopHeartBeat(); me.socket.close(); return this } }
介绍一下组件里的心跳包机制:
因为一些原因,有这么一种情况,socket还在客户端连着,但是服务端和客户端之间却没有办法互相发送消息,我们称这种现象叫做WebSocket失活。
组件里采用的解决办法是,客户端每5秒钟向服务端发送心跳包,讲道理服务端会返回一个心跳包以保活。但是如果客户端检查1分钟内没有收到服务端的返回,客户端会自动重连WebSocket。
WebSocket在建立连接之前,会先发一个http协议询问服务端要不要建立WebSocket,因为http请求是会带上cookie的,这时候如果域名下的cookie太多,有可能会导致WebSocket建立连接失败。。
我这里的解决方案是,更换接口的域名地址,利用WebSocket可以跨域的特性绕过当前域的cookie建立连接。