一、消息推送的常用方式
相信大家都在网页上聊过天,就像下面这样。
当对面收到消息并回复的时候,这个消息会在没有刷新页面的情况下自动的弹出来,那这是如何做到的呢?我们知道前后端交互大部分场景都是使用的HTTP
协议进行的。前端发起请求,后端收到请求再返回所需数据,但是上面的场景是服务器主动推送给我们消息,似乎又跟这个交互模式相矛盾,那到底是怎么样进行通信的呢?
(一)轮询
浏览器以指定时间间隔向服务器发送HTTP
请求,服务器实时返回数据给浏览器。
显然这种方式不能够适用上面的场景,它不仅有延迟还很消耗资源。
(二)长论询
浏览器发出请求,服务器端接收到请求后会阻塞请求直到有数据或者超时才返回。
这种方式其实也不太合适上面的场景,假如你给你的女神发消息,结果女神第二天才回你消息,这不一值超时一直重发请求嘛。
(三)WebSocket
WebSocket
是一种在基于TCP
连接上进行全双工通信的协议,全双工就是允许数据在两个方向上同时传输数据。
没错,这就很适合上面的场景,网页聊天使用的就是 WebSocket
协议,或者说这种通信。那么浏览器与服务器是如何从 HTTP
协议切换到WebSocket
协议的呢?
握手阶段(Handshake):
- 客户端发起一个
HTTP
请求,这个请求包含一个特殊的头部字段Upgrade: websocket
,还有一些其他的WebSocket
相关的头部字段,例如Connection: Upgrade
和Sec-WebSocket-Key
等。 - 服务器收到这个带有特殊头部字段的请求后,如果支持
WebSocket
协议,就会进行协议升级。服务器返回的响应中包含了状态码101
(Switching Protocols),还包括一些与WebSocket
握手相关的头部字段,例如Upgrade: websocket
和Connection: Upgrade
。 - 一旦客户端收到带有状态码 101 的响应,说明握手成功,此时连接升级完成,浏览器与服务器之间的通信将升级到
WebSocket
协议。
请求:
http
GET xxxxxxxx
Host:xxxxxx
Connection:Upgrade
Upgrade:websocket
Sec-WebSocket-Version:xx
Sec-WebSocket-Key:xxx
xxxxxxxxxxx
响应:
http
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade:websocket
Sec-WebSocket-Accept:xx
xxxxxxxxxxx
WebSocket 通信阶段:
- 一旦握手成功,浏览器和服务器之间的通信就切换到了
WebSocket
协议。此时,双方可以通过WebSocket
协议进行实时双向通信,而不再依赖于传统的请求-响应模型。
二、WebSocket 的协议格式
学习一个协议,那得先了解它的协议格式。WebSocket
的协议格式在RFC 6455: The WebSocket Protocol (rfc-editor.org) 文档中有详细的阐述(第5.2章节),这里我截取了里面的一部分,值得注意的是WebSocket
是应用层协议,不是传输层协议。
-
FIN(1 bit) :表示是否要关闭
websocket
-
RSV1、2、3(每个 1 bit):保留位
-
opcode(4 bit) :描述当前这个
websocket
数据帧是什么类型- %x1:文本数据
- %x2:二进制
-
MASK(1 bit):是否开启掩码操作。
-
payload length(7 bits, 7+16 bits, or 7+64 bits) :载荷长度,也就是数据报上要携带的具体数据的大小。当
payload length
< 126,此时是模式 1(7bit);如果 7 bit 的值是 126,此时是模式 2(7+16 bits)。如果 7 个 bit 的值是 127,此时是模式 3(7+64 bits)。 -
Masking-key: 0 or 4 bytes,如果屏蔽位设置为0(也就是 MASK 为0),则此字段不存在。这个字段用于对从客户端发送到服务器或从服务器发送到客户端的数据进行掩码处理,其目的是在数据传输过程中增加一定的安全性。
-
Payload Data:存储了实际的数据信息。
二、WebSocket API
(一)客户端 API
- 创建
websocket
对象(内置的)
java
let websocket = new WebSocket(ws://ip地址/访问路径);
- 事件处理方法
事件 | 方法名称 | 描述 |
---|---|---|
连接建立 | onopen |
当 WebSocket 连接成功建立时触发。 |
消息接收 | onmessage |
当接收到来自服务器的消息时触发。 |
连接关闭 | onclose |
当 WebSocket 连接关闭时触发。 |
错误发生 | onerror |
当 WebSocket 连接发生错误时触发。 |
示例:
html
<script>
let ws = new WebSocket("ws://localhost:8080/xxxx");
//当 WebSocket 连接成功建立时触发。
ws.onopen = function(){
//发送数据
ws.send(data);
}
//当接收到来自服务器的消息时触发,res.data 是服务器返回的消息
ws.onmessage = function(res){
}
//当 WebSocket 连接关闭时触发。
ws.onclose = function(){
}
//当 WebSocket 连接发生错误时触发。
ws.onerror = function(event){
}
</script>
- 发送数据
send(data)
: 将数据发送到服务器。
(二)服务端 API
实现WebSocket
的方式有很多种,比如原生jdk
注解、Spring
封装等,本篇演示的是Spring
封装的用法。jdk
注解式可以参考:在 Spring Boot 中整合、使用 WebSocket - spring 中文网 (springdoc.cn)
演示如下👇
三、WebSocket 演示
(一)服务端
- 创建一个
SpringBoot
项目。 - 引入如下依赖。
xml
<!-- WebSocket API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 写一个
demo
类继承TextWebSocketHandler
类,并重写其中一些方法。TextWebSocketHandler
是一个具体的抽象类,它实现了WebSocketHandler
接口,并专门用于处理文本消息(Text Messages)。WebSocket协议允许在客户端和服务器之间传输文本消息,而TextWebSocketHandler
封装了处理这些文本消息的逻辑。通过继承TextWebSocketHandler
,你可以专注于处理文本消息的逻辑,而无需实现整个WebSocketHandler
接口的所有方法。
java
package com.example.websocket.component;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
@Component
public class TestWebSocket extends TextWebSocketHandler {
/**
*
* @param session WebSocketSession的会话
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//在 websocket 连接建立成功后,被自动调用
System.out.println("TestWebSocket连接成功!");
}
/**
*
* @param session WebSocketSession的会话
* @param message 收到的消息
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//这个方法是在 websocket 收到消息的时候,被自动调用的
System.out.println("TestWebSocket收到消息:" + message.toString());
//session是一个会话,里面就记录了通信双方是谁。
//发送给客户端
session.sendMessage(message);
}
/**
*
* @param session WebSocketSession的会话
* @param exception 对象记录的异常信息
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
//连接出现异常的时候,被自动调用
System.out.println("TestWebSocket连接异常!");
}
/**
*
* @param session WebSocketSession的会话
* @param status 关闭的状态
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
//连接关闭后自动调用
System.out.println("TestWebSocket关闭连接!");
}
}
- 配置路由信息,创建一个配置类来实现
WebSocketConfigurer
接口。
java
package com.example.websocket.config;
import com.example.websocket.component.TestWebSocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket //启动 WebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private TestWebSocket testWebSocket;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//通过这个方法,把刚才创建好的 Handler 类注册到具体的路径上
//当浏览器 websocket 中的请求路径是"/test" 的时候,就会调用到 TestWebSocket 里的方法。
registry.addHandler(testWebSocket,"/test");
}
}
(二)客户端
自己手写一个简单的html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" id="message">
<button id="send-button">发送消息</button>
<script>
let websocket = new WebSocket("ws://localhost:8080/test");
//当 WebSocket 连接成功建立时触发。
websocket.onopen = function () {
//发送数据
console.log("websocket连接成功!");
}
//当 接收到来自服务器的消息时触发,res.data 是服务器返回的消息
websocket.onmessage = function (res) {
console.log("websocket收到消息!" + res.data);
}
//当 WebSocket 连接关闭时触发。
websocket.onclose = function () {
console.log("websocket连接断开!");
}
//当 WebSocket 连接发生错误时触发。
websocket.onerror = function (event) {
console.log("websocket连接异常!");
}
//发送消息
let sendButton = document.querySelector('#send-button');
let message = document.querySelector('#message');
sendButton.onclick = function(){
console.log("websocket发送消息"+message.value);
websocket.send(message.value);
}
</script>
</body>
</html>
启动项目,并访问:
成功!
(三)WebSocketSession 与 TextMessage
WebSocketSession
通常包含有关连接的信息,例如连接的 ID、协议版本、URI、和其他与连接相关的属性。通过 WebSocketSession
,我们可以在服务器端处理 WebSocket
连接,接收来自客户端的消息,发送消息到客户端,关闭连接等操作。
下面是常用的方法:
方法名 | 描述 |
---|---|
String getId() |
获取 WebSocket 连接的唯一标识符。 |
URI getUri() |
获取 WebSocket 连接的 URI。 |
boolean isOpen() |
检查 WebSocket 连接是否打开。 |
void sendMessage(WebSocketMessage<?> message) |
发送 WebSocket 消息到客户端。 |
void close() |
关闭 WebSocket 连接。 |
void setTextMessageSizeLimit(int messageSizeLimit) |
设置文本消息的大小限制。 |
int getTextMessageSizeLimit() |
获取文本消息的大小限制。 |
void close(CloseStatus status) |
使用指定的关闭状态关闭 WebSocket 连接。 |
TextMessage
用于表示文本消息的类,它是 WebSocketMessage
接口的一个实现,用于在 WebSocket
通信中传递文本数据。
方法名 | 描述 |
---|---|
String getPayload() |
获取文本消息的内容。 |
byte[] asBytes() |
将文本消息的内容转换为字节数组。 |
String toString() |
返回 TextMessage 对象的字符串表示。 |
(四)实现群发
如何实现群发呢?或者说如何让服务器群发给所有连接的客户端?其实只需要将所有的websocketsession
存起来,下面就给一个简单的demo
,这里演示的是Map
。如果客户端比较多,可以使用 Redis
来存。下面是一个工具类,用来操作Map
java
public class WebSocketMap {
//用来存储会话信息
public static final Map<Integer,WebSocketSession> map = new ConcurrentHashMap<>();
/**
* 添加会话
*/
public static WebSocketSession put(Integer id, WebSocketSession session){
return map.put(id,session);
}
/**
* 删除会话,并返回删除的会话,但是没有断开连接!
*/
public static WebSocketSession remove(Integer id){
return map.remove(id);
}
/**
* 删除并断开连接
* @param id
*/
public static void removeAndClose(Integer id){
//在缓存中删除会话
WebSocketSession webSocketSession = remove(id);
//保证会话是连接的,再进行删除
if(webSocketSession != null && webSocketSession.isOpen()){
try {
//断开连接
webSocketSession.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取会话
*/
public static WebSocketSession get(Integer id){
return map.get(id);
}
/**
* 给指定用户发送消息
*/
public static void send(Integer id,String msg) throws IOException {
//获取指定用户
WebSocketSession session = map.get(id);
if(session != null && session.isOpen()){
//发送消息
session.sendMessage(new TextMessage(msg));
}else {
//处理错误
System.out.println("session为null或者连接已断开");
}
}
/**
* 群发功能
*/
public static void sendAll(String msg) throws IOException {
//遍历每一个会话
for(WebSocketSession session : map.values()){
if(session != null && session.isOpen()){
session.sendMessage(new TextMessage(msg));
}
}
}
}
(五)获取 HTTP 的 HttpSession 数据
升级为WebSocket
协议后,不能直接获取 HTTP
的Session
了,但是可以利用WebSocketSession
来获取HttpSession
。WebSocketSession
对象提供了一个getAttributes()
方法,该方法返回一个Map
,这个Map
就是HttpSession
存储的键值对。
java
@Component
public class TestWebSocket extends TextWebSocketHandler {
//......................
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//这个方法是在 websocket 收到消息的时候,被自动调用的
//getAttributes() 的返回值是 Map,这个Map就是 HttpSession 装的键值对
Map<String, Object> map = session.getAttributes();
//假如在前面的登录业务中已经存了"user"这个session,这样就得到了 HttpSession 中的 User 对象。
User user = (User) map.get("user");
}
//......................
}