WebSocket入门学习

什么是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

在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连接



如果客户端发起的连接请求没有找到对应的服务端进行相应,则会触发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入门学习)