前后端 WebSocket

本文将介绍前后端持久通信协议 – WebSocket。

一、什么是 WebSocket ?

在项目开发中,我们常常会遇到 “即时通信” 的需求。

实现方式有四种:

  • 短轮询:
    • 客户端重复发送 HTTP 请求
    • 实时性较差(考虑网络资源消耗,两次请求之间必须留出一定时间)
  • 长轮询:
    • 客户端发送请求,持续等待
    • 服务端接收请求后,有消息则立即返回,没有消息则将请求挂起,待有通知后立即返回
    • 客户端接收请求响应后立即发起下一次请求
  • 长连接:
    • 通过 HTTP 1.1 中的 connection 字段进行长连接 (connection:keep-alive)
    • 在依次 TCP 连接中可以进行多次请求,但每个请求仍然要单独发 header
  • WebSocket:
    • 真双工通信
    • 多次请求中不需要携带 header

HTML5 定义了 WebSocket 协议,客户端和服务器只需要完成依次握手,两者之间就可以建立持久的连接,并进行双向数据传输。

二、简单示例

1. 建立 SpringBoot 项目

2. 引入依赖

修改 pom.xml,修改如下:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

3. 创建 WebSocket 配置类

1
2
3
4
5
6
7
8
9
@Configuration
public class WebSocketConfig {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}

}

4. 创建 WebSocket 服务类

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
@Component
@ServerEndpoint("/websocket")
public class WebSocketServer {

/**
* 静态变量,记录所有的连接
*/
private static final Map<String, WebSocketServer> connections = new LinkedHashMap<>();

/**
* 与当前连接对应的Session
*/
private Session session;

/**
* 连接成功的回调方法
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
connections.put(session.getId(), this);
}

/**
* 接收消息时的回调方法
*/
@OnMessage
public void onMessage(String message) {
try {
sendMessage(message);
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 发生错误时的回调方法
*/
@OnError
public void onError(Throwable error) throws Throwable {
throw error;
}

/**
* 连接关闭的回调方法
*/
@OnClose
public void onClose() {
connections.remove(session.getId());
}

/**
* 发送消息
*/
public void sendMessage(String message) throws IOException {
session.getBasicRemote().sendText(message);
}

/**
* 消息群发
*/
public static void broadcast(String message) {
connections.forEach((k, v) -> {
try {
v.sendMessage(message);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}

5. 增加群发接口

1
2
3
4
5
6
7
8
@RestController
public class BroadcastController {

@GetMapping("/broadcast")
public void broadcast(){
WebSocketServer.broadcast("发布通知");
}
}

6. 测试

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let socket = new WebSocket("ws://localhost:8080/websocket")

socket.onerror = err => {
console.log(err)
}

socket.onopen = event => {
console.log(event)
}

socket.onmessage = event => {
console.log(event)
}

socket.onclose = () => {
console.log("连接关闭")
}

socket.send("hello")

socket.close()

三、认证问题

在开发中,可能遇到已登录系统但进行 WebSocket 连接失败的错误。

这是因为 WebSocket 是基于 TCP 的一种独立实现,需要单独进行身份认证。

有两种解决方法:

  • 基于 cookie-session
  • 基于 JWT

以 JWT 为例,将连接 url 改为:

1
ws://localhost:8080/websocket?authorization=···

参考