1 背景
在 WebSocket 出现之前,为了实现推送技术,所用的技术都是轮询 ,轮询是指浏览器每隔一段时间向服务器发出 HTTP 请求,服务器再返回最新的数据给客户端
常见的轮询方式分为轮询与长轮询,它们的区别如下图所示:
这种传统的模式带来很明显的缺点,即浏览器需要不断向服务器发出请求,然而 HTTP 请求与响应可能会包含较长的头部,其中真正有效的数据可能只是很少的一部分,这样会消耗很多带宽资源。因此,HTML5 定义了 WebSocket 协议,能更好地节省服务器资源和带宽,并且能够更实时地进行通讯
2 什么是WebSocket
WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工 通信,它使得客户端和服务器之间的数据交换变得更加简单,只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输,本质上一种计算机网络应用层的协议 ,用来弥补 HTTP 协议在持久通信能力上的不足
它有以下特点:
- 建立在 TCP 协议之上
- 与 HTTP 协议有着良好的兼容性:与 HTTP 和 HTTPS 使用相同的 TCP 端口,可以绕过大多数防火墙的限制,默认端口是 80(ws) 和 443(wss,运行在 TLS 之上),并且握手阶段采用 HTTP 协议
- 较少的控制开销:连接创建后,ws 客户端、服务端进行数据交换时,协议控制的数据包头部较小,而 HTTP 协议每次通信都需要携带完整的头部
- 可以发送文本,也可以发送二进制数据
- 没有同源限制,客户端可以与任意服务器通信
- 协议标识符是 ws,如果加密则为 wss,服务器网址就是 URL
- 支持扩展:ws 协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议,比如支持自定义压缩算法
3 原理
3.1 建立连接
在 WebSocket 开始通信之前,通信双方需要先进行握手,WebSocket复用了 HTTP 的握手通道 ,即客户端通过 HTTP 请求与 WebSocket 服务端协商升级协议,协议升级完成后,后续的数据交换则遵照 WebSocket 的协议
利用 HTTP 完成握手有以下好处:
- 可以让 WebSocket 和 HTTP 基础设备兼容(运行在 80 端口 或 443 端口)
- 可以复用 HTTP 的 Upgrade 机制,完成升级协议的协商过程
3.2 交换数据
WebSocket 的每条消息可能会被切分成多个数据帧 ,发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息
以下是MDN 上的示例:
javascript
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, newmessage containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
在该示例中,客户端向服务器发送了两条消息,第一个消息在单个帧中发送,而第二个消息跨三个帧发送。当 WebSocket 的接收方收到一个数据帧时,会根据 FIN 字段值来判断是否收到消息的最后一个数据帧;利用 FIN 和 Opcode,我们就可以实现跨帧发送消息
其中 Opcode 表示操作码,它的可能值有:
- 0x1:传输数据是文本
- 0x2:传输数据是二进制数据
- 0x0:表示该帧是一个延续帧,这意味着服务器应该将帧的数据连接到从该客户端接收到的最后一个帧
- 0x3-7:保留的操作代码,用于后续定义的非控制帧
- 0x8:表示连接断开
- 0x9:表示这是一个心跳请求ping
- 0xA:表示这是一个心跳响应pong
- 0xB-F:保留的操作代码,用于后续定义的控制帧
具体的数据帧格式大概如下图所示,单位是比特:
- FIN:1 个比特,值为 1 表示这是消息的最后一帧,为 0 则不是
- RSV1, RSV2, RSV3:各占 1 个比特,一般情况下全为 0,非零值表示采用 WebSocket 扩展
- Mask: 1 个比特,表示是否要对数据进行掩码操作
- Payload length:数据负载的长度,单位是字节,为 7 位,或 7+16 位,或 1+64 位
- Masking-key:0 或 4 字节(32 位),所有从客户端传送到服务端的数据帧,数据都进行了掩码操作,Mask 为 1,且携带了 4 字节的 Masking-key;如果 Mask 为 0,则没有 Masking-key
- Payload data:具体数据
3.3 维持连接
使用 WebSocket 进行通信,可以通过建立心跳机制 来判断连接正常没有断开或者服务是否可用,所谓心跳机制,就是定时发送一个数据包,让对方知道自己在线且正常工作,确保通信有效;如果对方无法响应,便可以弃用旧连接,发起新的连接了
需要重连的场景可能包括:网络问题或者机器故障导致连接断开、连接没断但不可用了或者连接对端的服务不可用
发送方 -> 接收方:ping
接收方 -> 发送方:pong
ping 、pong 的操作,对应的是 WebSocket 的两个控制帧,Opcode 分别是 0x9、0xA。比如说,WebSocket 服务端向客户端发送 ping:
javascript
// ping
ws.ping()
// pong
ws.on('pong', () => {
console.log('pong received')
})
客户端也可以发送:
javascript
// 发送心跳包
ws.send('heart check')
// 接收响应
ws.onmessage = (e) => {
const response = e.data
if (response.message === 'connection alive') {
// 重置计时器
}
}
4 WebSocket使用
以下是一个 Vue + WebSocket 使用的 demo,逐步解释每个部分是如何工作的,将从 建立连接 到 发送消息和接收消息 全面讲解 WebSocket 的使用
4.1 创建 WebSocket 客户端
1)创建 WebSocket 实例
创建一个 WebSocket 连接,并连接到ws://localhost:8080 这个 WebSocket 服务器地址。这个地址指向服务器上的 WebSocket 服务,如果服务器成功启动并运行在 8080 端口上,客户端会建立连接
javascript
const socket = new WebSocket('ws://localhost:8080')
2)监听 WebSocket 事件
- onopen:连接建立成功时触发
- onmessage:接收到消息时触发
- onclose:连接关闭时触发
- onerror:发生错误时触发
onopen触发后,我们将连接状态设置为"连接已建立",并发送一条初始化消息到服务器
javascript
socket.onopen = () => {
console.log('WebSocket 连接已建立')
connectionStatus.value = '连接已建立'
socket.send('客户端已连接,发送初始化消息')
}
onmessage处理函数每次收到服务器消息时都会执行,将接收到的消息显示到页面上
javascript
socket.onmessage = (event) => {
console.log('从服务器接收到消息:', event.data)
receivedMessage.value = event.data // 将收到的消息存储到响应式变量中
}
3)发送消息
通过 WebSocket 的send()方法发送消息,sendMessage()函数会在点击按钮时触发,检查 WebSocket 是否已经连接 (readyState === WebSocket.OPEN),如果连接成功,则发送消息
javascript
function sendMessage() {
if (socket.readyState === WebSocket.OPEN) {
const message = '这是客户端发送的消息'
socket.send(message)
console.log('已发送消息:', message)
} else {
console.log('WebSocket 连接未打开,无法发送消息')
}
}
4)状态消息
使用 ref 来处理 WebSocket 连接的状态
- connectionStatus用来显示 WebSocket 连接的状态,如"未连接"、"连接已建立"或"连接已关闭"
- receivedMessage用来显示从服务器接收到的消息
javascript
const connectionStatus = ref('未连接')
const receivedMessage = ref('等待服务器消息...')
整体 vue 代码如下:
javascript
<template>
<div>
<h2>WebSocket 使用示例</h2>
<!-- 显示当前连接状态 -->
<p>连接状态: {{ connectionStatus }}</p>
<!-- 按钮触发发送消息 -->
<button @click="sendMessage">发送消息</button>
<!-- 显示从服务器接收到的消息 -->
<p><strong>从服务器接收到的消息:</strong> {{ receivedMessage }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 设置 WebSocket 相关状态
const connectionStatus = ref('未连接')
const receivedMessage = ref('等待服务器消息...')
// 创建 WebSocket 实例,连接到 WebSocket 服务器
// 确保 WebSocket 服务器已经在 8080 端口上运行
const socket = new WebSocket('ws://localhost:8080')
// 监听 WebSocket 连接打开事件
socket.onopen = () => {
console.log('WebSocket 连接已建立')
connectionStatus.value = '连接已建立'
socket.send('客户端已连接,发送初始化消息')
};
// 监听 WebSocket 接收到消息事件
socket.onmessage = (event) => {
console.log('从服务器接收到消息:', event.data)
receivedMessage.value = event.data // 将收到的消息存储到响应式变量中
};
// 监听 WebSocket 关闭事件
socket.onclose = () => {
console.log('WebSocket 连接已关闭')
connectionStatus.value = '连接已关闭'
};
// 监听 WebSocket 错误事件
socket.onerror = (error) => {
console.log('WebSocket 出现错误:', error)
connectionStatus.value = '连接错误'
};
// 发送消息的函数
function sendMessage() {
if (socket.readyState === WebSocket.OPEN) {
const message = '这是客户端发送的消息'
socket.send(message)
console.log('已发送消息:', message)
} else {
console.log('WebSocket 连接未打开,无法发送消息')
}
}
</script>
<style scoped>
h2 {
color: #42b983;
}
button {
margin-top: 10px;
padding: 10px;
background-color: #42b983;
color: white;
border: none;
cursor: pointer
}
button:hover {
background-color: #36a372;
}
</style>
4.2 运行 WebSocket 服务器
以下是一个简单的 WebSocket 服务器,使用 Node.js 和 ws 库
1)安装 ws 包
javascript
npm install ws
2)WebSocket 服务器代码
javascript
// server.js
const WebSocket = require('ws')
// 创建 WebSocket 服务器,监听 8080 端口
const wss = new WebSocket.Server({ port: 8080 })
// 当有客户端连接时触发
wss.on('connection', (ws) => {
console.log('客户端已连接')
// 向客户端发送一条消息
ws.send('欢迎连接 WebSocket 服务器!')
// 监听客户端发送的消息
ws.on('message', (message) => {
console.log('收到来自客户端的消息:', message)
// 将收到的消息原封不动地返回给客户端
ws.send(`服务器已收到: ${message}`)
});
// 处理连接关闭
ws.on('close', () => {
console.log('客户端已断开连接')
});
});
console.log('WebSocket 服务器正在监听端口 8080...')
3)启动 WebSocket 服务器
在 Node.js 项目对应的目录中,运行以下命令来启动服务器,xxx是该 js 文件名
javascript
node xxx.js
4.3 运行 Vue 项目
运行 Vue 项目后,在浏览器中打开 Vue 页面后,大致连接流程如下:
1)WebSocket 连接的状态:未连接 → 连接已建立
2)点击按钮可以发送消息到 WebSocket 服务器
3)从 WebSocket 服务器接收到的响应消息,展示到页面中
5 优缺点及适用场景
优点:
- 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实时性非常高,适用于实时聊天、在线协作等应用
- 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接
- 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用
- 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用
- 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信
缺点:
- 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担
- 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适
- 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面
适用场景:
- 实时聊天应用: WebSocket 是实现实时聊天室、即时通讯应用的理想选择,因为它能够提供低延迟和高实时性
- 在线协作和协同编辑: 对于需要多用户协同工作的应用,如协同编辑文档或绘图,WebSocket 的实时性使得用户能够看到其他用户的操作
- 实时数据展示: 对于需要实时展示数据变化的应用,例如股票行情、实时监控系统等,WebSocket 提供了一种高效的通信方式
- 在线游戏: 在线游戏通常需要快速、实时的通信,WebSocket 能够提供低延迟和高并发的通信能力
- 推送服务: 用于实现消息推送服务,向客户端主动推送更新或通知