1. 程式人生 > >SpringBoot整合WebSocket【基於STOMP協議】進行點對點[一對一]和廣播[一對多]實時推送,內附簡易聊天室demo

SpringBoot整合WebSocket【基於STOMP協議】進行點對點[一對一]和廣播[一對多]實時推送,內附簡易聊天室demo

最近專案來了新需求,需要做一個實時推送的功能,伺服器主動推送訊息給客戶端,在網上經過一輪搜查之後,確定使用WebSocket來進行開發。以前經常聽說WebSocket的神奇之處,如今終於可以嘗試使用它了。

1.淺談WebSocket

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");
    }
}

介紹以上幾個相關的註解和方法:

[email protected]:開啟使用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的物件

[email protected](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的物件

[email protected](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;
    }
}

簡單介紹新的註解一下:

[email protected]("/massRequest"):類似與@RequestMapping,客戶端請求伺服器的URL,前提是雙方端點已經開啟

[email protected]("/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,可以到我的資源裡進行下載!

4.測試結果:


該聊天室用於測試,所以好友都是固定寫死的,各位可以根據各自的需求進行修改

本來想弄個內網穿透,然後跟外網朋友玩玩的,弄了個NATAPP的免費的內網穿透,發現不知道為啥,webSocket訪問不到,有哪位大神知道原因,跟我說說唄。

如有錯漏,請各位大神指教!