WebSocket 的故事(四)—— Spingboot 中,如何利用 WebSocket 和 STOMP 快速構建點對點的訊息模式(2)

xNPE發表於2019-03-04

題外話

最近事情太多,也有好久沒有更新了。在此感謝大家的持續關注。如果有任何問題,都可以私信我一起討論。

概述

本文是WebSocket的故事系列第三篇第二節,將針對上篇的程式碼介紹,給出一個STOMP實現點對點訊息的簡單例子。WebSocket的故事系列計劃分六大篇,旨在由淺入深的介紹WebSocket以及在Springboot中如何快速構建和使用WebSocket提供的能力。

本系列計劃包含如下幾篇文章:

第一篇,什麼是WebSocket以及它的用途
第二篇,Spring中如何利用STOMP快速構建WebSocket廣播式訊息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(2)
第五篇,Springboot中,實現網頁聊天室之自定義WebSocket訊息代理
第六篇,Springboot中,實現更靈活的WebSocket

本篇的主線

上一篇由@SendTo@SendToUser開始,深入Spring的WebSocket訊息傳送關鍵程式碼進行講解。本篇將具體實現一個基於STOMP的點對點訊息示例,並針對性的進行一些說明。

在本篇編寫過程中,我也檢視了一些網上的例子,多數都存在著或多或少的問題,能跑起來的很少,所以我也在文後給出了Github的示例連結,有需要的同學可以自取。

本篇適合的讀者

想要了解STOMP協議,Spring內部程式碼細節,以及如何使用Springboot搭建WebSocket服務的同學。

實現一個點對點訊息模式

一、引入WebSecurity實現使用者管理

講到點對點訊息,想象一下常見的如微信、QQ這些聊天工具,都是有使用者管理模組的,包括資料庫等等實現。我們這裡為了簡化,採用WebSecurity實現一個基於記憶體的簡單使用者登入管理,即可在服務端,儲存兩個使用者資訊,即可讓這兩個使用者互發資訊。

1. 引入依賴

<!-- 引入security模組 -->
<dependency>
<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
複製程式碼

2. 實現WebSecurityConfig

這裡我們構建兩個記憶體級別的使用者賬戶,以便我們在後面模擬互發訊息。

package com.xnpe.chat.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/","/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/chat")
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }

    //宣告兩個記憶體儲存使用者
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    		auth
                .inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("Xiao Ming").password(new BCryptPasswordEncoder().encode("123")).roles("USER")
                .and().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("Suby").password(new BCryptPasswordEncoder().encode("123")).roles("USER");
    }

    @Override
    public void configure(WebSecurity web){
        web.ignoring().antMatchers("/resources/static/**");
    }

}
複製程式碼

二、實現WebSocket和頁面的配置

兩個記憶體級別的使用者賬戶建立好以後,我們來進行WebSocket和頁面相關的配置。

1. 配置頁面資源路由

package com.xnpe.chat.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/chat").setViewName("chat");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
}
複製程式碼

2. 配置WebSocket STOMP

這裡我們註冊一個Endpoint名為Chat,並註冊一個訊息代理,名為queue

package com.xnpe.chat.config;

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;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/Chat").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue");
    }
}
複製程式碼

三、實現WebSocket的訊息處理

客戶端會將訊息傳送到chat這個指定的地址,它會被handleChat捕獲並處理。我們這裡做了個硬邏輯,如果資訊是由Xiao Ming發來的,我們會將它路由給Suby。反之亦然。

1. Controller的實現

這裡強調一下,我們監聽的Mapping地址是chat,所以後續在客戶端傳送訊息的時候,要注意訊息都是發到伺服器的這個地址的。服務端在接收到訊息後,會將訊息路由給/queue/notification這個地址,那麼也就是說,我們客戶端WebSocket訂閱的地址即為/queue/notification

package com.xnpe.chat.controller;

import com.xnpe.chat.data.Info;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

import java.security.Principal;

@Controller
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @MessageMapping("/chat")
    public void handleChat(Principal principal, Info info) {
        if (principal.getName().equals("Xiao Ming")) {
            messagingTemplate.convertAndSendToUser("Suby",
                    "/queue/notification", principal.getName() + " send message to you: "
                            + info.getInfo());
        } else {
            messagingTemplate.convertAndSendToUser("Xiao Ming",
                    "/queue/notification", principal.getName() + " send message to you: "
                            + info.getInfo());
        }
    }
}

