Spring Boot系列20 Spring Websocket實現向指定的使用者傳送訊息

hryou0922發表於2018-07-19

概述

不同上文Spring Boot系列十七 Spring Boot 整合 websocket,使用RabbitMQ做為訊息代理,本文我們介紹通過Spring websocket實現向特定的使用者傳送訊息。 本文的內容如下: 1. 首先實現簡單的登入功能,這裡向特定使用者傳送訊息的必要條件 2. 使用者登入系統後,才可以登入websocket,並重寫MyPrincipal 3. 實現向特定使用者傳送訊息的功能 4. 測試

首先實現簡單的登入功能,這是向特定使用者傳送訊息的必要條件

TestMQCtl:控制類 提供模擬登入,登入成功後轉到websocket頁面

   /**
     * 模擬登入     */
    @RequestMapping(value = "loginIn", method = RequestMethod.POST)
    public String login(HttpServletRequest request, @RequestParam(required=true) String name, String pwd){
        HttpSession httpSession = request.getSession();
        // 如果登入成功,則儲存到會話中
        httpSession.setAttribute("loginName", name);
        return "websocket/sendtouser/ws-sendtouser-rabbitmq";
    }

    /**
     * 轉到登入頁面
     */
    @RequestMapping(value = "login", method = RequestMethod.GET)
    public String loginPage(){
        // 轉到登入頁面
        return "websocket/sendtouser/login";
    }

    /**
     * websocket頁面
     * @return
     */
    @RequestMapping(value="/broadcast-rabbitmq/index")
    public String broadcastIndex(){
        return "websocket/sendtouser/ws-sendtouser-rabbitmq";
    }

複製程式碼

login.jsp 簡單的form表單,將請求提到loginIn,並轉到ws-sendtouser-rabbitmq.jsp頁面

<form action="loginIn" method="post">
    使用者名稱:<input type="text" name="name" />
    <p>
        密碼:<input type="password" name="password" />
    <p>
        <input type="submit" value="submit" />
</form>
複製程式碼

ws-sendtouser-rabbitmq.jsp 連線websocket並訂閱訊息,這個jsp之前的文章已經介紹過了這裡不詳細描述。頁面通過向/ws/icc/websocket啟動websocket,然後訂閱/user/topic/demo訊息

<script type="text/javascript">
    var stompClient = null;

    function setConnected(connected) {
        document.getElementById('connect').disabled = connected;
        document.getElementById('disconnect').disabled = !connected;
        document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
        $('#response').html();
    }

    function connect() {
        // websocket的連線地址,此值等於WebSocketMessageBrokerConfigurer中registry.addEndpoint("/ws/icc/websocket").withSockJS()配置的地址
        var socket = new SockJS('/ws/icc/websocket'); //1
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function(frame) {
            setConnected(true);
            console.log('Connected: ' + frame);
            // 客戶端訂閱訊息的目的地址:此值等於BroadcastCtl中@SendTo註解的裡配置的值。
            stompClient.subscribe(
                '/user/topic/demo',
                function(respnose){
                showResponse(JSON.parse(respnose.body));
                }
                );
        });
    }


    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log("Disconnected");
    }

    function showResponse(message) {
        var response = $("#response");
        response.html(message.name + "<br\>" + response.html());
    }
</script>

複製程式碼

使用者登入系統後,才可以登入websocket,並重寫MyPrincipal

AuthHandshakeInterceptor AuthHandshakeInterceptor是HandshakeInterceptor 的子類。在websocket握手前判斷,判斷當前使用者是否已經登入。如果未登入,則不允許登入websocket

@Component
public class AuthHandshakeInterceptor implements HandshakeInterceptor {
    private static final Logger log = LoggerFactory.getLogger(AuthHandshakeInterceptor.class);


    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        HttpSession httpSession = getSession(request);
        String user = (String)httpSession.getAttribute("loginName");

        if(StringUtils.isEmpty(user)){
            log.error("未登入系統,禁止登入websocket!");
            return false;
        }
        log.info("login = " + user);

        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
    }

    // 參考 HttpSessionHandshakeInterceptor
    private HttpSession getSession(ServerHttpRequest request) {
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request;
            return serverRequest.getServletRequest().getSession(false);
        }
        return null;
    }
}

複製程式碼

MyPrincipalHandshakeHandler MyPrincipalHandshakeHandler是DefaultHandshakeHandler 的子類,處理websocket請求,這裡我們只重寫determineUser方法,生成我們自己的Principal ,這裡我們使用loginName標記登入使用者,而不是預設值

@Component
public class MyPrincipalHandshakeHandler extends DefaultHandshakeHandler {
    private static final Logger log = LoggerFactory.getLogger(MyPrincipalHandshakeHandler.class);

