Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebIM技术---编写前端WebSocket组件 #21

Open
matthew-sun opened this issue Jul 15, 2016 · 1 comment
Open

WebIM技术---编写前端WebSocket组件 #21

matthew-sun opened this issue Jul 15, 2016 · 1 comment

Comments

@matthew-sun
Copy link
Owner

matthew-sun commented Jul 15, 2016

过去我们想要实现一个实时Web应用通常会考虑采用ajax轮循或者是long polling技术,但是因为频繁的建立http连接会带来多余的请求以及消息精准性的问题,让我们在实现实时Web应用时头疼不已。现在,Html5提出了WebSocket协议来规范解决了这个问题。

ajax轮询,long polling技术实现原理

ajax轮询

ajax轮询非常简单了,就是在客户端设置一个定时器,频繁的去请求接口,看有没有数据返回,但是这样很明显会有很多的多余请求,导致服务器压力巨大。。

long polling

long polling技术,其实是在ajax轮询的基础上再做了一些优化,客户端请求服务端看有没有返回,如果暂时没有,服务端会把请求短暂的挂起,当有数据的时候再把数据返回给客户端,这时候客户端再另起一个请求,询问服务器。
为了保证连接的通道不断,我们通常会设置一个timeout,当过了这个timeout时还没有数据,服务端会把挂起的服务关闭,返回空的数据,然后客户端再进行请求。
这样做虽然比ajax轮询好很多,但是当消息量大的时候,请求数还是很多。而且轮询还极有可能在传输的过程中遇到消息丢失的情况,这时候需要服务端做消息缓存等处理。

WebSocket协议

WebSocket协议本质上是一个基于TCP的协议,它由通信协议和编程API组成,WebSocket能够在浏览器和服务器之间建立双向连接,以基于事件的方式,赋予浏览器实时通信能力。既然是双向通信,就意味着服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。
简单来说,WebSocket就是一个长连接通道,在这个通道里客户端和服务端可以自由的发送消息给对方,而且不用管对方是否有返回,双方都有关闭这个通道的权利。

WebSocket通信场景

客户端:啦啦啦,我要建立Websocket协议,Websocket协议版本:17(HTTP Request)
服务端:ok,确认,已建立Websocket(HTTP Protocols Switched)
客户端:有消息的时候记得推给我哦。
服务端:ok,有的时候会告诉你的。
服务端:balabalabalabala
服务端:balabalabalabala
服务端:哈哈哈哈哈啊哈哈哈哈
客户端:哈哈哈哈哈哈哈,你可以不用返回
...

使用WebSocket有什么好处

  • WebSocket 能节约带宽、CPU 资源并减少延迟。
  • WebSocket 基于事件交流,通信简单。
  • WebSocket 可以跨域。

WebSocket API介绍

建立WebSocket连接

var ws = new WebSocket('ws://www.websocket.org')

为WebSocket 对象添加 4 个不同的事件:

  • open
  • message
  • error
  • close

代码示例:

// 当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对象方法

  • send
  • close

代码示例:

// 发送消息 
ws.send('blablabla')

// 关闭socket
ws.close()

定义一个前端Socket组件

为什么需要定义一个Socket组件

HTML5提供的SocketAPI太过于简陋,并不能满足复杂环境的socket交互需要,API的调用也不太方便。

定义一个什么样的Socket组件

  • 简单好用的客户端和服务端的双向通信API
  • 支持断线重连功能
  • 支持自定义事件
  • 能够自由感知socket状态信息

Socket组件示例

首先要和服务端约定互相可以识别的通信协议,假设我们约定的通信协议是

// 客户端发送给服务端
{
    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建立连接。

@chenzx
Copy link

chenzx commented Jul 17, 2016

基于WebSocket可以构造实时搜索体验,因为如果不是活动长连接的话,每次搜索都会有连接建立的开销(不过当然也可以使用preconnect);不过ws只是种客户端技术,它仍然需要服务器端的适配改变:比如要能够支持上万并发连接的前端分发server端(而且还要求尽量小的线程调度、内存分配、上下文切换开销)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants