在做 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 {
@OnOpen public void onOpen(@PathParam("userId") Integer userId, Session session) { WebSocketUtil.userCache.put(userId, session); WebSocketUtil.sendMsg(userId, "连接成功"); log.info("有新连接加入,当前连接数为:" + WebSocketUtil.userCache.size()); }
@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()); }
@OnMessage public void onMessage(@PathParam("userId") Integer userId, String msg) { log.info("收到来自用户:[{}]的消息:{}", userId, msg); }
@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>
|
测试
- 启动项目
- 复制 html 文件,socket1.html 和 socket2.html,修改各自的 userId(项目地址后面)
- 打开文件后服务器日志输出
- 页面输入文本后点击 send,服务器日志输出
- 指定用户发送消息:http://localhost:8080/send/1?msg=消息1
- 群发消息:http://localhost:8080/send?msg=消息222
- 查看在线用户:http://localhost:8080/onlineUser