我想和你聊聊天-webSocket

598 阅读4分钟

近期公司做IM,用户与后台专家聊天的需求,顺便小研究下webSocket。

webSocket的目标是在一个单独的持久链接上提供全双工,双向通信。

为了解决什么问题:

  1. 传统的客户端与服务器通信,请求只能由客户端发起,服务器无法向客户端主动推送消息。
  2. 为了实时获取最新数据,长链接,轮循方式效率低。
  3. webSocket,让服务器可以主动向客户端推送数据,使浏览器具备了双向通信的能力。

在js中创建webSocket 之后,会有一个http请求发送到服务器来发起链接。在取得服务器响应后,建立的链接会使用http升级从http协议交换为socket协议。

也就是说,使用标准的http服务无法使用socket,只有支持这种协议的专门服务器才能正常工作。

由于websocket使用了自定义协议,url模式也略有不同,是ws:// 和wss://。

使用自定义协议的好处是,能够在客户端和服务器之间发送少量的数据,而不担心http那样字节级的开销。

使用自定义协议的缺点在于,制定协议的时间比制定jsAPI的时间还要长。webSocket曾几度搁浅,因为不断有人发现新协议存在一致性和安全性的问题,不过现在已经统一了。

socket优点:

  1. 双向通信。
  2. 数据格式轻量,性能开销小,通信高效。
  3. 支持二进制。
  4. 没有同源策略限制。
  5. 与http协议良好兼容,握手阶段采用http协议,因此能通过各种http代理服务器。

使用socket需要解决问题有:

webSocket不稳定,如果是有服务端主动关掉,会触发客户端关闭的回调。但是如果出现其他方面的通信异常,客户端或服务端是无法感知到当前socket已经不能正常工作了。

总结有两种办法:

在error和close事件中都触发重连,除非手动关闭。

优点是方便一点,缺点在重连这段事件可能会丢数据,可能有异常不会触发这两个事件。

心跳机制:

每个一段事件发送一次ping到服务端,来告诉服务端,我还在,服务端返回一个pong,告诉客户端,嗯,我也在。

心跳有两种策略,

一种你直接发送一个ping,如果紧接着返回一个pong,那么在过程中发送的消息如何界定成功与失败。如果ping与pong返回错乱,无法保证互相的对应,除非为每一个ping添加标示ID,同时返回pong也携带标识ID。

一种你没发送两次ping,去检测一次pong,如果检测到有一测pong,就认为当前socket的状态是正常的,如果两次都没有返回pong,则认为socket异常。

其实最后没有用心跳来检测当前socket状态,心跳只是为了维持与后台的socket连接,因为后台有超时时间,如果超时时间内没有数据交互,则会断开socket。

问题点:

如何判断当前socket状态,来进而判断socket是否有发送成功。

以及在何时触发重连。

close事件,

eror时间,

内部readyState,

心跳状态。

websocket 的close和error事件很怪。有人说socket 的底层也是用心跳来说实现的,但是断网完全不会触发socket的close事件,所以说断网的逻辑还需要单独添加。

同时也说明,内部的readystate也不靠谱。更为严格的说,socket发消息还需要确认逻辑,但是实现起来更为复杂,给每条消息赋值一个唯一的ID,然后服务器接收到消息后再将次ID返回,用于发送成功的确认。同时服务器给用户推送消息也需要确认。

封装了一个简单的socket类:

包含收发消息,心跳检测,中断重连,限制重连次数。

class SocketClass{
    constructor(params){
        this.params = params;
        const NOOP = () => {};
        let {url, name = 'default', socketOpen = NOOP, msgCbk = NOOP} = params;
        this.url = url;
        this.msgCbk = msgCbk;
        this.socketOpen = socketOpen;
        this.name = name;
        this.ws = null;  // websocket对象
        this.status = ''; // websocket是否被手动关闭
        this.pingPong = '';
        this.pingInterval = null;
        this.pongInterval = null;
        this.reConnectTime = 1; // 重连限制时间 minuit
        this.firsReConnectTime = ''; // 内部用 记录第一次重连时间
        this.reConnectCount = 0; // 内部用 当前时间段内已经重连次数
        this.reConnectMaxCount = 5; // 重连限制时间内的最大重连次数,
        this.reConnectFlag = false; // 超过次数限制 正在等待重连的标志位
    }
    connect(firstData) {
        this.ws = new WebSocket(this.url);
        this.ws.onopen = e => {
            this.status = 'open';
            this.heartCheck();
            this.socketOpen();
            if (firstData) {
                this.ws.send(firstData);
            }
        };
        this.ws.onmessage = e => {
            if (e.data === 'ping') {
                this.pingPong = 'pong';
            }
            return this.msgCbk(e.data);
        };
        this.ws.onclose = e => {
            console.warn('close');
            this.closeHandle(e);
        };
        this.ws.onerror = e => {
            console.warn('error');
            this.closeHandle(e);
        }
    }
    sendMessage(data) {
        if(this.ws.readyState === 1){
            this.ws.send(data);
            return true;
        }
        return false;
    }
    heartCheck() {
        this.pingPong = 'ping';
        this.pingInterval = setInterval(() => {
            if (this.ws.readyState === 1) {
                this.ws.send('ping');
            }
        }, 10000);
        this.pongInterval = setInterval(() => {
            if (this.pingPong === 'pong') {
                this.closeHandle('pingPong没有改变为pong');
            }
            this.pingPong = 'ping'
        }, 20000)
    }
    closeHandle(e = 'err') {
        if (this.status === 'close') {
            console.log(`${this.name}websocket手动关闭`)
            return;
        }
        if(!this.firsReConnectTime){
            this.firsReConnectTime = new Date().getTime();
        }
        if((new Date().getTime() - this.firsReConnectTime) < this.reConnectTime * 60 * 1000){
            if(this.reConnectCount < (this.reConnectMaxCount - 1) ){
                this.reConnectCount ++;
            }else{
                if(!this.reConnectFlag){
                    console.warn('一分钟内重连超过五次, 取消重连');
                    this.reConnectFlag = true;
                    setTimeout(() => {
                        this.connect();
                        this.reConnectFlag = false;
                    }, 60000);
                }
                return;
            }
        }else{
            this.reConnectCount = 0;
            this.firsReConnectTime = new Date().getTime();
        }
        if (this.pingInterval !== undefined && this.pongInterval !== undefined) {
            clearInterval(this.pingInterval);
            clearInterval(this.pongInterval);
        }
        this.connect();
    }
    closeSocket() {
        this.status = 'close';
        return this.ws.close();
    }
}



msgCbk = (msg) => {
    console.warn(msg);
};
const wsValue = new SocketClass({
    // url: 'ws://123.207.136.134:9010/ajaxchattest',
    url: 'ws://127.0.0.1:8080',
    msgCbk,
});
wsValue.connect('立即与服务器通信'); // 连接服务器
let i = 0;
setInterval(() => {
    i++;
    console.warn(wsValue.sendMessage(`传消息给服务器${i}`));
    wsValue.sendMessage(`传消息给服务器${i}`);
}, 1000);