Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
以按照以下步驟進行來實現,在Spring Boot中使用Spring WebFlux結合Bootstrap模板引擎實現單頁面應用(SPA)。
需要在POM文件中添加Spring Reactive Web、 Thymeleaf、Spring Data Reactive MongoDB等依賴配置,如下所示。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <!-- 或者使用其他模板引擎 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId> <!-- 如果需要持久化 -->
</dependency>
如果有特殊的需求,可以在application.yml或application.properties中添加WebFlux相關的配置信息,如果沒有就可以不用配置了。
在src/main/resources/templates目錄下創建Thymeleaf模板文件(或者你選擇的其他模板文件)。例如,創建index.html文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPA with Spring WebFlux</title>
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<h1 class="mt-5">Hello, Spring WebFlux with Bootstrap!</h1>
<!-- Add your SPA content here -->
<div id="app"></div>
</div>
<!-- Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
定義一個Controller來處理前端請求,如下所示。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import reactor.core.publisher.Mono;
@Controller
@RequestMapping("/")
public class MainController {
@GetMapping
public Mono<String> index() {
return Mono.just("index"); // 指向模板文件名
}
}
為了使你的應用能夠處理SPA的前端路由,你需要在Controller中添加相應的路由處理邏輯,或者利用JavaScript前端框架(如React、Vue.js等)來管理前端路由。
啟動Spring Boot應用,訪問http://localhost:8080(默認端口),應該能夠看到使用Bootstrap樣式的頁面。
通過這種方式,你可以利用Spring WebFlux的響應式特性和Bootstrap的前端樣式來構建現代化的單頁面應用。如果你需要使用現代SPA框架(如React、Vue.js),你可以將其構建后的靜態文件放在src/main/resources/static目錄中,并配置Spring Boot以提供這些靜態資源。
pringboot是如何路由到頁面?
現在我們創建了一個新的springboot的web工程
非常干凈。
resources目錄下面有兩個空的文件夾static和templates
static是用來放靜態資源的,包括靜態頁面,css,js,圖片等等
template用來放動態頁面,就是要根據java后臺代碼的返回值來動態生成的
先什么依賴都不加,當前我的maven依賴只有
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
在static下建一個login.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"/> </head> <body> <h2> 這是一個靜態登錄頁面 </h2> </body> </html>
啟動
測試
可以看到,默認情況下springboot會直接訪問到static下的靜態文件
必須加文件的后綴名。
測試結果:
可以看到,經過controller處理了一下請求,找到了靜態資源
但是,我們后臺開發主要還是使用動態頁面。現在引入Thymeleaf
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
在templates路徑下也創建一個login.html
測試:
已經找的是templates下的文件了。這說明引入Thymeleaf后,通過java代碼處理的請求,默認是找templates下的文件
當然,你測試
依然是靜態頁面
另外,此時,你的java代碼返回值帶不帶html后綴都可以
我們的最終目的是根據后臺數據動態生成頁面
所以傳點數據看看
言
Hi,大家好,我是希留。
在項目的開發工程中,可能會遇到實時性比較高的場景需求,例如說,聊天 IM 即時通訊功能、消息訂閱服務、在線客服等等。那遇到這種功能的時候應該怎么去做呢?通常是使用WebSocket去實現。
那么,本篇文章就帶大家來了解一下是什么是WebSocket,以及使用SpringBoot搭建一個簡易的聊天室功能。如果對你有幫助的話,還不忘點贊轉發支持一下,感謝!
源碼地址:
https://github.com/277769738/java-sjzl-demo/tree/master/springboot-websocket
https://gitee.com/huoqstudy/java-sjzl-demo/tree/master/springboot-websocket
目錄
一、什么是WebSocket
WebSocket 是HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通信(full-duplex)。一開始的握手需要借助HTTP請求完成。WebSocket是真正實現了全雙工通信的服務器向客戶端推的互聯網技術。它是一種在單個TCP連接上進行全雙工通訊協議。Websocket通信協議于2011年被IETF定為標準RFC 6455,Websocket API被W3C定為標準。
全雙工和單工的區別?
二、Http與WebSocket的區別
http協議是短連接,因為請求之后,都會關閉連接,下次重新請求數據,需要再次打開鏈接。
WebSocket協議是一種長鏈接,只需要通過一次請求來初始化鏈接,然后所有的請求和響應都是通過這個TCP鏈接進行通訊。
三、代碼實現
Maven 依賴:
<!--websocket依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 引入 Fastjson ,實現對 JSON 的序列化,因為后續我們會使用它解析消息 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
因為 WebSocket 協議,不像 HTTP 協議有 URI 可以區分不同的 API 請求操作,所以我們需要在 WebSocket 的 Message 里,增加能夠標識消息類型,這里我們采用 type 字段。所以在這個示例中,我們采用的 Message 采用 JSON 格式編碼,格式如下:
{
type : "", //消息類型
boby: {} //消息體
}
創建 Message 接口,基礎消息體,所有消息體都要實現該接口。目前作為一個標記接口,未定義任何操作。代碼如下:
public interface Message {
}
創建 AuthRequest 類,用戶認證請求。代碼如下:
public class AuthRequest implements Message{
public static final String TYPE="AUTH_REQUEST";
/**
* 認證 Token
*/
private String accessToken;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken=accessToken;
}
}
TYPE 靜態屬性,消息類型為 AUTH_REQUEST 。
accessToken 屬性,認證 Token 。在 WebSocket 協議中,我們也需要認證當前連接,用戶身份是什么。一般情況下,我們采用用戶調用 HTTP 登錄接口,登錄成功后返回的訪問令牌 accessToken 。
WebSocket 協議是基于 Message 模型,進行交互。但是,這并不意味著它的操作,不需要響應結果。例如說,用戶認證請求,是需要用戶認證響應的。所以,我們創建 AuthResponse 類,作為用戶認證響應。代碼如下:
public class AuthResponse implements Message {
public static final String TYPE="AUTH_RESPONSE";
/**
* 響應狀態碼
*/
private Integer code;
/**
* 響應提示
*/
private String message;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code=code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message=message;
}
}
創建 SendToOneRequest 類,發送給指定人的私聊消息的 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 void setToUser(String toUser) {
this.toUser=toUser;
}
public String getMsgId() {
return msgId;
}
public void setMsgId(String msgId) {
this.msgId=msgId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content=content;
}
}
在服務端接收到發送消息的請求,需要異步響應發送是否成功。所以,創建 SendResponse 類,發送消息響應結果的 Message 。代碼如下:
public class SendResponse implements Message{
public static final String TYPE="SEND_RESPONSE";
/**
* 消息編號
*/
private String msgId;
/**
* 響應狀態碼
*/
private Integer code;
/**
* 響應提示
*/
private String message;
}
在服務端接收到發送消息的請求,需要轉發消息給對應的人。所以,創建 SendToUserRequest 類,發送消息給一個用戶的 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 void setMsgId(String msgId) {
this.msgId=msgId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content=content;
}
}
每個客戶端發起的 Message 消息類型,我們會聲明對應的 MessageHandler 消息處理器。這個就類似在 SpringMVC 中,每個 API 接口對應一個 Controller 的 Method 方法。
創建 MessageHandler 接口,消息處理器接口。代碼如下:
public interface MessageHandler<T extends Message> {
/**
* 執行處理消息
* @param session 會話
* @param message 消息
*/
void execute(WebSocketSession session, T message);
/**
* 消息類型,即每個 Message 實現類上的 TYPE 靜態字段
* @return
*/
String getType();
}
創建 AuthMessageHandler 類,處理 AuthRequest 消息。代碼如下:
@Component
public class AuthMessageHandler implements MessageHandler<AuthRequest>{
@Override
public void execute(WebSocketSession session, AuthRequest message) {
// 如果未傳遞 accessToken
if (StringUtils.isEmpty(message.getAccessToken())) {
AuthResponse authResponse=new AuthResponse();
authResponse.setCode(1);
authResponse.setMessage("認證 accessToken 未傳入");
WebSocketUtil.send(session, AuthResponse.TYPE,authResponse);
return;
}
// 添加到 WebSocketUtil 中,考慮到代碼簡化,我們先直接使用 accessToken 作為 User
WebSocketUtil.addSession(session, message.getAccessToken());
// 判斷是否認證成功。這里,假裝直接成功
AuthResponse authResponse=new AuthResponse();
authResponse.setCode(0);
WebSocketUtil.send(session, AuthResponse.TYPE, authResponse);
}
@Override
public String getType() {
return AuthRequest.TYPE;
}
}
創建 SendToOneHandler 類,處理 SendToOneRequest 消息。代碼如下:
@Component
public class SendToOneHandler implements MessageHandler<SendToOneRequest>{
@Override
public void execute(WebSocketSession session, SendToOneRequest message) {
// 這里,假裝直接成功
SendResponse sendResponse=new SendResponse();
sendResponse.setMsgId(message.getMsgId());
sendResponse.setCode(0);
WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);
// 創建轉發的消息
SendToUserRequest sendToUserRequest=new SendToUserRequest();
sendToUserRequest.setMsgId(message.getMsgId());
sendToUserRequest.setContent(message.getContent());
// 廣播發送
WebSocketUtil.send(message.getToUser(), SendToUserRequest.TYPE, sendToUserRequest);
}
@Override
public String getType() {
return SendToOneRequest.TYPE;
}
}
創建 SendToAllHandler 類,處理 SendToAllRequest 消息。代碼如下:
@Component
public class SendToAllHandler implements MessageHandler<SendToAllRequest> {
@Override
public void execute(WebSocketSession session, SendToAllRequest message) {
// 這里,假裝直接成功
SendResponse sendResponse=new SendResponse();
sendResponse.setMsgId(message.getMsgId());
sendResponse.setCode(0);
WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);
// 創建轉發的消息
SendToUserRequest sendToUserRequest=new SendToUserRequest();
sendToUserRequest.setMsgId(message.getMsgId());
sendToUserRequest.setContent(message.getContent());
// 廣播發送
WebSocketUtil.broadcast(SendToUserRequest.TYPE, sendToUserRequest);
}
@Override
public String getType() {
return SendToAllRequest.TYPE;
}
}
創建 WebSocketUtil 工具類,代碼如下,主要提供兩方面的功能:
public class WebSocketUtil {
private static final Logger LOGGER=LoggerFactory.getLogger(WebSocketUtil.class);
/**
* Session 與用戶的映射
*/
private static final Map<WebSocketSession, String> SESSION_USER_MAP=new ConcurrentHashMap<>();
/**
* 用戶與 Session 的映射
*/
private static final Map<String, WebSocketSession> USER_SESSION_MAP=new ConcurrentHashMap<>();
/**
* 添加 Session 。在這個方法中,會添加用戶和 Session 之間的映射
* @param session Session
* @param user 用戶
*/
public static void addSession(WebSocketSession 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
* @param type 消息類型
* @param message 消息體
* @param <T> 消息類型
*/
public static <T extends Message> void send(WebSocketSession session, String type, T message) {
// 創建消息
TextMessage messageText=buildTextMessage(type, message);
// 遍歷給單個 Session ,進行逐個發送
sendTextMessage(session, messageText);
}
/**
* 廣播發送消息給所有在線用戶
* @param type 消息類型
* @param message 消息體
* @param <T> 消息類型
*/
public static <T extends Message> void broadcast(String type, T message) {
// 創建消息
TextMessage messageText=buildTextMessage(type, message);
// 遍歷 SESSION_USER_MAP ,進行逐個發送
for (WebSocketSession session : SESSION_USER_MAP.keySet()) {
sendTextMessage(session, messageText);
}
}
/**
* 發送消息給指定用戶
* @param user 指定用戶
* @param type 消息類型
* @param message 消息體
* @param <T> 消息類型
* @return 發送是否成功
*/
public static <T extends Message> boolean send(String user, String type, T message) {
// 獲得用戶對應的 Session
WebSocketSession 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 <T> 消息類型
* @return 消息
*/
private static <T extends Message> TextMessage buildTextMessage(String type, T message) {
JSONObject messageObject=new JSONObject();
messageObject.put("type", type);
messageObject.put("body", message);
return new TextMessage(messageObject.toString());
}
/**
* 真正發送消息
*
* @param session Session
* @param textMessage 消息
*/
private static void sendTextMessage(WebSocketSession session, TextMessage textMessage) {
if (session==null) {
LOGGER.error("[sendTextMessage][session 為 null]");
return;
}
try {
session.sendMessage(textMessage);
} catch (IOException e) {
LOGGER.error("[sendTextMessage][session({}) 發送消息{}) 發生異常",
session, textMessage, e);
}
}
}
處理類,在Spring中,處理消息的具體業務邏輯,進行開啟、關閉連接等操作。
public class MyHandler extends TextWebSocketHandler implements InitializingBean {
private Logger logger=LoggerFactory.getLogger(getClass());
/**
* 消息類型與 MessageHandler 的映射
* 無需設置成靜態變量
*/
private final Map<String, MessageHandler> HANDLERS=new HashMap<>();
@Autowired
private ApplicationContext applicationContext;
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
System.out.println("獲取到消息 >> " + message.getPayload());
logger.info("[handleMessage][session({}) 接收到一條消息({})]", session, message);
// 獲得消息類型
JSONObject jsonMessage=JSON.parseObject(message.getPayload());
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);
}
/**
* 連接建立時觸發
**/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("[afterConnectionEstablished][session({}) 接入]", session);
// 解析 accessToken
String accessToken=(String) session.getAttributes().get("accessToken");
// 創建 AuthRequest 消息類型
AuthRequest authRequest=new AuthRequest();
authRequest.setAccessToken(accessToken);
// 獲得消息處理器
MessageHandler<AuthRequest> messageHandler=HANDLERS.get(AuthRequest.TYPE);
if (messageHandler==null) {
logger.error("[onOpen][認證消息類型,不存在消息處理器]");
return;
}
messageHandler.execute(session, authRequest);
}
/**
* 關閉連接時觸發
**/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("斷開連接!");
}
@Override
public void afterPropertiesSet() throws Exception {
// 通過 ApplicationContext 獲得所有 MessageHandler Bean
applicationContext.getBeansOfType(MessageHandler.class).values()
// 添加到 handlers 中
.forEach(messageHandler -> HANDLERS.put(messageHandler.getType(), messageHandler));
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<Message>) actualTypeArguments[0];
} else {
throw new IllegalStateException(String.format("類型(%s) 獲得不到消息類型", handler));
}
}
}
}
}
throw new IllegalStateException(String.format("類型(%s) 獲得不到消息類型", handler));
}
}
在Spring中提供了websocket攔截器,可以在建立連接之前寫些業務邏輯,比如校驗登錄等。
public class MyHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
/**
* @Description 握手之前,若返回false,則不建立鏈接
* @Date 21:59 2021/5/16
* @return boolean
**/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> attributes) throws Exception {
//獲得 accessToken ,將用戶id放入socket處理器的會話(WebSocketSession)中
if (serverHttpRequest instanceof ServletServerHttpRequest) {
ServletServerHttpRequest serverRequest=(ServletServerHttpRequest) serverHttpRequest;
attributes.put("accessToken", serverRequest.getServletRequest().getParameter("accessToken"));
}
// 調用父方法,繼續執行邏輯
return super.beforeHandshake(serverHttpRequest, serverHttpResponse, webSocketHandler, attributes);
}
@Configuration
@EnableWebSocket //開啟spring websocket功能
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//配置處理器
registry.addHandler(this.myHandler(), "/")
//配置攔截器
.addInterceptors(new MyHandshakeInterceptor())
.setAllowedOrigins("*");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
@Bean
public MyHandshakeInterceptor webSocketShakeInterceptor() {
return new MyHandshakeInterceptor();
}
}
@SpringBootApplication
public class MyWebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(MyWebsocketApplication.class,args);
}
}
7.實現效果
打開三個瀏覽器,輸入在線測試websocket地址:
http://www.easyswoole.com/wstool.html
創建三個連接。分別設置服務地址如下:
發送單人消息
{
tpye: "SEND_TO_ONE_REQUEST",
boby: {
toUser: "1002",
msgId: "qwwerqrsfd123",
centent: "這是1001發送給1002的單聊消息"
}
}
可以看到1002收到了1001發的單聊信息,1003未收到。效果圖如下:
發送多人消息
{
tpye: "SEND_TO_ALL_REQUEST",
boby: {
msgId: "qwerqcfwwerqrsfd123",
centent: "我是一條群聊消息"
}
}
可以看到1001,1002,1003都收到了消息,效果圖如下:
結語
好了,以上就是今天要講的內容,本文介紹了WebSocket協議以及使用它簡單的實現及時聊天的場景。
感謝大家的閱讀,喜歡的朋友,歡迎點贊支持一下。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。