Spring+STOMP實現WebSocket廣播訂閱、權限認證、一對一通訊(附源碼)
版聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/elonpage/article/details/78446695
1. 項目代碼
首先,放上項目的代碼鏈接。
https://github.com/Jamin20/websocket-spring-demo
2. 背景
WebSocket 是 Html5 新增加特性之一,目的是瀏覽器與服務端建立全雙工的通信方式,解決 http 使用 ajax 輪詢或 long-polling 請求-響應帶來過多的資源消耗,同時對特殊場景應用提供了全新的實現方式,比如聊天、股票交易、遊戲等對對實時性要求較高的行業領域。
3. WebSocket 原理
WebSocket是一個持久化的協議,只需要一次HTTP握手就可以進行連接。整個通訊過程是建立在一次連接/狀態中,也就避免了HTTP的非狀態性,服務端會一直知道你的信息,直到你關閉請求,這樣服務器就不需要反覆解析HTTP協議。同時,服務端就可以主動推送信息給客戶端。
傳統 HTTP 請求響應
WebSocket 請求響應
4. STOMP 協議
STOMP是一個簡單的互操作協議,用於服務器在客戶端之間進行異步消息傳遞。
客戶端可以使用SEND命令來發送消息以及描述消息的內容,用SUBSCRIBE命令來訂閱消息以及由誰來接收消息。這樣就可以建立一個發佈訂閱系統,消息可以從客戶端發送到服務器進行操作,服務器也可以推送消息到客戶端。
5. WebSocket 與 STOMP
WebSocket 是底層協議,STOMP 是適用於WebSocket 的上層協議。直接使用 WebSocket 就類似於使用 TCP 套接字來編寫 web 應用,沒有高層協議定義消息的語意,不利於開發與維護。同HTTP在TCP套接字上添加請求-響應模型層一樣,STOMP在 WebSocket之上提供了一個基於幀的線路格式層,用來定義消息語義。
6. Spring + STOMP
當使用 Spring 實現 STOMP 時,Spring WebSocket 應用程序充當客戶端的 STOMP 代理。消息被路由到 @Controller 消息處理方法,或路由到一個簡單的內存代理,經過處理後,發送給訂閱用戶。
另外,還可以配置 Spring 使用專用的 STOMP 代理(例如RabbitMQ,ActiveMQ 等)來實際傳播消息。在這種情況下,Spring 維護代理(MQ系統)的 TCP 連接,將消息轉發給它,並將消息傳遞給連接的 WebSocket 客戶端。
7. Spring + STOMP 實現廣播訂閱
通訊過程:
- 客戶端與服務器進行 HTTP 握手連接,連接點 EndPoint 通過 WebSocketMessageBroker 設置
- 客戶端通過 subscribe 向服務器訂閱消息主題(/topic/demo1/greetings)
- 客戶端可通過 send 向服務器發送消息,消息通過路徑 /app/demo1/hello/10086 達到服務端,服務端將其轉發到對應的Controller(根據Controller配置的 @MessageMapping(“/demo1/hello/{typeId}”) 信息)
- 服務器一旦有消息發出,將被推送到訂閱了相關主題的客戶端(Controller中的@SendTo(“/topic/demo1/greetings”)表示將方法中 return 的信息推送到 /topic/demo1/greetings 主題)
7.1. 服務端 WebSocketMessageBroker 配置
- 設置對外暴露的 EndPoint ,客戶端通過這個 EndPoint 進行業務接入
- 設置Broker,配置訂閱主題、以及客戶端消息的前綴等信息
-
@Configuration
-
@EnableWebSocketMessageBroker
-
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
-
@Override
-
public void configureMessageBroker(MessageBrokerRegistry config) {
-
/*
-
* 用戶可以訂閱來自"/topic"和"/user"的消息,
-
* 在Controller中,可通過@SendTo註解指明發送目標,這樣服務器就可以將消息發送到訂閱相關消息的客戶端
-
*
-
* 在本Demo中,使用topic來達到羣發效果,使用user進行一對一發送
-
*
-
* 客戶端只可以訂閱這兩個前綴的主題
-
*/
-
config.enableSimpleBroker("/topic", "/user");
-
/*
-
* 客戶端發送過來的消息,需要以"/app"爲前綴,再經過Broker轉發給響應的Controller
-
*/
-
config.setApplicationDestinationPrefixes("/app");
-
}
-
@Override
-
public void registerStompEndpoints(StompEndpointRegistry registry) {
-
/*
-
* 路徑"/webSocketEndPoint"被註冊爲STOMP端點,對外暴露,客戶端通過該路徑接入WebSocket服務
-
*/
-
registry.addEndpoint("/webSocketEndPoint").setAllowedOrigins("*").withSockJS();
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
7.2. 服務端 Controller 配置
- 配置程序入口 URI @MessageMapping(“/demo1/hello/{typeId}”)
- 配置消息推送的目標主題 @SendTo(“/topic/demo1/greetings”)
-
@Controller
-
public class GreetingController {
-
/*
-
* 使用restful風格
-
*/
-
@MessageMapping("/demo1/hello/{typeId}")
-
@SendTo("/topic/demo1/greetings")
-
public Greeting greeting(@DestinationVariable Integer typeId, HelloMessage message, @Headers Map<String, Object> headers) throws Exception {
-
return new Greeting(headers.get("simpSessionId").toString(), typeId + "---" + message.getMessage());
-
}
-
/*
-
* 這裏沒用@SendTo註解指明消息目標接收者,消息將默認通過@SendTo("/topic/twoWays")交給Broker進行處理
-
* 不推薦不使用@SendTo註解指明目標接受者
-
*/
-
@MessageMapping("/demo1/twoWays")
-
public Greeting twoWays(HelloMessage message) {
-
return new Greeting("這是沒有指明目標接受者的消息:", message.getMessage());
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
7.3. 客戶端連接與訂閱
- 配置 WebSocket 連接的URI:/webSocket/webSocketEndPoint
- 配置客戶端訂閱的主題:/topic/demo1/greetings
-
function connect() {
-
var socket = new SockJS('/webSocket/webSocketEndPoint');
-
stompClient = Stomp.over(socket);
-
var headers={
-
username:'admin',
-
password:'admin'
-
};
-
stompClient.connect(headers, function (frame) {
-
setConnected(true);
-
console.log('Connected: ' + frame);
-
stompClient.subscribe('/topic/demo1/greetings', function (greeting) {
-
showGreeting(JSON.parse(greeting.body).userId, JSON.parse(greeting.body).content);
-
});
-
stompClient.subscribe('/topic/demo1/twoWays', function (greeting) {
-
showGreeting(JSON.parse(greeting.body).userId, JSON.parse(greeting.body).content);
-
});
-
});
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
7.4. 客戶端發送消息
- 發送消息
-
function sendName() {
-
stompClient.send("/app/demo1/hello/10086", {}, JSON.stringify({'message': $("#message").val()}));
-
}
- 1
- 2
- 3
7.5 效果
8. Spring + STOMP 實現用戶驗證
8.1. 服務端設置請求攔截器
- 爲 configureClientInboundChannel 設置攔截器
- WebSocket 首次請求連接的時候,獲取其 Header 信息,利用Header 裏面的信息進行權限認證
- 通過認證的用戶,使用 accessor.setUser(user); 方法,將登陸信息綁定在該 StompHeaderAccessor 上,在Controller方法上可以獲取 StompHeaderAccessor 的相關信息
-
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
-
@Override
-
public void configureClientInboundChannel(ChannelRegistration registration) {
-
registration.setInterceptors(new ChannelInterceptorAdapter() {
-
@Override
-
public Message<?> preSend(Message<?> message, MessageChannel channel) {
-
StompHeaderAccessor accessor =
-
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
-
//1. 判斷是否首次連接請求
-
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
-
//2. 驗證是否登錄
-
String username = accessor.getNativeHeader("username").get(0);
-
String password = accessor.getNativeHeader("password").get(0);
-
for (Map.Entry<String, String> entry : Users.USERS_MAP.entrySet()) {
-
// System.out.println(entry.getKey() + "---" + entry.getValue());
-
if (entry.getKey().equals(username) && entry.getValue().equals(password)) {
-
//驗證成功,登錄
-
Authentication user = new Authentication(username); // access authentication header(s)}
-
accessor.setUser(user);
-
return message;
-
}
-
}
-
return null;
-
}
-
//不是首次連接,已經成功登陸
-
return message;
-
}
-
});
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
8.2. 服務端 Controller 可以獲取在攔截器中綁定的用戶登錄信息
- 使用 StompHeaderAccessor 獲得相關頭信息
-
@Controller
-
public class GreetingController2 {
-
@MessageMapping("/demo2/hello/{typeId}")
-
@SendTo("/topic/demo2/greetings")
-
public Greeting greeting(HelloMessage message, StompHeaderAccessor headerAccessor) throws Exception {
-
Authentication user = (Authentication) headerAccessor.getUser();
-
String sessionId = headerAccessor.getSessionId();
-
return new Greeting(user.getName(), "sessionId: " + sessionId + ", message: " + message.getMessage());
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
8.3. 客戶端登陸時,帶上登陸信息
- 利用Header,將登陸信息在首次連接時發送到服務端
-
function connect() {
-
var socket = new SockJS('/webSocket/webSocketEndPoint');
-
stompClient = Stomp.over(socket);
-
var headers={
-
username:$("#username").val(),
-
password:$("#password").val()
-
};
-
stompClient.connect(headers, function (frame) {
-
setConnected(true);
-
console.log('Connected: ' + frame);
-
stompClient.subscribe('/topic/demo2/greetings', function (greeting) {
-
showGreeting(JSON.parse(greeting.body).userId, JSON.parse(greeting.body).content);
-
});
-
});
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
8.4. 效果
9. Spring + STOMP 實現指定目標發送
- 客戶端可訂閱個人專屬的主題:/user/{username}/demo3/greetings
- 在 程序 中利用 SendToUser 發送消息到指定的主題:
2.1 Controller 註解,發送到自己 @SendToUser(“/demo3/greetings”)
2.2 利用 messagingTemplate 發送到指定用戶 messagingTemplate.convertAndSendToUser(destUsername, “/demo3/greetings”, greeting);
9.1. 服務端 WebSocketMessageBroker 配置
- 增加定向發送的配置(以下代碼爲configureMessageBroker中需要增加的內容)
-
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
-
@Override
-
public void configureMessageBroker(MessageBrokerRegistry config) {
-
/*
-
* 一對一發送的前綴
-
* 訂閱主題:/user/{userID}//demo3/greetings
-
* 推送方式:1、@SendToUser("/demo3/greetings")
-
* 2、messagingTemplate.convertAndSendToUser(destUsername, "/demo3/greetings", greeting);
-
*/
-
config.setUserDestinationPrefix("/user");
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
9.2. 服務端 Controller 配置
- 必須注入SimpMessagingTemplate
- 使用註解和 messagingTemplate 發送消息到指定的訂閱主題(也就是目標客戶端)
- 目標客戶端使用 Restful 的方式在請求路徑中指定
-
@Controller
-
public class GreetingController3 {
-
private final SimpMessagingTemplate messagingTemplate;
-
/*
-
* 實例化Controller的時候,注入SimpMessagingTemplate
-
*/
-
@Autowired
-
public GreetingController3(SimpMessagingTemplate messagingTemplate) {
-
this.messagingTemplate = messagingTemplate;
-
}
-
@MessageMapping("/demo3/hello/{destUsername}")
-
@SendToUser("/demo3/greetings")
-
public Greeting greeting(@DestinationVariable String destUsername, HelloMessage message, StompHeaderAccessor headerAccessor) throws Exception {
-
Authentication user = (Authentication) headerAccessor.getUser();
-
String sessionId = headerAccessor.getSessionId();
-
Greeting greeting = new Greeting(user.getName(), "sessionId: " + sessionId + ", message: " + message.getMessage());
-
/*
-
* 對目標進行發送信息
-
*/
-
messagingTemplate.convertAndSendToUser(destUsername, "/demo3/greetings", greeting);
-
return new Greeting("系統", new Date().toString() + "消息已被推送。");
-
}
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
9.3. 客戶端訂閱
- 訂閱用戶相關消息主題:’/user/’ + $(“#username”).val() + ‘/demo3/greetings’
-
function connect() {
-
var socket = new SockJS('/webSocket/webSocketEndPoint');
-
stompClient = Stomp.over(socket);
-
var headers = {
-
username: $("#username").val(),
-
password: $("#password").val()
-
};
-
stompClient.connect(headers, function (frame) {
-
setConnected(true);
-
console.log('Connected: ' + frame);
-
stompClient.subscribe('/user/' + $("#username").val() + '/demo3/greetings', function (greeting) {
-
showGreeting(JSON.parse(greeting.body).userId, JSON.parse(greeting.body).content);
-
});
-
});
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
9.4 客戶端發送消息
- 客戶端發送消息,同時在請求路徑中指明發送目標客戶端
-
function sendName() {
-
stompClient.send("/app/demo3/hello/" + $("#destUsername").val(), {}, JSON.stringify({'message': $("#message").val()}));
-
}
- 1
- 2
- 3
9.5. 效果
代碼下載:
http://download.csdn.net/download/elonpage/10105442
https://github.com/Jamin20/websocket-spring-demo
參考文章:
1. Spring官方WebSocket文檔
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#websocket
2. WebSocket+SockJs+STMOP
http://www.jianshu.com/p/4ef5004a1c81
3. STOMP Over WebSocket(stomp.js)
http://jmesnil.net/stomp-websocket/doc/