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

  • 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容