複製程式碼

2. 訊息Bean

用來承載互發的訊息結構

package com.xnpe.chat.data;

public class Info {

    private String info;

    public String getInfo() {
        return info;
    }
}

複製程式碼

四、編寫客戶端Html頁面

1. 實現登入頁login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<head>
    <title>登陸頁面</title>
</head>
<body>
<div th:if="${param.error}">
    無效的賬號和密碼
</div>
<div th:if="${param.logout}">
    你已登出
</div>
<form th:action="@{/login}" method="post">
    <div><label> 賬號 : <input type="text" name="username"/> </label></div>
    <div><label> 密碼: <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="登陸"/></div>
</form>
</body>
</html>
複製程式碼

2. 實現聊天頁chat.html

強調一下兩個要點:

  • 連線WebSocket時,我們指定的是Chat這個Endpoint。傳送訊息時,我們要將訊息傳送到伺服器所mapping的地址上,即/chat
  • 由於服務端會將資訊發到/queue/notification這個訊息代理上,所以我們訂閱的也是這個地址,因為我們要實現的是一對一的訊息(根據上一篇的內容,不理解的同學可以參考上一篇文章),這裡在訂閱時要加上user字首。
<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<head>
    <title>歡迎進入聊天室</title>
    <script th:src="@{sockjs.min.js}"></script>
    <script th:src="@{stomp.min.js}"></script>
    <script th:src="@{jquery.js}"></script>
</head>
<body>
<p>
    聊天室
</p>

<form id="chatForm">
    <textarea rows="4" cols="60" name="text"></textarea>
    <input type="submit"/>
</form>

<script th:inline="javascript">
    $(`#chatForm`).submit(function(e){
        e.preventDefault();
        var text = $(`#chatForm`).find(`textarea[name="text"]`).val();
        sendSpittle(text);
        $(`#chatForm`).clean();
    });
    //連結endpoint名稱為 "/Chat" 的endpoint。
    var sock = new SockJS("/Chat");
    var stomp = Stomp.over(sock);
    stomp.connect(`abc`, `abc`, function(frame) {
        stomp.subscribe("/user/queue/notification", handleNotification);
    });

    function handleNotification(message) {
        $(`#output`).append("<b>Received: " + message.body + "</b><br/>")
    }

    function sendSpittle(text) {
        stomp.send("/chat", {}, JSON.stringify({ `info`: text }));
    }
    $(`#stop`).click(function() {sock.close()});
</script>

<div id="output"></div>
</body>
</html>
複製程式碼

演示點對點訊息

以上,我們程式的所有關鍵程式碼均已實現了。啟動後,訪問localhost:8080/login即可進入到登入頁。

WebSocket 的故事(四)—— Spingboot 中,如何利用 WebSocket 和 STOMP 快速構建點對點的訊息模式(2)

分別開啟兩個頁面,輸入賬號和密碼(程式碼中硬編碼的兩個賬戶資訊)。即可進入到chat頁面。

WebSocket 的故事(四)—— Spingboot 中,如何利用 WebSocket 和 STOMP 快速構建點對點的訊息模式(2)

在輸入框中輸入資訊,然後點選提交,訊息會被髮送到另一個使用者處。

WebSocket 的故事(四)—— Spingboot 中,如何利用 WebSocket 和 STOMP 快速構建點對點的訊息模式(2)

程式碼

本篇所用的程式碼工程已上傳至Github,想要體驗的同學自取。

GitHub-STOMP實現點對點訊息

總結

本篇羅列了基於STOMP實現點對點訊息的一個基本步驟,比較簡單,注意客戶端傳送訊息的地址和訂閱的地址即可。由於採用STOMP,我們實現的點對點訊息是基於使用者地址的,即STOMP實現了使用者地址到會話session的一個對映,這也幫助我們能夠輕鬆的給對端使用者傳送訊息,而不必關心底層實現的細節。但如果我們想自己封裝更復雜的業務邏輯,管理使用者的WebSocket session,更靈活的給使用者傳送資訊,這就是我們下一篇所要講述的內容,不使用STOMP,看看如何來實現更靈活的WebSocket點對點通訊。

歡迎持續關注

小銘出品,必屬精品

歡迎關注xNPE技術論壇,更多原創乾貨每日推送。

WebSocket 的故事(四)—— Spingboot 中,如何利用 WebSocket 和 STOMP 快速構建點對點的訊息模式(2)

相關文章