Java應用層協(xié)議WebSocket實現(xiàn)消息推送
前言
大部分的web開發(fā)者,開發(fā)的業(yè)務都是基于Http協(xié)議的:前端請求后端接口,攜帶參數(shù),后端執(zhí)行業(yè)務代碼,再返回結果給前端。作者參與開發(fā)的項目,有一個報警推送的功能,服務端實時推送報警信息給瀏覽器端;還有像抖音里面,如果有人關注、回復你的評論時,抖音就會推送相關消息給你了,你就會收到一條消息。
有些同學會說了,基于Http協(xié)議也能實現(xiàn)?。呵岸硕〞r訪問后端(每隔3s或者幾秒),后端返回消息數(shù)據(jù),前端拿到后彈出消息。這種方式太low了,而且每個瀏覽器都這樣,使用系統(tǒng)的人一多,服務器的壓力就太大了些。那到底用什么技術手段實現(xiàn)呢?我們的主角就登場了。
WebSocket是在單個TCP連接上進行全雙工通信的應用層協(xié)議(Http協(xié)議也是應用層),瀏覽器端和服務端都可主動發(fā)送數(shù)據(jù)給另一端。這樣是不是比Http協(xié)議更適合消息推送這種場景。
瀏覽器端
作者建了一個SpringBoot項目,Html放在src\main\resources\static下:
<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <!-- 解決中文亂碼--> <meta charset="UTF-8"/> <title></title> <script type="text/javascript" src="./js/jquery.min.js"></script> </head> <body> <input id="input1" type="text" /><br/> <input type="button" value="瀏覽器發(fā)送服務端" onclick="btnClick()" /> <input type="button" value="服務端發(fā)送瀏覽器" onclick="btnClick1()" /> <input type="button" value="重新打開連接" onclick="btnClick2()" /> <br/> <textarea id="textArea" style="height: 50px"></textarea> <script> var ws; webSocketInit(); function webSocketInit() { ws =new WebSocket('ws://localhost:8080/bootdemo/webSocket/10086'); // 獲取連接狀態(tài) console.log('ws連接狀態(tài)[初始]:' + ws.readyState); //監(jiān)聽是否連接成功 ws.onopen = function () { console.log('ws連接狀態(tài)[成功]:' + ws.readyState); }; // 接聽服務器發(fā)回的信息并處理展示 ws.onmessage = function (obj) { console.log('接收到來自服務器的消息:'); var txt = $("#textArea").val(); $("#textArea").val(txt + "\n" + obj.data); $("#textArea").scrollTop($("#textArea")[0].scrollHeight); //完成通信后關閉WebSocket連接 // ws.close(); }; // 監(jiān)聽連接關閉事件 ws.onclose = function () { // 監(jiān)聽整個過程中websocket的狀態(tài) console.log('ws連接狀態(tài)[關閉]:' + ws.readyState); }; // 監(jiān)聽并處理error事件 ws.onerror = function (error) { console.log(error); }; } function btnClick() { console.log("瀏覽器端發(fā)送消息:"); //連接成功則發(fā)送一個數(shù)據(jù) ws.send($("#input1").val()); } function btnClick1() { $.ajax({ url: 'http://localhost:8080/bootdemo/pushWebSocket/publish?' + 'userId=10086&message=' + $("#input1").val(), type: 'GET', success: function (data) { // console.log(data); } }); } function btnClick2() { webSocketInit(); } </script> </body> </html>
服務器端
先引入依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency>
bean上添加@ServerEndpoint,作為WebSocket的服務端。
import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CopyOnWriteArraySet; @Component @Slf4j @ServerEndpoint("/webSocket/{userId}") public class WebSocketServer { //與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù) private Session session; private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>(); // 用來存在線連接數(shù) private static final Map<String, Session> sessionPool = new HashMap<String, Session>(); /** * 連接成功調(diào)用的方法 */ @OnOpen public void onOpen(Session session, @PathParam(value = "userId") String userId) { try { this.session = session; webSockets.add(this); sessionPool.put(userId, session); } catch (Exception e) { } } /** * 收到客戶端消息后調(diào)用的方法 */ @OnMessage public void onMessage(String message) { log.info("websocket消息: 收到客戶端消息:" + message); } public void sendOneMessage(String userId, String message) { Session session = sessionPool.get(userId); if (session != null && session.isOpen()) { try { log.info("服務端推送消息:" + message); session.getAsyncRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }
進行注冊:
@Configuration public class WebSocketConfigOne { /** * 這個bean會自動注冊使用了@ServerEndpoint注解聲明的對象 * 沒有的話會報404 * * @return */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
推送消息的控制器:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; import java.util.Map; @Controller @RequestMapping("/pushWebSocket") public class WebSocketController { @Autowired private WebSocketServer webSocketServer; @GetMapping("/publish") @ResponseBody public Map publish(String userId, String message) { webSocketServer.sendOneMessage(userId, message); HashMap<String, Object> map = new HashMap<>(); map.put("code", 200); return map; } }
還有我的配置文件application.properties:
# web port
server.port=8080
server.servlet.context-path=/bootdemo
運行啟動類后,訪問html(localhost:8080/bootdemo/index.html)如下:
有的同學一思索,點擊圖中的第2個按鈕"服務端發(fā)送瀏覽器",你這好像也是前端先請求,再推送的消息;我們的WebSocketController#publish方法,在真實的場景下,可以在后端的定時任務中、消息中間件的消費者端調(diào)用,不用前端先發(fā)送請求。
當然SpringBoot有專門構建WebSocket服務端的方式。
核心配置類:
import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.server.HandshakeInterceptor; import javax.servlet.http.HttpServletRequest; import java.util.Map; @Configuration @EnableWebSocket @Slf4j public class WebSocketConfig1 implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MyWebSocketHandler(), "/webSocket/{userId}")//設置連接路徑和處理 .setAllowedOrigins("*") .addInterceptors(new MyWebSocketInterceptor());//設置攔截器 } class MyWebSocketInterceptor implements HandshakeInterceptor { //前置攔截一般用來注冊用戶信息,綁定 WebSocketSession @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { log.info("前置攔截~~"); if (!(request instanceof ServletServerHttpRequest)) { return true; } HttpServletRequest servletRequest = ((ServletServerHttpRequest)request).getServletRequest(); Map map = (Map)servletRequest.getAttribute(HandlerMapping. URI_TEMPLATE_VARIABLES_ATTRIBUTE); String userId = (String)map.get("userId"); attributes.put("userId", userId); return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { log.info("后置攔截~~"); } } }
核心處理器:
import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.socket.*; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Component public class MyWebSocketHandler implements WebSocketHandler { private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>(); /** * 建立新的socket連接后回調(diào)的方法 */ @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String userId = (String) session.getAttributes().get("userId"); SESSIONS.put(userId, session); } /** * 接收到瀏覽器端的消息后回調(diào)的方法 */ @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { String msg = message.getPayload().toString(); log.info("收到客戶端消息:" + msg); } /** * 連接出錯時回調(diào)的方法 */ @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { log.info("連接出錯"); if (session.isOpen()) { session.close(); } } /** * 連接關閉時回調(diào)的方法 */ @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { log.info("連接關閉:status:" + closeStatus); } /** * 是否處理部分消息,返回false就行 */ @Override public boolean supportsPartialMessages() { return false; } /** * 推送消息給瀏覽器端 */ public void sendMessage(String userId, String message) { WebSocketSession webSocketSession = SESSIONS.get(userId); if (webSocketSession == null || !webSocketSession.isOpen()) { return; } try { webSocketSession.sendMessage(new TextMessage(message)); } catch (Exception ex) { log.error("推送消息異常:" + ex); } } }
控制器也改造下:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; import java.util.Map; @Controller @RequestMapping("/pushWebSocket") public class WebSocketController { @Autowired private MyWebSocketHandler handler; @GetMapping("/publish") @ResponseBody public Map publish(String userId, String message) { handler.sendMessage(userId, message); HashMap<String, Object> map = new HashMap<>(); map.put("code", 200); return map; } }
前端部分不用做修改,和之前一樣的代碼。
到此這篇關于Java應用層協(xié)議WebSocket實現(xiàn)消息推送的文章就介紹到這了,更多相關Java WebSocket內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- Java實現(xiàn)使用Websocket發(fā)送消息詳細代碼舉例
- 如何在Java中使用WebSocket協(xié)議
- springboot整合websocket后啟動報錯(javax.websocket.server.ServerContainer not available)
- Java實現(xiàn)WebSocket四個步驟
- java中Websocket的使用方法例子
- java基于websocket實現(xiàn)im聊天功能
- Java?spring?MVC環(huán)境中實現(xiàn)WebSocket的示例代碼
- Java中實現(xiàn)WebSocket方法詳解
- 教你如何使用Java實現(xiàn)WebSocket
- 一步步教你如何使用Java實現(xiàn)WebSocket
- java?WebSocket?服務端實現(xiàn)代碼
- Java中使用WebSocket的幾種方式
相關文章
如何使用IDEA查看java文件編譯后的字節(jié)碼內(nèi)容
這篇文章主要介紹了如何使用IDEA查看java文件編譯后的字節(jié)碼內(nèi)容,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03Java并發(fā)編程ReentrantReadWriteLock加讀鎖流程
這篇文章主要介紹了Java并發(fā)編程ReentrantReadWriteLock加讀鎖流程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05Spring Boot實現(xiàn)郵件服務(附:常見郵箱的配置)
這篇文章主要給大家介紹了關于Spring Boot實現(xiàn)郵件服務的相關資料,文中還附上了常見郵箱的配置,通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2018-12-12Spring中@RabbitHandler和@RabbitListener的區(qū)別詳析
@RabbitHandler是用于處理消息的方法注解,它與@RabbitListener注解一起使用,這篇文章主要給大家介紹了關于Spring中@RabbitHandler和@RabbitListener區(qū)別的相關資料,需要的朋友可以參考下2024-02-02Springboot使用RestTemplate調(diào)用第三方接口的操作代碼
這篇文章主要介紹了Springboot使用RestTemplate調(diào)用第三方接口,我只演示了最常使用的請求方式get、post的簡單使用方法,當然RestTemplate的功能還有很多,感興趣的朋友可以參考RestTemplate源碼2022-12-12