美文网首页让前端飞
WebSocket入门学习

WebSocket入门学习

作者: 前端辉羽 | 来源:发表于2021-01-13 19:51 被阅读0次

    什么是WebSocket?
    WebSocket是一种网络传输协议,可在单个TCP链接上进行全双工通信,位于OSI模型的应用层。
    特点:

    • TCP链接,与HTTP协议兼容
    • 双向通信,主动推送(服务端向客户端)
    • 无同源限制,协议标识符是ws(加密wss)

    应用场景:

    • 聊天,消息,点赞(后台消息的及时通知)
    • 直播评论(弹幕)
    • 游戏,协同编辑,基于位置的应用

    传统做法:ajax轮询
    缺点:因为http的无状态特征,轮询的间隙的变化无法感知,频繁的请求给服务器造成很大的压力,造成效率低下,浪费资源

    进行WebSocket开发常用的两个库

    • ws:基于原生进行实现,效率高
    • socket.io:很好的实现了向下兼容,但因为代码臃肿,运行效率比较低

    初识websocket应用

    项目地址:https://github.com/chenhui-syz/ws-chat-demo
    项目根目录名称:ws-chat-demo
    项目文件结构:

    示意图.png
    新建两个文件夹:
    • client
    • server

    分别在这个两个文件夹中进行初始化并安装ws

    • npm init -y
    • npm install ws@7.2.1 -S

    在index.html文件模板的script写入下面代码

    var ws = new WebSocket('ws://127.0.0.1:3000')
    

    这就相当于在客户端通过ws协议,向本地服务器的3000端口发送了一个WebSocket连接请求。
    此时在client文件夹的index.js入口文件中初始化一个WebSocket的Server服务,并监听连接请求

    const WebSocket = require('ws')
    const wss = new WebSocket.Server({
        port: 3000
    })
    wss.on('connection', function connection(ws) {
        console.log('one client is connected')
    })
    

    node index.js运行这个文件,并在浏览器中打开index.html,此时终端中会打印“one client is connected”,这代表客户端发起的ws请求已成功连接了。

    WebSocket通信的最大特点就是既可以在客户端主动发起请求,也可以在服务端主动发起请求。

    上面的操作websocket长连接已经建立起来了,但是还没有开始去发送消息
    在index.js中增加监听接收到消息的回调事件以及主动发送消息的代码:

    wss.on('connection', function connection(ws) {
        console.log('one client is connected')
        // 接收客户端发来的消息
        ws.on('message', function (msg) {
            console.log('接收客户端发来的消息', msg)
        })
        // 主动发送消息给客户端
        ws.send('一条来自服务端的消息')
    })
    

    在客户端监听“建立连接”以及接收消息:

    var ws = new WebSocket('ws://127.0.0.1:3000')
    // 监听“建立连接”
    ws.onopen = function () {
        ws.send('客户端建立了连接')
    }
    // 监听“接受到新消息”
    ws.onmessage = function (event) {
        console.log('客户端接收到的消息', event.data)
    }
    

    在client文件夹新建一个testClient.js,可以实现在客户端通过ws去发起建立连接请求,并主动向服务端发送消息

    const WebSocket = require('ws')
    const ws = new WebSocket('ws://127.0.0.1:3000')
    ws.on('open', function () {
        console.log('client is connected to Server')
        ws.send('client say hello to server')
        ws.on('message', function (msg) {
            console.log('接收服务端发来的消息', msg)
        })
    })
    

    分别在终端中通过node运行index.js和testClient.js文件,
    index.js打印如下结果:

    one client is connected
    接收客户端发来的消息 client say hello to server
    

    testClient.js打印如下结果:

    client is connected to Server
    接收服务端发来的消息 一条来自服务端的消息
    

    同样的,将testClient.js的代码建立在server文件夹中,可以实现在node端建立client服务

    我们最终要实现的还是在浏览器端实现client服务,在node端实现server服务,客户端和服务端的常用事件基本一样,区别只是在客户端是下面这个监听方式:

    ws.onopen = function () {
        ws.send('客户端建立了连接')
    }
    

    服务端:

    ws.on('open', function () {
        ws.send('服务端建立了连接')
    })
    

    认识ws.readyState

    Constant Value
    WebSocket.CONNECTING 0
    WebSocket.OPEN 1
    WebSocket.CLOSING 2
    WebSocket.CLOSED 3

    在node端如果通过ctrl+c停止服务运行,websocket自然也就强行停止了,此时ws.readyState变成3
    客户端可以通过触发事件去手动关闭ws连接

    <button type="button" id="app">按钮</button>
    <script>
    document.getElementById('app').addEventListener('click', function () {
        ws.close()
    })
    </script>
    

    如果客户端发起的连接请求没有找到对应的服务端进行相应,则会触发error事件回调

    ws.onerror = function () {
        console.log('error', ws.readyState)
    }
    

    此时的readyState也是3

    多人聊天室应用

    定制脚本:
    npm install -D nodemon
    package.json的script中添加"dev":"nodemon index.js"
    下次npm run dev启动server服务就可以了。

    服务端通过监听“message”然后send消息,只是针对此次事件进行了回应,当前发来消息的客户端可以接收到,而其他客户端接收不到,这正好和我们的需求实现是相反的。

    ws.on('message', function (msg) {
        console.log('接收客户端发来的消息', msg)
        // 服务端收到消息之后把这个消息再发送回去
        ws.send('form server'+msg)
    })
    

    改写成下面这样:

    ws.on('message', function (msg) {
        console.log('接收客户端发来的消息', msg)
        // 服务端收到消息之后把这个消息再发送回去
        // ws.send('form server'+msg)
        // 广播消息
        wss.clients.forEach((client) => {
            // 判断非自己的客户端并且是连接状态,才去广播消息
            if (ws !== client && client.readyState === WebSocket.OPEN) {
                client.send(msg)
            }
        })
    })
    

    wx.send只能发送字符串或者是二进制底层数据,所以为了添加更多的标识,需要将对象转换为字符串进行发送

    this.ws.send(JSON.stringify({
        event: 'enter',
        message: this.name
    }))
    

    后台还是原来那样把收到的字符串原样返回来,然后前端进行处理

    onMessage: function (event) {
        var obj = JSON.parse(event.data)
        if (obj.event === 'enter') {
            // 当有一个新用户进入聊天室
            this.lists.push('欢迎' + obj.message + '加入聊天室!')
        } else {
            this.lists.push(obj.message)
        }
    },
    

    统计进入聊天室的人数wss.clients.size,但是如果想统计最近时间段有发送消息的客户端数量,可以定义一个全局变量num,然后再ws.on('message', function (msg) {...}进行++,过滤掉一些挂机状态的连接
    但是上面的做法又存在如果已发送消息的客户端断开连接,num没有--,所以需要增加对close的监听,同时增加xxxx离开聊天室的广播消息,需要注意的是,浏览器f5也会相当于触发了断开和重新连接的操作

    ws.on('close', function () {
        // ws.name有值,则表示当前连接是有效的
        if (ws.name) {
            num--
        }
        let msgObj = {}
        wss.clients.forEach((client) => {
            // 连接状态,才去广播消息
            if (client.readyState === WebSocket.OPEN) {
                // 给返回的消息添加name
                msgObj.name = ws.name
                // 添加当前在线人数
                msgObj.num = num
                msgObj.event = 'out'
                client.send(JSON.stringify(msgObj))
            }
        })
    })
    

    多聊天室功能实现,首先需要前端传递用户想要进入的聊天室

    this.ws.send(JSON.stringify({
        event: 'enter',
        message: this.name,
        roomid: this.roomid
    }))
    

    在后台代码里,ws特指当前客户端和服务端进行的当此连接
    后台根据拿到的roomid进行相应的广播区别操作

    WebSocket鉴权

    在服务端发起的链接则可以去直接自定义headers

    const WebSocket = require('ws')
    const ws = new WebSocket('ws://127.0.0.1:3000',{
        headers: {
            token: 'demo123'
        }
    })
    

    浏览器发送ws链接不支持自定义headers
    浏览器端也没法去引用ws库,因为浏览器端发起的ws请求会被自动浏览器降级为http自带的WebSocket
    所以在客户端jwt鉴权直接添加到headers的方法没法使用
    协议本身在握手阶段不提供鉴权方案,所以只能在建立连接之后再专门发送一次消息进行token的验证
    解决办法:浏览器在连接成功之后主动发送一次消息,此时消息的作用就是发送本地的token给服务端进行本次连接的鉴权,如果鉴权通过,则本次ws连接后续的所有操作都放行,否则就给弹到login登录页面,让重新获取合法的token

    心跳检测

    原理:服务端去定时的向客户端发送消息,客户端收到消息后进行回应
    心跳检测的请求一定要放在鉴权消息的后面,也就是说建立连接之后第一时间是发送鉴权消息再进行其他操作,否则会导致后面的消息发送鉴权失败。
    心跳检测建立之后,客户端的应用:
    心跳间隔时间+网络时延=判定时间,每次收到ping,都开启一个定时器,如果判定时间过后,还没有收到下次的ping,就主动去断开当此连接,并开启下一次连接

    checkServer: function () {
        var _this = this
        clearTimeout(this.handle)
        _this.handle = setTimeout(function () {
            _this.onClose()
            _this.init()
        }, 2000)
    }
    

    自动断线重连的包推荐:
    ES5:
    reconnecting-websocket
    https://github.com/joewalnes/reconnecting-websocket
    var ws = new WebSocket('ws://....');改为var ws = new ReconnectingWebSocket('ws://....');
    不需要额外的代码,同时支持很多options的配置
    ES6包:
    https://www.npmjs.com/package/reconnecting-websocket

    相关文章

      网友评论

        本文标题:WebSocket入门学习

        本文链接:https://www.haomeiwen.com/subject/pscsaktx.html