springboot+websocket+sockjs進行消息推送【基於STOMP協議】 WebSocket是在HTML5基礎上單個TCP連接上進行全雙工通訊的協議,只要瀏覽器和服務器進行一次握手,就可以建立一條快速通道,兩者就可以實現數據互傳了。說白了,就是打破了傳統的http協議的無狀態傳輸(只能瀏覽器請求,服務端響應),websocket全雙工通訊,就是瀏覽器和服務器進行一次握手,瀏覽器可以隨時給服務器發送信息,服務器也可以隨時主動發送信息給瀏覽器了。對webSocket原理有興趣的客官,可以自行百度。 2.環境搭建 因為是根據項目的需求來的,所以這里我只介紹在SpringBoot下使用WebSocket的其中一種實現【STOMP協議】。因此整個工程涉及websocket使用的大致框架為SpringBoot+Maven+websocket,其他框架的基礎搭建,我這里就不說了,相信各位也都很熟悉,我就直接集成websocket了。 在pox.xml加上對springBoot對WebSocket的支持: <!-- webSocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> 這樣SpringBoot就和WebSocket集成好了,我們就可以直接使用SpringBoot提供對WebSocket操作的API了 3.編碼實現 ①在Spring上下文中添加對WebSocket的配置 import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; /** * 配置WebSocket */ @Configuration //注解開啟使用STOMP協議來傳輸基於代理(message broker)的消息,這時控制器支持使用@MessageMapping,就像使用@RequestMapping一樣 @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{ @Override //注冊STOMP協議的節點(endpoint),並映射指定的url public void registerStompEndpoints(StompEndpointRegistry registry) { //注冊一個STOMP的endpoint,並指定使用SockJS協議 registry.addEndpoint("/endpointOyzc").setAllowedOrigins("*").withSockJS(); } @Override //配置消息代理(Message Broker) public void configureMessageBroker(MessageBrokerRegistry registry) { //點對點應配置一個/user消息代理,廣播式應配置一個/topic消息代理 registry.enableSimpleBroker("/topic","/user"); //點對點使用的訂閱前綴(客戶端訂閱路徑上會體現出來),不設置的話,默認也是/user/ registry.setUserDestinationPrefix("/user"); } } 介紹以上幾個相關的注解和方法: 1.@EnableWebSocketMessageBroker:開啟使用STOMP協議來傳輸基於代理(message broker)的消息,這時控制器支持使用@MessageMapping,就像使用@RequestMapping一樣。 2.AbstractWebSocketMessageBrokerConfigurer:繼承WebSocket消息代理的類,配置相關信息。 3.registry.addEndpoint("/endpointOyzc").setAllowedOrigins("*").withSockJS(); 添加一個訪問端點“/endpointGym”,客戶端打開雙通道時需要的url,允許所有的域名跨域訪問,指定使用SockJS協議。 4. registry.enableSimpleBroker("/topic","/user"); 配置一個/topic廣播消息代理和“/user”一對一消息代理 5. registry.setUserDestinationPrefix("/user");點對點使用的訂閱前綴(客戶端訂閱路徑上會體現出來),不設置的話,默認也是/user/ ②實現服務器主動向客戶端推送消息 SpringBoot封裝得太好,webSocket用起來太簡單(好處:用起來方便,壞處:你不知道底層實現) 1.一對多的實現: 先上后台java的代碼 package com.cheng.sbjm.boot; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Controller; import com.cheng.sbjm.domain.User; @Controller public class WebSocketController { @Autowired private SimpMessagingTemplate template; //廣播推送消息 @Scheduled(fixedRate = 10000) public void sendTopicMessage() { System.out.println("后台廣播推送!"); User user=new User(); user.setUserName("oyzc"); user.setAge(10); this.template.convertAndSend("/topic/getResponse",user); } } 簡單介紹一下 1.SimpMessagingTemplate:SpringBoot提供操作WebSocket的對象 2.@Scheduled(fixedRate = 10000):為了測試,定時10S執行這個方法,向客戶端推送 3.template.convertAndSend("/topic/getResponse",new AricResponse("后台實時推送:,Oyzc!")); :直接向前端推送消息。 3.1參數一:客戶端監聽指定通道時,設定的訪問服務器的URL 3.2參數二:發送的消息(可以是對象、字符串等等) 在上客戶端的代碼(PC現代瀏覽器) html頁面: <!DOCTYPE html> <html> <head> <title>websocket.html</title> <meta name="keywords" content="keyword1,keyword2,keyword3"> <meta name="description" content="this is my page"> <meta name="content-type" content="text/html" charset="UTF-8"> <!--<link rel="stylesheet" type="text/css" href="./styles.css">--> </head> <body> <div> <p id="response"></p> </div> <!-- 獨立JS --> <script type="text/javascript" src="jquery.min.js" charset="utf-8"></script> <script type="text/javascript" src="webSocket.js" charset="utf-8"></script> <script type="text/javascript" src="sockjs.min.js" charset="utf-8"></script> <script type="text/javascript" src="stomp.js" charset="utf-8"></script> </body> </html> JS代碼[webSocket.js] var stompClient = null; //加載完瀏覽器后 調用connect(),打開雙通道 $(function(){ //打開雙通道 connect() }) //強制關閉瀏覽器 調用websocket.close(),進行正常關閉 window.onunload = function() { disconnect() } function connect(){ var socket = new SockJS('http://127.0.0.1:9091/sbjm-cheng/endpointOyzc'); //連接SockJS的endpoint名稱為"endpointOyzc" stompClient = Stomp.over(socket);//使用STMOP子協議的WebSocket客戶端 stompClient.connect({},function(frame){//連接WebSocket服務端 console.log('Connected:' + frame); //通過stompClient.subscribe訂閱/topic/getResponse 目標(destination)發送的消息 stompClient.subscribe('/topic/getResponse',function(response){ showResponse(JSON.parse(response.body)); }); }); } //關閉雙通道 function disconnect(){ if(stompClient != null) { stompClient.disconnect(); } console.log("Disconnected"); } function showResponse(message){ var response = $("#response"); response.append("<p>"+message.userName+"</p>"); } 值得注意的是,只需要在連接服務器注冊端點endPoint時,寫訪問服務器的全路徑URL: new SockJS('http://127.0.0.1:9091/sbjm-cheng/endpointOyzc'); 其他監聽指定服務器廣播的URL不需要寫全路徑 stompClient.subscribe('/topic/getResponse',function(response){ showResponse(JSON.parse(response.body)); }); 2.一對一的實現 先上后台java的代碼 package com.cheng.sbjm.boot; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Controller; import com.cheng.sbjm.domain.User; @Controller public class WebSocketController { @Autowired private SimpMessagingTemplate template; //一對一推送消息 @Scheduled(fixedRate = 10000) public void sendQueueMessage() { System.out.println("后台一對一推送!"); User user=new User(); user.setUserId(1); user.setUserName("oyzc"); user.setAge(10); this.template.convertAndSendToUser(user.getUserId()+"","/queue/getResponse",user); } } 簡單介紹一下: 1.SimpMessagingTemplate:SpringBoot提供操作WebSocket的對象 2.@Scheduled(fixedRate = 10000):為了測試,定時10S執行這個方法,向客戶端推送 3.template.convertAndSendToUser(user.getUserId()+"","/queue/getResponse",user); :直接向前端推送消息。 3.1參數一:指定客戶端接收的用戶標識(一般用用戶ID) 3.2參數二:客戶端監聽指定通道時,設定的訪問服務器的URL(客戶端訪問URL跟廣播有些許不同) 3.3參數三:向目標發送消息體(實體、字符串等等) 在上客戶端的代碼(PC現代瀏覽器) html頁面: <!DOCTYPE html> <html> <head> <title>websocket.html</title> <meta name="keywords" content="keyword1,keyword2,keyword3"> <meta name="description" content="this is my page"> <meta name="content-type" content="text/html" charset="UTF-8"> <!--<link rel="stylesheet" type="text/css" href="./styles.css">--> <!-- 獨立css --> </head> <body> <div> <p id="response"></p> </div> <!-- 獨立JS --> <script type="text/javascript" src="jquery.min.js" charset="utf-8"></script> <script type="text/javascript" src="webSocket.js" charset="utf-8"></script> <script type="text/javascript" src="sockjs.min.js" charset="utf-8"></script> <script type="text/javascript" src="stomp.js" charset="utf-8"></script> </body> </html> JS代碼[webSocket.js] var stompClient = null; //加載完瀏覽器后 調用connect(),打開雙通道 $(function(){ //打開雙通道 connect() }) //強制關閉瀏覽器 調用websocket.close(),進行正常關閉 window.onunload = function() { disconnect() } function connect(){ var userId=1; var socket = new SockJS('http://127.0.0.1:9091/sbjm-cheng/endpointOyzc'); //連接SockJS的endpoint名稱為"endpointOyzc" stompClient = Stomp.over(socket);//使用STMOP子協議的WebSocket客戶端 stompClient.connect({},function(frame){//連接WebSocket服務端 console.log('Connected:' + frame); //通過stompClient.subscribe訂閱/topic/getResponse 目標(destination)發送的消息 stompClient.subscribe('/user/' + userId + '/queue/getResponse',function(response){ var code=JSON.parse(response.body); showResponse(code) }); }); } //關閉雙通道 function disconnect(){ if(stompClient != null) { stompClient.disconnect(); } console.log("Disconnected"); } function showResponse(message){ var response = $("#response"); response.append("<p>只有userID為"+message.userId+"的人才能收到</p>"); } 與廣播不同的是,在指定通道的URL加個用戶標識: stompClient.subscribe('/user/' + userId + '/queue/getResponse',function(response){ var code=JSON.parse(response.body); showResponse(code) }); 該標識userId必須與服務器推送消息時設置的用戶標識一致 以上就是實現服務器實時向客戶端推送消息,各位可以按照各自的需求進行配合使用。 ③實現客戶端與服務器之間的直接交互,聊天室demo[在②的基礎上添加了一些代碼] 1.在webSocket配置中,增加2個WebSocket的代理 package com.cheng.sbjm.configure; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; /** * 配置WebSocket */ @Configuration //注解開啟使用STOMP協議來傳輸基於代理(message broker)的消息,這時控制器支持使用@MessageMapping,就像使用@RequestMapping一樣 @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{ @Override //注冊STOMP協議的節點(endpoint),並映射指定的url public void registerStompEndpoints(StompEndpointRegistry registry) { //注冊一個STOMP的endpoint,並指定使用SockJS協議 registry.addEndpoint("/endpointOyzc").setAllowedOrigins("*").withSockJS(); } @Override //配置消息代理(Message Broker) public void configureMessageBroker(MessageBrokerRegistry registry) { //點對點應配置一個/user消息代理,廣播式應配置一個/topic消息代理,群發(mass),單獨聊天(alone) registry.enableSimpleBroker("/topic","/user","/mass","/alone"); //點對點使用的訂閱前綴(客戶端訂閱路徑上會體現出來),不設置的話,默認也是/user/ registry.setUserDestinationPrefix("/user"); } } "/mass"用以代理群發消息 "/alone"用以代碼一對一聊天 2.java后台實現 package com.cheng.sbjm.boot; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; import com.cheng.sbjm.onput.ChatRoomRequest; import com.cheng.sbjm.onput.ChatRoomResponse; @Controller public class WebSocketController { @Autowired private SimpMessagingTemplate template; //客戶端主動發送消息到服務端,服務端馬上回應指定的客戶端消息 //類似http無狀態請求,但是有質的區別 //websocket可以從服務器指定發送哪個客戶端,而不像http只能響應請求端 //群發 @MessageMapping("/massRequest") //SendTo 發送至 Broker 下的指定訂閱路徑 @SendTo("/mass/getResponse") public ChatRoomResponse mass(ChatRoomRequest chatRoomRequest){ //方法用於群發測試 System.out.println("name = " + chatRoomRequest.getName()); System.out.println("chatValue = " + chatRoomRequest.getChatValue()); ChatRoomResponse response=new ChatRoomResponse(); response.setName(chatRoomRequest.getName()); response.setChatValue(chatRoomRequest.getChatValue()); return response; } //單獨聊天 @MessageMapping("/aloneRequest") public ChatRoomResponse alone(ChatRoomRequest chatRoomRequest){ //方法用於一對一測試 System.out.println("userId = " + chatRoomRequest.getUserId()); System.out.println("name = " + chatRoomRequest.getName()); System.out.println("chatValue = " + chatRoomRequest.getChatValue()); ChatRoomResponse response=new ChatRoomResponse(); response.setName(chatRoomRequest.getName()); response.setChatValue(chatRoomRequest.getChatValue()); this.template.convertAndSendToUser(chatRoomRequest.getUserId()+"","/alone/getResponse",response); return response; } } 簡單介紹新的注解一下: 一.@MessageMapping("/massRequest"):類似與@RequestMapping,客戶端請求服務器的URL,前提是雙方端點已經打開 二.@SendTo("/mass/getResponse"):作用跟convertAndSend類似,廣播發給與該通道相連的客戶端 其他已經在前面解釋過了。 3.html代碼 <!DOCTYPE html> <html> <head> <title>login.html</title> <meta name="keywords" content="keyword1,keyword2,keyword3"> <meta name="description" content="this is my page"> <meta name="content-type" content="text/html" charset="UTF-8"> <!--<link rel="stylesheet" type="text/css" href="./styles.css">--> <!-- 獨立css --> <link rel="stylesheet" type="text/css" href="chatroom.css"> </head> <body> <div> <div style="float:left;width:40%"> <p>請選擇你是誰:</p> <select id="selectName" onchange="sendAloneUser();"> <option value="1">請選擇</option> <option value="ALong">ALong</option> <option value="AKan">AKan</option> <option value="AYuan">AYuan</option> <option value="ALai">ALai</option> <option value="ASheng">ASheng</option> </select> <div class="chatWindow"> <p style="color:darkgrey">群聊:</p> <section id="chatRecord" class="chatRecord"> <p id="titleval" style="color:#CD2626;"></p> </section> <section class="sendWindow"> <textarea name="sendChatValue" id="sendChatValue" class="sendChatValue"></textarea> <input type="button" name="sendMessage" id="sendMessage" class="sendMessage" onclick="sendMassMessage()" value="發送"> </section> </div> </div> <div style="float:right; width:40%"> <p>請選擇你要發給誰:</p> <select id="selectName2"> <option value="1">請選擇</option> <option value="ALong">ALong</option> <option value="AKan">AKan</option> <option value="AYuan">AYuan</option> <option value="ALai">ALai</option> <option value="ASheng">ASheng</option> </select> <div class="chatWindow"> <p style="color:darkgrey">單獨聊:</p> <section id="chatRecord2" class="chatRecord"> <p id="titleval" style="color:#CD2626;"></p> </section> <section class="sendWindow"> <textarea name="sendChatValue2" id="sendChatValue2" class="sendChatValue"></textarea> <input type="button" name="sendMessage" id="sendMessage" class="sendMessage" onclick="sendAloneMessage()" value="發送"> </section> </div> </div> </div> <!-- 獨立JS --> <script type="text/javascript" src="jquery.min.js" charset="utf-8"></script> <script type="text/javascript" src="chatroom.js" charset="utf-8"></script> <script type="text/javascript" src="sockjs.min.js" charset="utf-8"></script> <script type="text/javascript" src="stomp.js" charset="utf-8"></script> </body> </html> JS代碼[chatroom.js]: var stompClient = null; //加載完瀏覽器后 調用connect(),打開雙通道 $(function(){ //打開雙通道 connect() }) //強制關閉瀏覽器 調用websocket.close(),進行正常關閉 window.onunload = function() { disconnect() } //打開雙通道 function connect(){ var socket = new SockJS('http://172.16.0.56:9091/sbjm-cheng/endpointOyzc'); //連接SockJS的endpoint名稱為"endpointAric" stompClient = Stomp.over(socket);//使用STMOP子協議的WebSocket客戶端 stompClient.connect({},function(frame){//連接WebSocket服務端 console.log('Connected:' + frame); //廣播接收信息 stompTopic(); }); } //關閉雙通道 function disconnect(){ if(stompClient != null) { stompClient.disconnect(); } console.log("Disconnected"); } //廣播(一對多) function stompTopic(){ //通過stompClient.subscribe訂閱/topic/getResponse 目標(destination)發送的消息(廣播接收信息) stompClient.subscribe('/mass/getResponse',function(response){ var message=JSON.parse(response.body); //展示廣播的接收的內容接收 var response = $("#chatRecord"); response.append("<p><span>"+message.name+":</span><span>"+message.chatValue+"</span></p>"); }); } //列隊(一對一) function stompQueue(){ var userId=$("#selectName").val(); alert("監聽:"+userId) //通過stompClient.subscribe訂閱/topic/getResponse 目標(destination)發送的消息(隊列接收信息) stompClient.subscribe('/user/' + userId + '/alone/getResponse',function(response){ var message=JSON.parse(response.body); //展示一對一的接收的內容接收 var response = $("#chatRecord2"); response.append("<p><span>"+message.name+":</span><span>"+message.chatValue+"</span></p>"); }); } //選擇發送給誰的時候觸發連接服務器 function sendAloneUser(){ stompQueue(); } //群發 function sendMassMessage(){ var postValue={}; var chatValue=$("#sendChatValue"); var userName=$("#selectName").val(); postValue.name=userName; postValue.chatValue=chatValue.val(); if(userName==1||userName==null){ alert("請選擇你是誰!"); return; } if(chatValue==""||userName==null){ alert("不能發送空消息!"); return; } stompClient.send("/massRequest",{},JSON.stringify(postValue)); chatValue.val(""); } //單獨發 function sendAloneMessage(){ var postValue={}; var chatValue=$("#sendChatValue2"); var userName=$("#selectName").val(); var sendToId=$("#selectName2").val(); var response = $("#chatRecord2"); postValue.name=userName; postValue.chatValue=chatValue.val(); postValue.userId=sendToId; if(userName==1||userName==null){ alert("請選擇你是誰!"); return; } if(sendToId==1||sendToId==null){ alert("請選擇你要發給誰!"); return; } if(chatValue==""||userName==null){ alert("不能發送空消息!"); return; } stompClient.send("/aloneRequest",{},JSON.stringify(postValue)); response.append("<p><span>"+userName+":</span><span>"+chatValue.val()+"</span></p>"); chatValue.val(""); } chatroom.css .chatWindow{ width: 100%; height: 500px; border: 1px solid blue; } .chatRecord{ width: 100%; height: 400px; border-bottom: 1px solid blue; line-height:20px; overflow:auto; overflow-x:hidden; } .sendWindow{ width: 100%; height: 200px; } .sendChatValue{ width: 90%; height: 40px; } 另外還需要的3個JS包,jquery.min.js、sockjs.min.js、stomp.js。