目录
[什么是 WebSocket](#什么是 WebSocket)
[WebSocket 报文格式](#WebSocket 报文格式)
[WebSocket 的握手过程](#WebSocket 的握手过程)
[客户端发送 WebSocket 握手请求](#客户端发送 WebSocket 握手请求)
[服务器回应 WebSocket 握手响应](#服务器回应 WebSocket 握手响应)
[配置 WebSocket](#配置 WebSocket)
什么是 WebSocket
Websocket 是一种 协议 ,用于在 客户端 和 服务器 之间建立持久的 双向通信连接 ,广泛应用于需要实时数据交换 的应用程序,例如在线聊天、实时游戏、股票行情等。是从 HTML5 开始支持的一种网页端和服务端保持长连接的 消息推送机制
WebSocket 协议 与 HTTP 协议有什么区别呢?
在 HTTP详解_httip-CSDN博客 中,我们学习了HTTP 协议
而 HTTP 协议通常使用在 "一问一答" 的应用场景中:
客户端向服务器发送一个 HTTP 请求,服务器给客户端返回一个 HTTP 响应:
![](https://i-blog.csdnimg.cn/direct/b5e7889fc4704cf28a936110fa475e43.jpeg)
在上述情况中,服务器属于被动的一方,若客户端不主动发起请求,服务器就无法主动给客户端响应
那,若需要服务器主动给客户端发送消息,该如何实现呢?
此时就需要消息推送机制
什么是消息推送机制呢?
消息推送 (Push Notification)是一种通信机制 ,允许服务器向客户端主动发送消息或通知,而无需客户端的请求。消息推送使得服务器可以在有重要信息或事件发生时,主动将消息推送给用户
例如,两个用户在进行在线聊天:
![](https://i-blog.csdnimg.cn/direct/4ea292fa9e6548fab96bed090e6e2e71.jpeg)
当 user1 发送消息时,user1 的客户端就需要给服务器发送消息,告诉服务器需要将消息发送给 user2,此时,服务器就需要立即将消息转发给 user2,及时将消息同步到 user2 的客户端
此时,若基于 HTTP 来实现上述消息推送效果,就需要基于 **"轮询"**的机制
也就是说 user2 的客户端需要不停地向服务器发送请求
![](https://i-blog.csdnimg.cn/direct/6fa6a28ef2ee47cab096f4b40d8375d3.jpeg)
由于 user2 并不知道 user1 什么时候发送消息,因此,只能不停的向服务器发送请求,直到 user1 发送消息,user2 才能获取到消息
显而易见,上述的 轮询 操作,开销较大,成本也较高
若轮询间隔时间较长,user1 发送消息时,user2 不能及时获取到消息,及时性较差
若轮询间隔时间较短,此时及时性得到了提升,但 user2 会浪费更多的机器资源(如带宽)
因此,消息推送机制 更适合实现服务器主动给客户端发送消息的场景,而 WebSocket是实现消息推送的主要方式
接下来,我们就来进一步学习 WebSocket 的报文格式
WebSocket 报文格式
WebSocket 协议是一个 应用层协议 ,其下层是基于TCP 协议的
报文格式
![](https://i-blog.csdnimg.cn/direct/5f7c1461348a41eab51bf45ba59df881.png)
WebSocket报文主要由以下几个部分组成:
1. 标识字段(FIN, RSV1, RSV2, RSV3)
2. 操作码(Opcode)
3. 掩码标识符(Mask)
4. 数据长度(Payload length)
5. 扩展数据(Extension data)
6. Masking-key
6. 负载数据(Payload data)
标识字段
FIN, RSV1, RSV2, RSV3:
FIN: 该帧是否为消息的最后一帧(1 表示是,0 表示不是)
RSV1, RSV2, RSV3:是保留字段,协议规定必须为 0(除非协议扩展时使用)
操作码
操作码(Opcode):表示数据帧的类型
操作码 | 含义 |
---|---|
0x0 | 继续帧(Continuation Frame),表示本次数据传输采用了数据分片,当前收到的帧为其中一个分片 |
0x1 | 文本帧(Text Frame) |
0x2 | 二进制帧(Binary Frame) |
0x3-0x7 | 保留,暂未使用 |
0x8 | 连接关闭帧(Close Frame) |
0x9 | Ping 帧(Ping Frame) |
0xA | Pong 帧(Pong Frame) |
0xB-0xF | 保留,暂未使用 |
文本帧(opcode 0x1):用于发送 UTF-8 编码的文本数据
二进制帧(opcode 0x2):用于发送二进制数据
继续帧(opcode 0x0):用于多帧消息传输中的数据分片
控制帧(opcode 0x8, 0x9, 0xA):用于连接关闭、心跳检测等
掩码标识符
掩码(Mask):标识是否对数据进行掩码处理
Mask = 1:表示客户端对负载数据进行了掩码,客户端发送时必须设置为 1
Mask = 0:表示没有掩码(服务器端不会使用掩码)
数据长度
数据长度(Payload length):表示数据负载的长度
数据长度字段指示负载数据的长度,长度的表示方式根据实际数据的大小分为三种:
0-125 字节:如果数据长度小于等于 125 字节,直接在 Payload Length 字段中表示该长度
126 字节 :如果数据长度大于 125 字节,Payload Length 字段的值为 126,此时紧随其后会有 2 字节表示真实的长度
127 字节 :如果数据长度大于 65535 字节,Payload Length 字段的值为 127,此时紧随其后会有 8字节表示真实的长度
扩展数据
扩展数据(Extension data):可选字段
默认情况下,这部分数据为空,只有在 WebSocket 协议扩展时才会有内容
Masking-key
Masking-key:0 或 4 字节(32位)
所有从客户端传送到服务端的数据帧,数据载荷部分都进行了掩码操作,Mask 为 1,且携带 4 字节的 Masking-key,若 Mask 为 0,则没有 Masking-key
负载数据
负载数据(Payload data):实际传输的数据
其长度由 Payload length指定,内容可以是文本、二进制数据或其他类型的数据,具体取决于帧的操作码
在学习了WebSocket 的报文格式 后,我们继续学习WebSocket 的握手过程,也就是建立连接的过程
WebSocket 的握手过程
WebSocket 的握手过程是建立 WebSocket 连接的关键步骤,使用 HTTP 协议进行初始化 ,但在成功建立连接 后,通信会切换到 WebSocket协议,整个握手过程包括:客户端向服务器发起 HTTP 请求,服务器响应后,连接被升级为 WebSocket 连接
步骤:
客户端发送 WebSocket 握手请求(HTTP 请求)
服务器回应 WebSocket 握手响应(HTTP 响应)
连接升级
客户端发送 WebSocket 握手请求
首先,客户端会尝试与服务器建立 WebSocket 连接,使用HTTP 请求 向服务器发送 WebSocket 握手请求,在这个请求中会带有特殊的 header 信息,表示客户端希望建立 WebSocket 连接,并告知服务器它支持 WebSocket 协议
客户端请求:
![](https://i-blog.csdnimg.cn/direct/2f9098e98cb54681b656ba84fd98b9ba.png)
Upgrade: websocket::请求将协议升级为 WebSocket
**Connection: Upgrade:**表示希望将当前 HTTP 连接升级为 WebSocket 连接
**Sec-WebSocket-Key:**一串随机生成的 Base64 编码的字符串,用于安全验证
**Sec-WebSocket-Version:**WebSocket 协议的版本,通常是 13(最新版本)
服务器回应 WebSocket 握手响应
若服务器支持 WebSocket 协议,并且准备好升级连接,服务器会返回一个 HTTP 101 状态码响应,表示协议切换成功。响应中会包含 WebSocket 协议相关信息
服务器响应头:
![](https://i-blog.csdnimg.cn/direct/49254c07f245433fb120914690ff6955.png)
HTTP/1.1 101: 表示服务器同意进行协议切换
Upgrade: websocket: 表示服务器同意切换到 WebSocket 协议
Connection: Upgrade: 表示服务器同意升级连接
Sec-WebSocket-Accept : 服务器返回的一个字符串,是对客户端 Sec-WebSocket-Key 的响应,服务器会用 Sec-WebSocket-Key和一个固定的 GUID 拼接后进行 SHA-1 哈希计算,再将结果进行 Base64 编码得到这个值
连接升级
当客户端收到服务器返回的 Sec-WebSocket-Accept 时,客户端就知道连接升级成功,WebSocket 连接已经建立。此时,HTTP 协议转化为 WebSocket 协议,连接升级完成,双方可以通过 WebSocket 协议进行双向通信
总而言之,WebSocket 握手实际上是一个基于 HTTP 的协议升级的过程:客户端发起 HTTP 请求,服务器确认并返回一个 HTTP 响应,协议成功切换为 WebSocket
在握手过程中,Sec-WebSocket-Key 和 Sec-WebSocket-Accept的配对是 WebSocket 握手是否成功的重要验证机制,确保连接的安全性
当握手成功后,WebSocket 连接将保持打开状态,双方可以在连接上进行任意的数据交换,直到一方主动关闭连接
在了解了相关知识后,接下来,我们就可以实现 WebSocket 相关代码了
代码实现示例
服务器代码
由于 Spring 中内置了 WebSocket,因此可以直接使用
引入依赖
在创建项目时添加:
![](https://i-blog.csdnimg.cn/direct/4ee10364683c4e718c40551c4d1b7d6a.png)
或在 pom 文件中添加:
XML
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>3.3.4</version>
</dependency>
代码编写
实现一个类并继承 TextWebSocketHandler类:
java
@Component
public class TestWebSocketHandler extends TextWebSocketHandler {
public TestWebSocketHandler () {
}
}
重写 TextWebSocketHandler中的主要方法:
![](https://i-blog.csdnimg.cn/direct/c861d691b9d1413092608b1e6660f4f7.png)
afterConnectionEstablished() :该方法在 WebSocket 连接成功建立后会被调用 ,可以在方法中进行初始化操作,如 进行身份验证
handleTextMessage() :该方法是 TextWebSocketHandler 中最重要的方法,用于处理接收到的文本消息,对该方法进行重写,以在接收到客户端发送的文本消息时执行特定逻辑
handleTransportError() :当 WebSocket 连接出现错误时,该方法会被调用,可以在方法中进行错误处理,如 关闭连接 或 记录错误日志
afterConnectionClosed() :该方法在 WebSocket 连接关闭时会被调用 ,可以在方法中做一些清理工作,如 释放资源
java
@Component
public class TestWebSocketHandler extends TextWebSocketHandler {
public TestWebSocketHandler() {
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("WebSocket 连接建立成功");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 对接收到的消息进行业务逻辑处理
System.out.println("接收到消息: " + message.toString());
// 将消息发送给客户端
session.sendMessage(message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.out.println("WebSocket 连接异常");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("WebSocket 连接关闭");
}
}
我们来进一步了解一下 TextWebSocketHandler类
TextWebSocketHandler
TextWebSocketHandler 是 Spring WebSocket 模块中的一个类,用于处理 WebSocket 连接中的文本消息 ,继承自 WebSocketHandler接口,专门处理文本消息,并提供了一些方法来处理连接、消息传输、断开等 WebSocket 相关事件
TextWebSocketHandler 中的主要方法:
handleTextMessage(WebSocketSession session, TextMessage message)
作用:
当客户端发送文本消息时,调用该方法处理接收到的文本消息
参数:
WebSocketSession session:当前 WebSocket 会话的上下文,提供了与客户端的连接信息
TextMessage message:客户端发送的文本消息,包含消息的内容(通过 message.getPayload() 方法获取)
afterConnectionEstablished(WebSocketSession session)
作用:
当 WebSocket 连接成功建立后,调用此方法,可以用来执行初始化操作,例如 记录日志、进行身份验证等
参数:
WebSocketSession session:当前的 WebSocket 会话信息
与 handleTextMessage 的区别:
afterConnectionEstablished 方法在 WebSocket 连接建立时调用 ,用于执行连接建立后的初始化任务;而 handleTextMessage 用于处理客户端发送的消息,用于实时交互的消息处理
afterConnectionClosed(WebSocketSession session, CloseStatus status)
作用:
当 WebSocket 连接被关闭时,调用此方法,可以用来进行资源清理、记录日志等相关操作
参数:
WebSocketSession session:当前的 WebSocket 会话信息
CloseStatus status:关闭连接的状态信息,包含关闭的原因等
handleTransportError(WebSocketSession session, Throwable exception)
作用:
当 WebSocket 连接出现错误时,调用此方法,可以用于错误处理、日志记录等相关操作
参数:
WebSocketSession session:当前的 WebSocket 会话信息
Throwable exception:发生的异常
supportsPartialMessages()
作用:
检查当前的 WebSocket 处理器是否支持接收部分消息(即,是否支持将大消息拆分成多个较小的部分)。通常情况下,如果不需要支持分片消息,则返回 false
返回值:
true:支持部分消息
false:不支持
TestWebSocketHandler只是用于处理 WebSocket 连接中的文本消息,而访问路径等相关信息,还需要进行相关配置
配置 WebSocket
TextWebSocketHandler 只是 WebSocket 处理的一个部分,要想使用 WebSocket,还需要配置 WebSocket 接口,注册 WebSocket 端点
创建 WebSocketConfig 类来对 WebSocket 进行相关配置,并实现 WebSocketConfigurer接口:
java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private TestWebSocketHandler testWebSocketHandler; // 注入实现的TestWebSocketHandler
/**
* 将 testWebSocketHandler 与 /test 路径进行绑定
* 当客户端访问 /test 路径时,就会触发执行相关方法
* @param registry 用于注册 WebSocket 端点和配置的对象
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(testWebSocketHandler, "/test");
}
}
WebSocketConfigurer:
是 Spring WebSocket 中用于配置 WebSocket 的接口 ,包含一个方法registerWebSocketHandlers(),用于注册 WebSocket 端点并设置相关配置。通过这个接口,可以将 WebSocket 处理器(如上述实现的 TestWebSocketHandler)与特定的 URL 端点进行绑定
@Configuration 注解:
是 Spring Framework 中的一种标识注解,标识当前类作为配置类,用于替代传统的 XML 配置文件
@EnableWebSocket 注解:
用于启动 Spring 的 WebSocket 功能,告诉 Spring 容器需要在应用中配置 WebSocket 服务,允许通过 Spring 配置类来管理 WebSocket 连接和消息的处理
配置完成后,服务器端代码就基本完成了,我们继续实现客户端代码
客户端代码
实现一个输入框,用于输入要发送的内容;一个按钮,用于提交消息:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test</title>
</head>
<body>
<input type="text" id="message">
<button id="submit">提交</button>
</body>
</html>
接下来,创建 WebSocket 实例,并挂载回调函数:
javascript
<script>
// 创建 WebSocket 连接
let websocket = new WebSocket("ws://127.0.0.1:8080/test"); // 与后端服务器路径相匹配
// 监听 WebSocket 事件
// 连接成功时触发
websocket.onopen = function() {
console.log("连接建立");
}
// 接收到服务器消息时触发
websocket.onmessage = function(e) {
console.log("接收到消息:" + e.data);
}
// 连接异常时触发
websocket.onerror = function() {
console.log("连接异常");
}
// 连接关闭时触发
websocket.onclose = function() {
console.log("连接关闭");
}
</script>
可以发现前端实现的回调函数,与后端实现的相关方法是相匹配的:
![](https://i-blog.csdnimg.cn/direct/ce2d4883ba924aff86312f5ec4726788.png)
分别用于:建立连接 、接收消息 、处理异常 和连接关闭
最后,实现点击事件:
javascript
// 点击按钮,通过 websocket 发送请求
let input = document.querySelector("#message"); // 输入的消息内容
let button = document.querySelector("#submit");
button.onclick = function() {
console.log("发送消息:" + input.value);
websocket.send(input.value) // 发送消息
}
完整实现:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test</title>
</head>
<body>
<input type="text" id="message">
<button id="submit">提交</button>
<script>
// 创建 WebSocket 连接
let websocket = new WebSocket("ws://127.0.0.1:8080/test"); // 与后端服务器路径相匹配
// 监听 WebSocket 事件
// 连接成功时触发
websocket.onopen = function() {
console.log("连接建立");
}
// 接收到服务器消息时触发
websocket.onmessage = function(e) {
console.log("接收到消息:" + e.data);
}
// 连接异常时触发
websocket.onerror = function() {
console.log("连接异常");
}
// 连接关闭时触发
websocket.onclose = function() {
console.log("连接关闭");
}
// 点击按钮,通过 websocket 发送请求
let input = document.querySelector("#message"); // 输入的消息内容
let button = document.querySelector("#submit");
button.onclick = function() {
console.log("发送消息:" + input.value);
websocket.send(input.value) // 发送消息
}
</script>
</body>
</html>
在实现了 服务器端代码 和 客户端代码 后,就可以运行程序,观察效果了
结果验证
运行程序,访问 http://127.0.0.1:8080/test.html :
![](https://i-blog.csdnimg.cn/direct/8946abdb1153456f8bb5c515aa26f9ca.png)
此时,WebSocket 连接建立成功
其中,产生报错信息的原因是客户端在尝试获取图标资源时获取失败,但其并不会影响我们程序的运行
输入数据并点击提交:
![](https://i-blog.csdnimg.cn/direct/ceb6f00f00a14edb82517c3dc598eeac.png)
此时消息成功发送给服务器,同时也接受到服务器发送的消息
观察服务器端日志:
若此时关闭页面,连接断开:
![](https://i-blog.csdnimg.cn/direct/f83e103828a040579bd260d9b3773bca.png)
若关闭服务端,则客户端显示连接关闭: