WebSocket是一种在Web浏览器和Web服务器之间创建持久性连接的通信协议。它允许客户端和服务器之间进行全双工通信,这意味着服务器可以主动向客户端发送数据,而不需要客户端首先发起请求。通常用于实时数据传输的场景

背景

  • 在 websocket 出来前,想实现实时通信、变更推送、服务端信息推送功能,一般方案是使用 ajax 短轮询、长轮询两种方式
  • 短轮询(short polling)是客户端定时向服务器发出请求,询问服务器是否有新的数据。如果服务器有新的数据,就立即返回给客户端;如果没有,就返回一个空的响应。这种方式的问题是如果数据变化的频率比较低,会产生大量无效请求,浪费带宽。
  • 长轮询(long polling)也是客户端向服务器发出请求,但服务器并不立即返回响应,而是等到有新的数据时才返回。这样就可以减少无效请求,降低带宽的消耗。但长轮询可能导致服务器需要维持大量未完成的请求,增加了服务器的负担。

通信过程以及原理

建立链接

  • WebSocket 协议属于应用层协议,依赖传输层的 TCP 协议。通过 HTTP/1.1 协议的101状态码进行握手建立连接

具体过程

  • 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket
  • 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性
  • 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应
// 客户端请求
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Upgrade: websocket
Origin: http://localhost:63342
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,en;q=0.7
Sec-WebSocket-Key: b7wpWuB9MCzOeQZg2O/yPg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

// 服务端响应
HTTP/1.1 101 Web Socket Protocol Handshake
Connection: Upgrade
Date: Wed, 22 Nov 2023 08:15:00 GMT
Sec-WebSocket-Accept: Q4TEk+qOgJsKy7gedijA5AuUVIw=
Server: TooTallNate Java-WebSocket
Upgrade: websocket
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

优缺点

优点

  • 实时性
  • 减少网络延迟:与短轮询和长轮询相比
  • 较小的数据传输开销:WebSocket 的数据帧相比于 HTTP 请求报文较小
  • 较低的服务器资源占用
  • 跨域通信:与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信

缺点

  • 连接状态保持
  • 不适用于所有场景
  • 复杂性

适用场景

  • 实时聊天应用
  • 在线协作和协同编辑
  • 实时数据展示
  • 在线游戏
  • 推送服务

封装一个 Socket 类

class Socket {
    constructor (email) {
        this.email = email;
        this.instance = null;
        this.ws = null;
        this.autoConnectTimes = 0;
        this.aliveTimer = null;
        this.handlers = {};
    }
    static getInstance(email) {
        if(!this.instance) {
            this.instance = new Socket(email);
        };
        return this.instance;
    }
    connect () {
        //无效
        if(!this.email) return;
        //创建
        if('WebSocket' in window) {
            //协议类型 http: / https:
            const protocalType = window.location.protocol;
            const wsProtocol = protocalType == 'https:' ? 'wss:' : 'ws:'
            //实例话WebSocket对象
            let connectUrl = '';
            if(process.env.NODE_ENV == 'development' || process.env.DEPLOY_ENV == 'dev' || process.env.DEPLOY_ENV == 'qa') {
                connectUrl = '';
            } else if(process.env.DEPLOY_ENV == 'pre') {
                connectUrl = '';
            } else {
                connectUrl = '';
            };

            this.ws && this.ws.close();
            this.ws = new WebSocket(connectUrl);

            this.ws.onopen = () => {
                this.onOpen.apply(this);
            };
            this.ws.onmessage = (evt) => {
                this.onMeessage.apply(this, [evt])
            };
            this.ws.onclose = (evt) => {
                this.onClose.apply(this, [evt]);
            };
            this.ws.onerror = () => {
                this.onError.apply(this);
            };

        } else {
            Vue.prototype.$toast({
                type: 'error',
                content: '您的浏览器不支持 WebSocket,请选择Chrome、Firefox等登录!'
            });
        };

        //关闭
        let that = this;
        window.onbeforeunload = function() {
            try {
                if(that.ws) {
                    that.ws.onclose = function () {};
                    that.ws.close();
                };
            } catch (error) {
                //
            };
        };
    }
    //建立连接后的回调
    onOpen () {
        this.sendMessage({'model': 'callcenter', 'type': 'seatstatus'});
        //ping && pang
        this.keepWsAlive();
        this.autoConnectTimes = 0; //重置
    }
    //收到消息回调
    onMeessage (evt) {
        let data = JSON.parse(evt.data);
        // 处理各种消息类型
        this.dispatchEvent('message', data);
    }
    //连接断开回调
    onClose (evt) {
        //打印断开原因
        let flag = evt.wasClean || false;
        //关闭心跳
        if(this.aliveTimer !== null) {
            clearInterval(this.aliveTimer);
        };
        //非手动断开状态状态,断开连接后自动连接
        if(!flag) {
            this.autoConnect();
        };
    }
    //通信错误时触发
    onError () {
        //断开连接后自动连接
        // this.autoConnect();
    }
    //发送消息 @params {name: params, type: Obejct}
    sendMessage (params) {
        let str = JSON.stringify(params);
        try {
            if(this.ws.readyState == 1) {
                this.ws.send(str);
            } else {
                console.log(this.ws.readyState);
            };
        } catch (error) {
            console.log(error);
        };
    }
    //心跳
    keepWsAlive () {
        if(this.aliveTimer !== null) {
            clearInterval(this.aliveTimer);
        };
        this.aliveTimer = setInterval(() => {
            this.sendMessage({'model': 'ping'});
        }, 10000);
    }
    autoConnect () {
        if(this.autoConnectTimes < 20) {
            this.autoConnectTimes++;
            setTimeout(() => {
                this.connect();
            }, 2000);
        } else {
            // 异常断开
            this.dispatchEvent('ccBreakLine', {});
        }
    }
    //添加订阅者
    addEventListener (type='message', handler) {
        // 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
        if (!(type in this.handlers)) {
            this.handlers[type] = [];
        };
        // 将事件存入
        this.handlers[type].push(handler);
    }
    // 触发事件
    dispatchEvent (type='message', ...params) {
        // 若没有注册该事件则抛出错误
        if (!(type in this.handlers)) {
            return new Error('未注册该事件')
        };
        // 便利触发
        this.handlers[type].forEach(handler => {
            handler(...params);
        });
    }
    removeEventListener (type, handler) {
        // 无效事件抛出
        if (!(type in this.handlers)) {
            return new Error('无效事件');
        }
        if (!handler) {
            // 直接移除事件
            delete this.handlers[type];
        } else {
            const idx = this.handlers[type].findIndex(ele => ele === handler);
            // 抛出异常事件
            if (idx === undefined) {
                return new Error('无该绑定事件');
            }
            // 移除事件
            this.handlers[type].splice(idx, 1)
            if (this.handlers[type].length === 0) {
                delete this.handlers[type];
            };
        };
    }
};

export default Socket;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.