起因
- 想處理后端向前端發送訊息的情況,然后就了解到了原生
websocket和stomp協議方式來處理的幾種方式,最終選擇了stomp來,但很多參考資料都不全,導致費了很多時間,所以這里不說基礎的內容了,只記錄一些疑惑的點,
相關前綴和注解
在后臺的websocket配置中,我們看到有/app、/queue、/topic、/user這些前綴:
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");//注意此處有設定
registry.setUserDestinationPrefix("/user");
}
同時在controller中又有@MessageMapping、@SubscribeMapping、@SendTo、@SendToUser等注解,這些前綴和這些注解是由一定的關系的,這邊理一下:
- 首先前端
stompjs有兩種方式向后端互動,一種是發送訊息send,一種是訂閱subscribe,它們在都會帶一個目的地址/app/hello - 如果地址前綴是
/app,那么此訊息會關聯到@MessageMapping(send命令會到這個注解)、@SubscribeMapping(subscribe命令會到這個注解)中,如果沒有/app,則不會映射到任何注解上去,例如:
當前端發送://接收前端send命令發送的 @MessageMapping("/hello") //@SendTo("/topic/hello2") public String hello(@Payload String message) { return "123"; } //接收前端subscribe命令發送的 @SubscribeMapping("/subscribe") public String subscribe() { return "456"; } //接收前端send命令,但是單對單回傳 @MessageMapping("/test") @SendToUser("/queue/test") public String user(Principal principal, @Payload String message) { log.debug(principal.getName()); log.debug(message); //可以手動發送,同樣有queue //simpMessagingTemplate.convertAndSendToUser("admin","/queue/test","111"); return "111"; }send("/app/test",...)才會走到上方第一個中,而回傳的這個123,并不是直接回傳,而是默認將123轉到/topic/hello這個訂閱中去(自動在前面加上/topic),當然可以用@SendTo("/topic/hello2")中將123轉到/topic/hello2這個訂閱中;當前端發送subscribe("/app/subscribe",{接收直接回傳的內容},會走到第二個中,而456就不經過轉發了,直接會回傳,當然也可以增加@SendTo("/topic/hello2")注解來不直接回傳,而是轉到其它訂閱中, - 如果地址前綴是
/topic,這個沒什么說的,一般用于訂閱訊息,后臺群發, - 如果地址前綴是
/user,這個和一對一訊息有關,而且會和queue有關聯,前端必須同時增加queue,類似subscribe("/user/queue/test",...),后端的@SendToUser("/queue/test")同樣要加queue才能正確的發送到前端訂閱的地址,
token鑒權相關
權限相關一般是增加攔截器,網上查到的資料一般有兩種方式:
- 實作
HandshakeInterceptor介面在beforeHandshake方法中來處理,這種方式缺點是無法獲取header中的值,只能獲取url中的引數,如果token用jwt等很長的,用這種方式實作并不友好, - 實作
ChannelInterceptor介面在preSend方法中來處理,這種方式可以獲取header中的值,而且還可以設定用戶資訊等,詳細見下方攔截器代碼
vue端相關注意點
vue端用websocket的好處是單頁應用,不會頻繁的斷開和重連,所以相關代碼放到App.vue中- 由于要鑒權,所以需要登錄后再連接,這里用的方法是
watch監聽token,如果token從無到有,說明剛登錄,觸發websocket連接, - 前端引入包
npm instll sockjs-client和npm install stompjs,具體代碼見下方,
相關代碼
- 后臺配置
@Configuration @EnableWebSocketMessageBroker @Slf4j public class WebsocketConfig implements WebSocketMessageBrokerConfigurer { @Autowired private AuthChannelInterceptor authChannelInterceptor; @Bean public WebSocketInterceptor getWebSocketInterceptor() { return new WebSocketInterceptor(); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws")//請求地址:http://ip:port/ws .addInterceptors(getWebSocketInterceptor())//攔截器方式1,暫不用 .setAllowedOrigins("*")//跨域 .withSockJS();//開啟socketJs } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue", "/topic"); registry.setApplicationDestinationPrefixes("/app"); registry.setUserDestinationPrefix("/user"); } /** * 攔截器方式2 * * @param registration */ @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(authChannelInterceptor); } } - 攔截器
@Component @Order(Ordered.HIGHEST_PRECEDENCE + 99) public class AuthChannelInterceptor implements ChannelInterceptor { /** * 連接前監聽 * * @param message * @param channel * @return */ @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); //1、判斷是否首次連接 if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) { //2、判斷token List<String> nativeHeader = accessor.getNativeHeader("Authorization"); if (nativeHeader != null && !nativeHeader.isEmpty()) { String token = nativeHeader.get(0); if (StringUtils.isNotBlank(token)) { //todo,通過token獲取用戶資訊,下方用loginUser來代替 if (loginUser != null) { //如果存在用戶資訊,將用戶名賦值,后期發送時,可以指定用戶名即可發送到對應用戶 Principal principal = new Principal() { @Override public String getName() { return loginUser.getUsername(); } }; accessor.setUser(principal); return message; } } } return null; } //不是首次連接,已經登陸成功 return message; } } - 前端代碼,放在App.vue中:
import Stomp from 'stompjs' import SockJS from 'sockjs-client' import {mapGetters} from "vuex"; export default { name: 'App', data() { return { stompClient: null,//由于不需要客戶端給服務的發訊息,所以暫不設定全域了 } }, computed: { ...mapGetters(["token"]) }, created() { //只有登錄后才連接 if (this.token) { this.initWebsocket(); } }, destroyed() { this.closeWebsocket() }, watch: { token(val, oldVal) { //如果一開始沒有,現在有了,說明剛登錄,連接websocket if (!oldVal && val) { this.initWebsocket(); } //如果原先由,現在沒有了,說明退出登錄,斷開websocket if (oldVal && !val) { this.closeWebsocket(); } } }, methods: { initWebsocket() { let socket = new SockJS('http://localhost:8060/ws'); this.stompClient = Stomp.over(socket); this.stompClient.connect( {"Authorization": this.token},//傳遞token (frame) => { //測驗topic this.stompClient.subscribe("/topic/subscribe", (res) => { console.log("訂閱訊息1:"); console.log(res); }); //測驗 @SubscribeMapping this.stompClient.subscribe("/app/subscribe", (res) => { console.log("訂閱訊息2:"); console.log(res); }); //測驗單對單 this.stompClient.subscribe("/user/queue/test", (res) => { console.log("訂閱訊息3:"); console.log(res.body); }); //測驗發送 this.stompClient.send("/app/test", {}, JSON.stringify({"user": "user"})) }, (err) => { console.log("錯誤:"); console.log(err); //10s后重新連接一次 setTimeout(() => { this.initWebsocket(); }, 10000) } ); this.stompClient.heartbeat.outgoing = 20000; //若使用STOMP 1.1 版本,默認開啟了心跳檢測機制(默認值都是10000ms) this.stompClient.heartbeat.incoming = 0; //客戶端不從服務端接收心跳包 }, closeWebsocket() { if (this.stompClient !== null) { this.stompClient.disconnect(() => { console.log("關閉連接") }) } } } }
參考
- Spring訊息之STOMP,寫的挺詳細的,還有原始碼
- Spring官方檔案
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/253373.html
標籤:Java
上一篇:2021年可能回不去家了,只能努力刷Java面試題!!備戰金三銀四了~
下一篇:FastJson 處理json資料中物件相互參考,最后轉為json字串出現占位符("$ref"標識回圈參考)"的問題
