服务器消息推送-WebSocket

在做 Web 系统时,经常会遇到一些需要服务端主动向客户端发送通知的场景,一般我们可以使用轮询来查询服务端信息,但是这样会对服务器造成较大的压力,对于以上问题,我们可以使用 WebSocket 或者 SSE 来解决

WebSocket:基于 TCP 的一种网络协议,它实现了浏览器与服务器全双工通信,而且数据格式比较轻量,性能开销小,不用频繁创建及销毁 TCP 请求

依赖

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

代码

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

@Bean
public ServerEndpointExporter getServerEndpointExporter() {
return new ServerEndpointExporter();
}
}
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
@Slf4j
@ServerEndpoint(value = "/websocket/{userId}")
@Component
public class WebSocketServer {

/**
* 建立连接
*
* @param userId
* @param session
*/
@OnOpen
public void onOpen(@PathParam("userId") Integer userId, Session session) {
WebSocketUtil.userCache.put(userId, session);
WebSocketUtil.sendMsg(userId, "连接成功");
log.info("有新连接加入,当前连接数为:" + WebSocketUtil.userCache.size());
}

/**
* 关闭连接
*
* @param userId
* @param session
*/
@OnClose
public void onClose(@PathParam("userId") Integer userId, Session session) {
try {
session.close();
} catch (IOException e) {
log.error("关闭连接异常", e);
}
WebSocketUtil.sendMsg(userId, "关闭连接");
WebSocketUtil.userCache.remove(userId);
log.info("有一个连接关闭,当前连接数为:" + WebSocketUtil.userCache.size());
}

/**
* 客户端发送消息
*
* @param userId
* @param msg
*/
@OnMessage
public void onMessage(@PathParam("userId") Integer userId, String msg) {
log.info("收到来自用户:[{}]的消息:{}", userId, msg);
}

/**
* 连接异常
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
try {
session.close();
} catch (IOException ignored) {
}
log.error("连接异常:{}", error.getMessage());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
public class WebSocketUtil {

public static final Map<Integer, Session> userCache = new ConcurrentHashMap<>();

public static void sendAllMsg(String msg) {
userCache.forEach((k, v) -> sendMsg(k, msg));
}

public static void sendMsg(Integer userId, String msg) {
log.info("发送消息:{},到用户:[{}]", msg, userId);
try {
userCache.get(userId).getBasicRemote().sendText(msg);
} catch (IOException e) {
log.error("发送消息失败:", e);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class WebSocketController {

@GetMapping("/send/{userId}")
public void send(@PathVariable Integer userId, @RequestParam String msg) {
WebSocketUtil.sendMsg(userId, msg);
}

@GetMapping("/send")
public void send(@RequestParam String msg) {
WebSocketUtil.sendAllMsg(msg);
}

@GetMapping("/onlineUser")
public String onlineUser() {
return WebSocketUtil.userCache.keySet().toString();
}
}
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>websocket测试</title>
<script type="text/javascript">
let ws;

function init() {
ws = new WebSocket("ws://localhost:8080/websocket/1");

ws.onopen = function () {
printMsg("onOpen");
};
ws.onmessage = function (e) {
printMsg("onMessage: " + e.data);
};
ws.onclose = function () {
printMsg("onClose");
};
ws.onerror = function () {
printMsg("onError");
};
}

function onSubmit() {
const input = document.getElementById("input");
ws.send(input.value);
printMsg("sendMessage: " + input.value);
input.value = "";
input.focus();
}

function onCloseClick() {
ws.close();
}

function printMsg(str) {
const log = document.getElementById("log");
log.innerHTML = str + "<br>" + log.innerHTML;
}
</script>
</head>

<body onload="init();">
<form onsubmit="onSubmit(); return false;">
<input type="text" id="input">
<input type="submit" value="send">
<button onclick="onCloseClick(); return false;">close</button>
</form>
<div id="log"></div>
</body>
</html>

测试

  1. 启动项目
  2. 复制 html 文件,socket1.html 和 socket2.html,修改各自的 userId(项目地址后面)
  3. 打开文件后服务器日志输出

image.png

  1. 页面输入文本后点击 send,服务器日志输出

image.png

  1. 指定用户发送消息:http://localhost:8080/send/1?msg=消息1

image.png

  1. 群发消息:http://localhost:8080/send?msg=消息222

image.png

image.png

  1. 查看在线用户:http://localhost:8080/onlineUser