WebSocket实现聊天室
# 背景
我们公司的3d渲染之前一直在客户端,客户端渲染虽然服务器压力小,但是弊端还是很明显,对客户端硬件要求高,加载很慢,而且模型一大就会卡,因此,老板决定要把模型渲染挪到服务端去,调研过后决定用nodejs去实现,大概原理如下:
- nodejs 安装模拟浏览器环境的包 以及 threejs
- 渲染出画面以后用websocket推送给客户端
- 客户端鼠标的事件传给服务端
- 服务端的画面根据客户端鼠标事件发生改变,然后将最新的截图推送给客户端
万丈高楼平地起,前两周把nodejs基础课程刷完了,现在开始一步步实现我的想法,首先先实现一下nodejs的websocket
# WebSocket出现的原因
Http协议发布RESR API的不足:
每次请求响应完成之后,服务器与客户端之间的连接就断开了,如果客户端想要继续获取服务器的消息,必须再次向服务器发起请求。这显然无法适应对实时通信要求高的场景。改善http的不足:Web通信领域出现了一些其他的解决方案,如轮询、长轮询、服务器推送事件,WebSocket
轮询:就是重复发送新的请求到服务器。如果服务器没有新的数据,就发送适当的指示并关闭连接。然后客户端等待一段时间(比如间隔一秒),再发送另一个请求。这种实现方式相对比较简单,无需做过多的更改。但学点是轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询时间过短,会导致查询请求过多,增加服务器端的负担。
长轮询:客户端发送一个请求到服务器,如果服务器端没有新的数据,就保持这个连接直到有数据。一旦服务器端有了数据(消息)给客户端。他就是用这个连接发送数据给客户端,接着关闭连接。
服务器推送事件: Server Sent Events(SSE),SSE通常中庸一个连接处理多个消息(事件)。SSE还定义了一个专门的媒体类型,用于描述一个从服务端发送到客户端的简单格式。
WebSocket: 提供了一个真正的全双工链接。发起者是一个客户端,发送一个带特殊HTTP头的请求到服务端,通知服务器。该方案的有点事属于html5标准,已经被大多数浏览器支持,而且是真正的全双工,性能比较好,其缺点是实现起来比较复杂,需要对ws协议专门处理
# Node使用ws创建WebSocket服务器
- Node.js原生API没有提供对WebSocket的支持,需要安装第三方包才能使用WebSocket功能
- ws模块 :是一个用于支持WebSocket客户端和服务器的框架。它易于使用,功能强大,且不依赖于其他环境
- 安装ws: npm install ws 这个库在浏览器不起作用
文档https://github.com/websockets/ws/blob/HEAD/doc/ws.md (opens new window)
https://www.npmjs.com/package/ws#api-docs (opens new window) - 创建WebSocket服务器
// 创建一个WebSocket服务器,在8080端口启动
const WebSocket = require('ws')
const server = new WebSocket.Server({port:8080})
2
3
- WebSocket.Server(options[, callback])方法中options对象所支持的参数
|参数|作用| |----|---| |host|绑定服务器的主机名| |port|绑定服务器的端口号| |backlog|挂起链接队列的最大长度| |server|预先创建的node.js http/s服务器| |verifyClient|可用于验证传入连接的函数| |handelProtocols|可用于处理WebSocket子协议的函数| |path|仅接受此路径匹配的连接| |noServer|不启用服务器模式| |clientTracking|指定是否跟踪客户端| |perMessageDeflate|启用/禁用消息压缩| |maxPayload|允许的最大消息大小(以字节为单位)|
# 监听链接
ws通过connection事件来监听连接
// 只要有WebSocket连接到服务器,就会触发connection事件,req对象可以用来获取客户端的信息,如ip,端口号
// 获取所有已连接的客户端信息,则可以使用server.clients数据集
server.on('connection',function connection(ws, req) {
const ip = req.socket.remoteAddress
const port = req.socket.remotePort
const clientName = ip + port
})
2
3
4
5
6
7
# 发送数据
ws通过send()方法来发送数据
/**
* send(data [,options][,callback])
* data:发送的数据
* options对象:
* (1)compress: 指定数据是否需要压缩。默认为true
* (2) binary: 指定数据是否通过二进制传送,默认是自动检测
* (3)mask: 指定是否应遮罩数据
* (4)fin: 指定数据是否为消息的最后一个片段,默认为true
*/
server.on('connection',function connection(ws, req){
const ip = req.socket.remoteAddress
const port = req.socket.remotePort
const clientName = ip + port
ws.send('Welcome '+clientName)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 接收数据
ws通过message事件来接收数据。当客户端有消息发送给服务器时,服务器就能触发该消息
server.on('connection',function connection(ws, req){
const ip = req.socket.remoteAddress
const port = req.socket.remotePort
const clientName = ip + port
ws.send('Welcome '+clientName)
ws.on('message', function incoming(message){
console.log('recived: %s from %s', message, clientName)
server.clients.forEach(function each(client) {
if(client.readyState === WebSocket.OPEN) {
client.send(clientName + "->" + message)
}
})
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 准备的状态
ws中WebSocket类具有以下4种准备状态
状态 | 值 | 含义 |
---|---|---|
CONNECTION | 0 | 连接还没有打开 |
OPEN | 1 | 连接已经打开,可以通信了 |
CLOSING | 2 | 连接正在关闭 |
CLOSED | 3 | 连接已经关闭 |
# 服务器端 server.js
// 创建一个websocket服务器,在3000端口启动
const WebSocket = require('ws')
const server = new WebSocket.Server({port:3000})
server.on('open', () => {
console.log('connected')
})
server.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress
const port = req.socket.remotePort
const clientName = ip + port
console.log('%s is connected', clientName)
ws.send('Welcome '+ clientName)
ws.on('message', (message) => {
console.log('received: %s from %s', message, clientName)
server.clients.forEach((client) => {
if(client.readyState === WebSocket.OPEN) {
client.send(clientName + " -> " + message)
}
})
})
})
server.on('close', () => {
console.log('disconnected')
})
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
# 客户端(使用了vue)
<template>
<div class="main">
<h3>WebSocket 聊天室:</h3>
<div id="responseTest"></div>
<input type="text" v-model="message" style="width:300px;">
<input type="button" value="发送消息" @click="send(message)">
</div>
</template>
<script setup>
import {ref, onMounted} from "vue"
const message = ref('')
let socket = null
onMounted(() => {
if(!window.WebSocket) {
window.WebSocket = window.MozWebSocket
}
if(window.WebSocket) {
socket = new WebSocket("ws://127.0.0.1:3000/")
socket.onmessage = function(event) {
console.log(event)
var ta = document.getElementById('responseTest')
ta.innerHTML += '<br>' + event.data
}
socket.onopen = function(event) {
var ta = document.getElementById('responseTest')
ta.innerHTML = '连接开启'
}
socket.onclose = function(event) {
var ta = document.getElementById('responseTest')
ta.innerHTML += '<br>' + '连接关闭'
}
}else{
alert('连接没有开启')
}
})
function send(message) {
if(!window.WebSocket) {
return
}
if(socket.readyState === WebSocket.OPEN) {
socket.send(message)
}else{
alert('连接没有开启')
}
}
</script>
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