为何要用 WebSocket
HTTP 协议遵循经典的客户端-服务器模型,客户端发送一个请求,然后等待服务器端的响应,服务器端只能在接收到客户端的请求之后进行响应,不能主动的发送数据到客户端。
客户端想要在不刷新页面的情况下实时获取到服务器端最新的数据,可以通过以下途径:
- 轮询:客户端(浏览器)定时向服务器端发送请求,获取最新的数据。轮询的间隔过长会导致用户不能及时接收到更新的数据;轮询的间隔过短会增加服务器端的负担。
- 长轮询:客户端发起一个请求到服务器端,服务器端一直保持连接打开,直到有数据推送,再返回这个请求,客户端收到服务器端返回的数据后,处理数据并发起一个新请求。使用这种方法,每有一个客户端服务器端就要一直保持一条连接,当达到服务器处理的上限的时候,服务器将无法响应新的请求。
- SSE:是基于 HTTP 实现的一套服务器向客户端发送数据的 API。他是针对上面说到的三种方法(轮询,长轮询,HTTP 流)的一个标准 API 实现。但不兼容 IE 浏览器。
- Web Sockets:Web Sockets 采用了一套全新的协议(ws/wss)来建立客户端到服务器端的全双工、双向通信连接,相较于 HTTP 请求更加高效(不需要握手,连接始终存在;无需携带头部信息)。
在使用的过程中,根据产品将来的使用环境(支持的浏览器类型、版本),使用场景(双向通信、单向通信)这些点,并结合每一种方法的优缺点去考虑,然后选取对应的策略。
WebSocket 协议在 2008 年诞生,2011 年成为国际标准。所有浏览器都已经支持了。最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
其他特点包括:
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。
下面从服务器端和客户端分别展开讨论。
服务器端
目前有两个流行的 ws 通信库,分别是 ws 和 socket.io。两个库各有特点,简单说,ws 通信速度更快,socket.io 兼容性更好,最低可支持 IE9,而且易用性更好,封装了心跳检测等功能。
两个库用法类似,下面以 ws 为例展开。(ws 的官网:https://github.com/websockets/ws)
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
| const WebSocket = require('ws') const http = require('http') const wss = new WebSocket.Server({ noServer: true }) const server = http.createServer() const jwt = require('jsonwebtoken')
const timeInterval = 1000
let group = {}
wss.on('connection', function connection(ws) { ws.isAlive = true
console.log('one client is connected') ws.on('message', function (msg) { const msgObj = JSON.parse(msg) if (msgObj.event === 'enter') { ws.name = msgObj.message ws.roomid = msgObj.roomid if (typeof group[ws.roomid] === 'undefined') { group[ws.roomid] = 1 } else { group[ws.roomid]++ } } if (msgObj.event === 'heartbeat' && msgObj.message === 'pong') { ws.isAlive = true return }
wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN && client.roomid === ws.roomid) { msgObj.name = ws.name msgObj.num = group[ws.roomid] client.send(JSON.stringify(msgObj)) } }) })
ws.on('close', function () { if (ws.name) { group[ws.roomid]-- } let msgObj = {} wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN && ws.roomid === client.roomid) { msgObj.name = ws.name msgObj.num = group[ws.roomid] msgObj.event = 'out' client.send(JSON.stringify(msgObj)) } }) }) })
server.on('upgrade', function upgrade(request, socket, head) { wss.handleUpgrade(request, socket, head, function done(ws) { wss.emit('connection', ws, request) }) })
server.listen(3000)
setInterval(() => { wss.clients.forEach((ws) => { if (!ws.isAlive && ws.roomid) { group[ws.roomid]-- delete ws['roomid'] return ws.terminate() } ws.isAlive = false ws.send( JSON.stringify({ event: 'heartbeat', message: 'ping', num: group[ws.roomid], }) ) }) }, timeInterval)
|
客户端
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
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> <script src="https://cdn.staticfile.org/vue/2.6.11/vue.min.js"></script> </head>
<body> <div id="app"> <div v-if="isShow"> <p>昵称:<input type="text" v-model="name" /></p> <p>房间号:<input type="text" v-model="roomid" /></p> <button type="button" @click="enter()">进入聊天室</button> </div> <div v-else> <ul> <li v-for="(item,index) in lists" :key="'message' + index"> {{item}} </li> <li>在线人数{{num}}</li> </ul> <div class="ctrl"> <input type="text" v-model="message" /> <button type="button" @click="send()">按钮</button> </div> </div> </div> <script> var app = new Vue({ el: '#app', data: { message: '', lists: [], ws: {}, name: '', isShow: true, num: 0, roomid: '', handle: {}, }, mounted() {}, methods: { init() { this.ws = new WebSocket('ws://127.0.0.1:3000') this.ws.onopen = this.onOpen this.ws.onmessage = this.onMessage this.ws.onclose = this.onClose this.ws.onerror = this.onError }, enter() { if (this.name.trim() === '') { alert('用户名不得为空') return } this.init() this.isShow = false }, onOpen: function () { this.ws.send( JSON.stringify({ event: 'enter', message: this.name, roomid: this.roomid, }) ) }, onMessage: function (event) { if (this.isShow) { return } var obj = JSON.parse(event.data) switch (obj.event) { case 'noauth': break case 'enter': this.lists.push('欢迎:' + obj.message + '加入聊天室!') break case 'out': this.lists.push(obj.name + '已经退出了聊天室!') break case 'heartbeat': this.checkServer() this.ws.send( JSON.stringify({ event: 'heartbeat', message: 'pong', }) ) break default: if (obj.name !== this.name) { this.lists.push(obj.name + ':' + obj.message) } } this.num = obj.num }, onClose: function () { console.log('close:' + this.ws.readyState) console.log('已关闭websocket') this.ws.close() }, onError: function () { console.log('error:' + this.ws.readyState) console.log('websocket连接失败!') var _this = this setTimeout(function () { _this.init() }, 1000) }, send: function () { this.lists.push(this.name + ':' + this.message) this.ws.send( JSON.stringify({ event: 'message', message: this.message, name: this.name, }) ) this.message = '' }, checkServer: function () { var _this = this clearTimeout(this.handle) this.handle = setTimeout(function () { _this.onClose() _this.init() }, 1000 + 500) }, }, }) </script> </body> </html>
|
最后更新时间:
转载请注明出处