    @Override
    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {

        HttpSession httpSession = getSession(request);
        String user = (String)httpSession.getAttribute("loginName");

        if(StringUtils.isEmpty(user)){
            log.error("未登入系統,禁止登入websocket!");
            return null;
        }
        log.info(" MyDefaultHandshakeHandler login = " + user);
        return new MyPrincipal(user);
    }

    private HttpSession getSession(ServerHttpRequest request) {
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request;
            return serverRequest.getServletRequest().getSession(false);
        }
        return null;
    }
}

複製程式碼

MyPrincipal 定義自己的Principal

public class MyPrincipal implements Principal {
    private String loginName;

    public MyPrincipal(String loginName){
        this.loginName = loginName;
    }
    @Override
    public String getName() {
        return loginName;
    }
}
複製程式碼

配置websocket 在registerStompEndpoints中將我們MyPrincipalHandshakeHandler 和AuthHandshakeInterceptor 配置到服務中 configureMessageBroker方法配置rabbitmq資訊,這裡略

@Configuration
// 此註解開使用STOMP協議來傳輸基於訊息代理的訊息,此時可以在@Controller類中使用@MessageMapping
@EnableWebSocketMessageBroker
public class WebSocketRabbitMQMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer {

    @Autowired
    private MyPrincipalHandshakeHandler myDefaultHandshakeHandler;
    @Autowired
    private AuthHandshakeInterceptor sessionAuthHandshakeInterceptor;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
     
        registry.addEndpoint("/ws/icc/websocket")
                .addInterceptors(sessionAuthHandshakeInterceptor)
                .setHandshakeHandler(myDefaultHandshakeHandler)
                .withSockJS();
    }
	…. 
}
複製程式碼

實現向特定使用者傳送訊息的功能

TestMQCtl: 登入到模擬傳送頁面:send.jsp 我們使用SimpMessagingTemplate 物件的convertAndSendToUser向指定使用者的/topic/demo傳送訊息

  @Autowired
    private SimpMessagingTemplate template;

    /**
     * 傳送頁面
     */
    @RequestMapping(value = "send")
    public String sendMq2UserPage(String msg, String userName){
        return "websocket/sendtouser/send";
    }
    /**
     * 向執行使用者傳送請求
     */
    @RequestMapping(value = "send2user")
    @ResponseBody
    public int sendMq2User(String msg, String name){
        System.out.println("===========" + msg + "=======" + name);
        RequestMessage demoMQ = new RequestMessage();
        demoMQ.setName(msg);
        template.convertAndSendToUser(name, "/topic/demo", JSON.toJSONString(demoMQ));
        return 0;
    }

複製程式碼

send.jsp 模擬傳送頁面

<form action="login" method="post">
        接收者使用者:<input type="text" id="name" name="name" value="<%=session.getAttribute("loginName") %>" />
    <p>
        訊息內容:<input type="text" id="msg" name="msg" />
    <p>
        <input type="button" id="send" value="傳送" />
</form>


<script src="/websocket/jquery.js"></script>
<script type=text/javascript>

    $("#send").click(function(){
        $.post("send2user",
            {
                name: $('#name').val(),
                msg: $('#msg').val()
            },
            function(data, status){
                alert("Data: " + data + "\nStatus: " + status);
            });
    });
</script>

複製程式碼

測試

測試一:

登入 http://127.0.0.1:8080/ws/login,使用xiaoming登入,並提交

Spring Boot系列20 Spring Websocket實現向指定的使用者傳送訊息

點選連線,如果連線變灰色,則登入websocket成功

Spring Boot系列20 Spring Websocket實現向指定的使用者傳送訊息

登入模擬傳送頁面http://127.0.0.1:8080/ws/send,向xiaoming傳送test-msg

Spring Boot系列20 Spring Websocket實現向指定的使用者傳送訊息

此時頁面收到資訊:

Spring Boot系列20 Spring Websocket實現向指定的使用者傳送訊息
在模擬介面,如果我們向其它使用者傳送資訊,則此介面不會收到資訊

測試二:

開啟兩個不同的瀏覽器,分別使用xiaoming1,xiaoming2登入系統, 使用模擬介面向xiaoming1傳送訊息,則只有xiaoming1收到 使用模擬介面向xiaoming2傳送訊息,則只有xiaoming2收到

###結論: 我們已經實現向特定的使用者傳送訊息的功能

程式碼

所有的詳細程式碼見github程式碼,請儘量使用tag v0.23,不要使用master,因為master一直在變,不能保證文章中程式碼和github上的程式碼一直相同

相關文章