From 0806fbce26f8056bd5fb740c1009a2ce3179680a Mon Sep 17 00:00:00 2001
From: YunaiV <>
Date: Thu, 21 Nov 2019 00:56:45 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20spring=20websocket=20?=
=?UTF-8?q?=E7=A4=BA=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lab-25/lab-websocket-25-01/pom.xml | 2 +-
lab-25/lab-websocket-25-02/pom.xml | 31 ++++
.../lab25/springwebsocket/Application.java | 13 ++
.../config/WebSocketConfiguration.java | 16 ++
.../handler/AuthMessageHandler.java | 40 +++++
.../handler/MessageHandler.java | 25 +++
.../handler/SendToAllHandler.java | 32 ++++
.../handler/SendToOneHandler.java | 32 ++++
.../springwebsocket/message/AuthRequest.java | 24 +++
.../springwebsocket/message/AuthResponse.java | 37 +++++
.../springwebsocket/message/Message.java | 7 +
.../springwebsocket/message/SendResponse.java | 50 ++++++
.../message/SendToAllRequest.java | 37 +++++
.../message/SendToOneRequest.java | 49 ++++++
.../message/SendToUserRequest.java | 37 +++++
.../message/UserJoinNoticeRequest.java | 24 +++
.../springwebsocket/util/WebSocketUtil.java | 152 ++++++++++++++++++
.../websocket/WebsocketServerEndpoint.java | 134 +++++++++++++++
lab-25/pom.xml | 1 +
19 files changed, 742 insertions(+), 1 deletion(-)
create mode 100644 lab-25/lab-websocket-25-02/pom.xml
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/Application.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/config/WebSocketConfiguration.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/AuthMessageHandler.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/MessageHandler.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/SendToAllHandler.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/SendToOneHandler.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/AuthRequest.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/AuthResponse.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/Message.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendResponse.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToAllRequest.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToOneRequest.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToUserRequest.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/UserJoinNoticeRequest.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/util/WebSocketUtil.java
create mode 100644 lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/websocket/WebsocketServerEndpoint.java
diff --git a/lab-25/lab-websocket-25-01/pom.xml b/lab-25/lab-websocket-25-01/pom.xml
index 02682bae..3e105363 100644
--- a/lab-25/lab-websocket-25-01/pom.xml
+++ b/lab-25/lab-websocket-25-01/pom.xml
@@ -10,7 +10,7 @@
4.0.0
- lab-25-01
+ lab-websocket-25-01
diff --git a/lab-25/lab-websocket-25-02/pom.xml b/lab-25/lab-websocket-25-02/pom.xml
new file mode 100644
index 00000000..832a7663
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/pom.xml
@@ -0,0 +1,31 @@
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.1.10.RELEASE
+
+
+ 4.0.0
+
+ lab-websocket-25-02
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+
+
+ com.alibaba
+ fastjson
+ 1.2.62
+
+
+
+
+
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/Application.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/Application.java
new file mode 100644
index 00000000..a6f84bbf
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/Application.java
@@ -0,0 +1,13 @@
+package cn.iocoder.springboot.lab25.springwebsocket;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/config/WebSocketConfiguration.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/config/WebSocketConfiguration.java
new file mode 100644
index 00000000..47f36c44
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/config/WebSocketConfiguration.java
@@ -0,0 +1,16 @@
+package cn.iocoder.springboot.lab25.springwebsocket.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+@Configuration
+// @EnableWebSocket // 无需添加该注解,因为我们并不是使用 Spring WebSocket
+public class WebSocketConfiguration {
+
+ @Bean
+ public ServerEndpointExporter serverEndpointExporter() {
+ return new ServerEndpointExporter();
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/AuthMessageHandler.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/AuthMessageHandler.java
new file mode 100644
index 00000000..6d93efd4
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/AuthMessageHandler.java
@@ -0,0 +1,40 @@
+package cn.iocoder.springboot.lab25.springwebsocket.handler;
+
+import cn.iocoder.springboot.lab25.springwebsocket.message.AuthRequest;
+import cn.iocoder.springboot.lab25.springwebsocket.message.AuthResponse;
+import cn.iocoder.springboot.lab25.springwebsocket.message.UserJoinNoticeRequest;
+import cn.iocoder.springboot.lab25.springwebsocket.util.WebSocketUtil;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import javax.websocket.Session;
+
+@Component
+public class AuthMessageHandler implements MessageHandler {
+
+ @Override
+ public void execute(Session session, AuthRequest message) {
+ // 如果未传递 accessToken
+ if (StringUtils.isEmpty(message.getAccessToken())) {
+ WebSocketUtil.send(session, AuthResponse.TYPE,
+ new AuthResponse().setCode(1).setMessage("认证 accessToken 未传入"));
+ return;
+ }
+
+ // 添加到 WebSocketUtil 中
+ WebSocketUtil.addSession(session, message.getAccessToken()); // 考虑到代码简化,我们先直接使用 accessToken 作为 User
+
+ // 判断是否认证成功。这里,假装直接成功
+ WebSocketUtil.send(session, AuthResponse.TYPE, new AuthResponse().setCode(0));
+
+ // 通知所有人,某个人加入了。这个是可选逻辑,仅仅是为了演示
+ WebSocketUtil.broadcast(UserJoinNoticeRequest.TYPE,
+ new UserJoinNoticeRequest().setNickname(message.getAccessToken())); // 考虑到代码简化,我们先直接使用 accessToken 作为 User
+ }
+
+ @Override
+ public String getType() {
+ return AuthRequest.TYPE;
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/MessageHandler.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/MessageHandler.java
new file mode 100644
index 00000000..aa89c13c
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/MessageHandler.java
@@ -0,0 +1,25 @@
+package cn.iocoder.springboot.lab25.springwebsocket.handler;
+
+import cn.iocoder.springboot.lab25.springwebsocket.message.Message;
+
+import javax.websocket.Session;
+
+/**
+ * 消息处理器接口
+ */
+public interface MessageHandler {
+
+ /**
+ * 执行处理消息
+ *
+ * @param session 会话
+ * @param message 消息
+ */
+ void execute(Session session, T message);
+
+ /**
+ * @return 消息类型,即每个 Message 实现类上的 TYPE 静态字段
+ */
+ String getType();
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/SendToAllHandler.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/SendToAllHandler.java
new file mode 100644
index 00000000..cda1efac
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/SendToAllHandler.java
@@ -0,0 +1,32 @@
+package cn.iocoder.springboot.lab25.springwebsocket.handler;
+
+import cn.iocoder.springboot.lab25.springwebsocket.message.SendResponse;
+import cn.iocoder.springboot.lab25.springwebsocket.message.SendToAllRequest;
+import cn.iocoder.springboot.lab25.springwebsocket.message.SendToUserRequest;
+import cn.iocoder.springboot.lab25.springwebsocket.util.WebSocketUtil;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.Session;
+
+@Component
+public class SendToAllHandler implements MessageHandler {
+
+ @Override
+ public void execute(Session session, SendToAllRequest message) {
+ // 这里,假装直接成功
+ SendResponse sendResponse = new SendResponse().setMsgId(message.getMsgId()).setCode(0);
+ WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);
+
+ // 创建转发的消息
+ SendToUserRequest sendToUserRequest = new SendToUserRequest().setMsgId(message.getMsgId())
+ .setContent(message.getContent());
+ // 广播发送
+ WebSocketUtil.broadcast(SendToUserRequest.TYPE, sendToUserRequest);
+ }
+
+ @Override
+ public String getType() {
+ return SendToAllRequest.TYPE;
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/SendToOneHandler.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/SendToOneHandler.java
new file mode 100644
index 00000000..d7437bd4
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/handler/SendToOneHandler.java
@@ -0,0 +1,32 @@
+package cn.iocoder.springboot.lab25.springwebsocket.handler;
+
+import cn.iocoder.springboot.lab25.springwebsocket.message.SendResponse;
+import cn.iocoder.springboot.lab25.springwebsocket.message.SendToOneRequest;
+import cn.iocoder.springboot.lab25.springwebsocket.message.SendToUserRequest;
+import cn.iocoder.springboot.lab25.springwebsocket.util.WebSocketUtil;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.Session;
+
+@Component
+public class SendToOneHandler implements MessageHandler {
+
+ @Override
+ public void execute(Session session, SendToOneRequest message) {
+ // 这里,假装直接成功
+ SendResponse sendResponse = new SendResponse().setMsgId(message.getMsgId()).setCode(0);
+ WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);
+
+ // 创建转发的消息
+ SendToUserRequest sendToUserRequest = new SendToUserRequest().setMsgId(message.getMsgId())
+ .setContent(message.getContent());
+ // 广播发送
+ WebSocketUtil.send(message.getToUser(), SendToUserRequest.TYPE, sendToUserRequest);
+ }
+
+ @Override
+ public String getType() {
+ return SendToOneRequest.TYPE;
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/AuthRequest.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/AuthRequest.java
new file mode 100644
index 00000000..29b8bf11
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/AuthRequest.java
@@ -0,0 +1,24 @@
+package cn.iocoder.springboot.lab25.springwebsocket.message;
+
+/**
+ * 用户认证请求
+ */
+public class AuthRequest implements Message {
+
+ public static final String TYPE = "AUTH_REQUEST";
+
+ /**
+ * 认证 Token
+ */
+ private String accessToken;
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public AuthRequest setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ return this;
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/AuthResponse.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/AuthResponse.java
new file mode 100644
index 00000000..cfa4fbda
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/AuthResponse.java
@@ -0,0 +1,37 @@
+package cn.iocoder.springboot.lab25.springwebsocket.message;
+
+/**
+ * 用户认证响应
+ */
+public class AuthResponse implements Message {
+
+ public static final String TYPE = "AUTH_RESPONSE";
+
+ /**
+ * 响应状态码
+ */
+ private Integer code;
+ /**
+ * 响应提示
+ */
+ private String message;
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public AuthResponse setCode(Integer code) {
+ this.code = code;
+ return this;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public AuthResponse setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/Message.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/Message.java
new file mode 100644
index 00000000..974b146a
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/Message.java
@@ -0,0 +1,7 @@
+package cn.iocoder.springboot.lab25.springwebsocket.message;
+
+/**
+ * 基础消息体
+ */
+public interface Message {
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendResponse.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendResponse.java
new file mode 100644
index 00000000..c695cf83
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendResponse.java
@@ -0,0 +1,50 @@
+package cn.iocoder.springboot.lab25.springwebsocket.message;
+
+/**
+ * 发送消息响应结果的 Message
+ */
+public class SendResponse implements Message {
+
+ public static final String TYPE = "SEND_RESPONSE";
+
+ /**
+ * 消息编号
+ */
+ private String msgId;
+ /**
+ * 响应状态码
+ */
+ private Integer code;
+ /**
+ * 响应提示
+ */
+ private String message;
+
+ public String getMsgId() {
+ return msgId;
+ }
+
+ public SendResponse setMsgId(String msgId) {
+ this.msgId = msgId;
+ return this;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public SendResponse setCode(Integer code) {
+ this.code = code;
+ return this;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public SendResponse setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToAllRequest.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToAllRequest.java
new file mode 100644
index 00000000..f75e0f9b
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToAllRequest.java
@@ -0,0 +1,37 @@
+package cn.iocoder.springboot.lab25.springwebsocket.message;
+
+/**
+ * 发送给所有人的群聊消息的 Message
+ */
+public class SendToAllRequest implements Message {
+
+ public static final String TYPE = "SEND_TO_ALL_REQUEST";
+
+ /**
+ * 消息编号
+ */
+ private String msgId;
+ /**
+ * 内容
+ */
+ private String content;
+
+ public String getContent() {
+ return content;
+ }
+
+ public SendToAllRequest setContent(String content) {
+ this.content = content;
+ return this;
+ }
+
+ public String getMsgId() {
+ return msgId;
+ }
+
+ public SendToAllRequest setMsgId(String msgId) {
+ this.msgId = msgId;
+ return this;
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToOneRequest.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToOneRequest.java
new file mode 100644
index 00000000..8daa74ca
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToOneRequest.java
@@ -0,0 +1,49 @@
+package cn.iocoder.springboot.lab25.springwebsocket.message;
+
+/**
+ * 发送给指定人的私聊消息的 Message
+ */
+public class SendToOneRequest implements Message {
+
+ public static final String TYPE = "SEND_TO_ONE_REQUEST";
+
+ /**
+ * 发送给的用户
+ */
+ private String toUser;
+ /**
+ * 消息编号
+ */
+ private String msgId;
+ /**
+ * 内容
+ */
+ private String content;
+
+ public String getToUser() {
+ return toUser;
+ }
+
+ public SendToOneRequest setToUser(String toUser) {
+ this.toUser = toUser;
+ return this;
+ }
+
+ public String getMsgId() {
+ return msgId;
+ }
+
+ public SendToOneRequest setMsgId(String msgId) {
+ this.msgId = msgId;
+ return this;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public SendToOneRequest setContent(String content) {
+ this.content = content;
+ return this;
+ }
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToUserRequest.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToUserRequest.java
new file mode 100644
index 00000000..67ad15c0
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/SendToUserRequest.java
@@ -0,0 +1,37 @@
+package cn.iocoder.springboot.lab25.springwebsocket.message;
+
+/**
+ * 发送消息给一个用户的 Message
+ */
+public class SendToUserRequest implements Message {
+
+ public static final String TYPE = "SEND_TO_USER_REQUEST";
+
+ /**
+ * 消息编号
+ */
+ private String msgId;
+ /**
+ * 内容
+ */
+ private String content;
+
+ public String getMsgId() {
+ return msgId;
+ }
+
+ public SendToUserRequest setMsgId(String msgId) {
+ this.msgId = msgId;
+ return this;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public SendToUserRequest setContent(String content) {
+ this.content = content;
+ return this;
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/UserJoinNoticeRequest.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/UserJoinNoticeRequest.java
new file mode 100644
index 00000000..e1e923af
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/message/UserJoinNoticeRequest.java
@@ -0,0 +1,24 @@
+package cn.iocoder.springboot.lab25.springwebsocket.message;
+
+/**
+ * 用户加入群聊的通知 Message
+ */
+public class UserJoinNoticeRequest implements Message {
+
+ public static final String TYPE = "USER_JOIN_NOTICE_REQUEST";
+
+ /**
+ * 昵称
+ */
+ private String nickname;
+
+ public String getNickname() {
+ return nickname;
+ }
+
+ public UserJoinNoticeRequest setNickname(String nickname) {
+ this.nickname = nickname;
+ return this;
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/util/WebSocketUtil.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/util/WebSocketUtil.java
new file mode 100644
index 00000000..d135a1f7
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/util/WebSocketUtil.java
@@ -0,0 +1,152 @@
+package cn.iocoder.springboot.lab25.springwebsocket.util;
+
+import cn.iocoder.springboot.lab25.springwebsocket.message.Message;
+import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.Session;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * WebSocket 工具类,提供客户端连接的管理等功能
+ */
+public class WebSocketUtil {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUtil.class);
+
+ // ========== 会话相关 ==========
+
+ /**
+ * Session 与用户的映射
+ */
+ private static final Map SESSION_USER_MAP = new ConcurrentHashMap<>();
+ /**
+ * 用户与 Session 的映射
+ */
+ private static final Map USER_SESSION_MAP = new ConcurrentHashMap<>();
+
+ /**
+ * 添加 Session 。在这个方法中,会添加用户和 Session 之间的映射
+ *
+ * @param session Session
+ * @param user 用户
+ */
+ public static void addSession(Session session, String user) {
+ // 更新 USER_SESSION_MAP
+ USER_SESSION_MAP.put(user, session);
+ // 更新 SESSION_USER_MAP
+ SESSION_USER_MAP.put(session, user);
+ }
+
+ /**
+ * 移除 Session 。
+ *
+ * @param session Session
+ */
+ public static void removeSession(Session session) {
+ // 从 SESSION_USER_MAP 中移除
+ String user = SESSION_USER_MAP.remove(session);
+ // 从 USER_SESSION_MAP 中移除
+ if (user != null && user.length() > 0) {
+ USER_SESSION_MAP.remove(user);
+ }
+ }
+
+ // ========== 消息相关 ==========
+
+ /**
+ * 广播发送消息给所有在线用户
+ *
+ * @param type 消息类型
+ * @param message 消息体
+ * @param 消息类型
+ */
+ public static void broadcast(String type, T message) {
+ // 创建消息
+ String messageText = buildTextMessage(type, message);
+ // 遍历 SESSION_USER_MAP ,进行逐个发送
+ for (Session session : SESSION_USER_MAP.keySet()) {
+ sendTextMessage(session, messageText);
+ }
+ }
+
+ /**
+ * 发送消息给单个用户的 Session
+ *
+ * @param session Session
+ * @param type 消息类型
+ * @param message 消息体
+ * @param 消息类型
+ */
+ public static void send(Session session, String type, T message) {
+ // 创建消息
+ String messageText = buildTextMessage(type, message);
+ // 遍历给单个 Session ,进行逐个发送
+ sendTextMessage(session, messageText);
+ }
+
+ /**
+ * 发送消息给指定用户
+ *
+ * @param user 指定用户
+ * @param type 消息类型
+ * @param message 消息体
+ * @param 消息类型
+ * @return 发送是否成功你那个
+ */
+ public static boolean send(String user, String type, T message) {
+ // 获得用户对应的 Session
+ Session session = USER_SESSION_MAP.get(user);
+ if (session == null) {
+ LOGGER.error("[send][user({}) 不存在对应的 session]", user);
+ return false;
+ }
+ // 发送消息
+ send(session, type, message);
+ return true;
+ }
+
+ /**
+ * 构建完整的消息
+ *
+ * @param type 消息类型
+ * @param message 消息体
+ * @param 消息类型
+ * @return 消息
+ */
+ private static String buildTextMessage(String type, T message) {
+ JSONObject messageObject = new JSONObject();
+ messageObject.put("type", type);
+ messageObject.put("body", message);
+ return messageObject.toString();
+ }
+
+ /**
+ * 真正发送消息
+ *
+ * @param session Session
+ * @param messageText 消息
+ */
+ private static void sendTextMessage(Session session, String messageText) {
+ if (session == null) {
+ LOGGER.error("[sendTextMessage][session 为 null]");
+ return;
+ }
+ RemoteEndpoint.Basic basic = session.getBasicRemote();
+ if (basic == null) {
+ LOGGER.error("[sendTextMessage][session 的 为 null]");
+ return;
+ }
+ try {
+ basic.sendText(messageText);
+ } catch (IOException e) {
+ LOGGER.error("[sendTextMessage][session({}) 发送消息{}) 发生异常",
+ session, messageText, e);
+ }
+ }
+
+}
diff --git a/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/websocket/WebsocketServerEndpoint.java b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/websocket/WebsocketServerEndpoint.java
new file mode 100644
index 00000000..dde62c33
--- /dev/null
+++ b/lab-25/lab-websocket-25-02/src/main/java/cn/iocoder/springboot/lab25/springwebsocket/websocket/WebsocketServerEndpoint.java
@@ -0,0 +1,134 @@
+package cn.iocoder.springboot.lab25.springwebsocket.websocket;
+
+import cn.iocoder.springboot.lab25.springwebsocket.handler.MessageHandler;
+import cn.iocoder.springboot.lab25.springwebsocket.message.AuthRequest;
+import cn.iocoder.springboot.lab25.springwebsocket.message.Message;
+import cn.iocoder.springboot.lab25.springwebsocket.util.WebSocketUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.aop.framework.AopProxyUtils;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Controller;
+import org.springframework.util.CollectionUtils;
+
+import javax.websocket.*;
+import javax.websocket.server.ServerEndpoint;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@Controller
+@ServerEndpoint("/")
+public class WebsocketServerEndpoint implements InitializingBean {
+
+ private Logger logger = LoggerFactory.getLogger(getClass());
+
+ /**
+ * 消息类型与 MessageHandler 的映射
+ *
+ * 注意,这里设置成静态变量。虽然说 WebsocketServerEndpoint 是单例,但是 Spring Boot 还是会为每个 WebSocket 创建一个 WebsocketServerEndpoint Bean 。
+ */
+ private static final Map HANDLERS = new HashMap<>();
+
+ @Autowired
+ private ApplicationContext applicationContext;
+
+ @OnOpen
+ public void onOpen(Session session, EndpointConfig config) {
+ logger.info("[onOpen][session({}) 接入]", session);
+ // 解析 accessToken
+ List accessTokenValues = session.getRequestParameterMap().get("accessToken");
+ String accessToken = !CollectionUtils.isEmpty(accessTokenValues) ? accessTokenValues.get(0) : null;
+ // 创建 AuthRequest 消息类型
+ AuthRequest authRequest = new AuthRequest().setAccessToken(accessToken);
+ // 获得消息处理器
+ MessageHandler messageHandler = HANDLERS.get(AuthRequest.TYPE);
+ if (messageHandler == null) {
+ logger.error("[onOpen][认证消息类型,不存在消息处理器]");
+ return;
+ }
+ messageHandler.execute(session, authRequest);
+ }
+
+ @OnMessage
+ public void onMessage(Session session, String message) {
+ logger.info("[onOpen][session({}) 接收到一条消息({})]", session, message); // 生产环境下,请设置成 debug 级别
+ try {
+ // 获得消息类型
+ JSONObject jsonMessage = JSON.parseObject(message);
+ String messageType = jsonMessage.getString("type");
+ // 获得消息处理器
+ MessageHandler messageHandler = HANDLERS.get(messageType);
+ if (messageHandler == null) {
+ logger.error("[onMessage][消息类型({}) 不存在消息处理器]", messageType);
+ return;
+ }
+ // 解析消息
+ Class extends Message> messageClass = this.getMessageClass(messageHandler);
+ // 处理消息
+ Message messageObj = JSON.parseObject(jsonMessage.getString("body"), messageClass);
+ messageHandler.execute(session, messageObj);
+ } catch (Throwable throwable) {
+ logger.info("[onMessage][session({}) message({}) 发生异常]", session, throwable);
+ }
+ }
+
+ @OnClose
+ public void onClose(Session session, CloseReason closeReason) {
+ logger.info("[onClose][session({}) 连接关闭。关闭原因是({})}]", session, closeReason);
+ WebSocketUtil.removeSession(session);
+ }
+
+ @OnError
+ public void onError(Session session, Throwable throwable) {
+ logger.info("[onClose][session({}) 发生异常]", session, throwable);
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ // 通过 ApplicationContext 获得所有 MessageHandler Bean
+ applicationContext.getBeansOfType(MessageHandler.class).values() // 获得所有 MessageHandler Bean
+ .forEach(messageHandler -> HANDLERS.put(messageHandler.getType(), messageHandler)); // 添加到 handlers 中
+ logger.info("[afterPropertiesSet][消息处理器数量:{}]", HANDLERS.size());
+ }
+
+ private Class extends Message> getMessageClass(MessageHandler handler) {
+ // 获得 Bean 对应的 Class 类名。因为有可能被 AOP 代理过。
+ Class> targetClass = AopProxyUtils.ultimateTargetClass(handler);
+ // 获得接口的 Type 数组
+ Type[] interfaces = targetClass.getGenericInterfaces();
+ Class> superclass = targetClass.getSuperclass();
+ while ((Objects.isNull(interfaces) || 0 == interfaces.length) && Objects.nonNull(superclass)) { // 此处,是以父类的接口为准
+ interfaces = superclass.getGenericInterfaces();
+ superclass = targetClass.getSuperclass();
+ }
+ if (Objects.nonNull(interfaces)) {
+ // 遍历 interfaces 数组
+ for (Type type : interfaces) {
+ // 要求 type 是泛型参数
+ if (type instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ // 要求是 MessageHandler 接口
+ if (Objects.equals(parameterizedType.getRawType(), MessageHandler.class)) {
+ Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
+ // 取首个元素
+ if (Objects.nonNull(actualTypeArguments) && actualTypeArguments.length > 0) {
+ return (Class) actualTypeArguments[0];
+ } else {
+ throw new IllegalStateException(String.format("类型(%s) 获得不到消息类型", handler));
+ }
+ }
+ }
+ }
+ }
+ throw new IllegalStateException(String.format("类型(%s) 获得不到消息类型", handler));
+ }
+
+}
diff --git a/lab-25/pom.xml b/lab-25/pom.xml
index b90adbb4..da81aa31 100644
--- a/lab-25/pom.xml
+++ b/lab-25/pom.xml
@@ -13,6 +13,7 @@
pom
lab-websocket-25-01
+ lab-websocket-25-02