1. 程式人生 > >springboot2.0+websocket整合【群發訊息+單對單】(二)

springboot2.0+websocket整合【群發訊息+單對單】(二)

繼續上次的專案。如果對下面的程式碼有部分看不明白的,請到上一篇看看流程,或者到文末貼出專案的git地址。

1. 先從配置開始,WebStompConfig

程式碼中的註釋基本能夠解釋清楚每行的意思了,這裡就不再細說
完整程式碼

package com.example.websocketdemo1.stomp;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.
messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.
web.socket.config.annotation.WebSocketMessageBrokerConfigurer; /** * EnableWebSocketMessageBroker 註解表明: 這個配置類不僅配置了 WebSocket,還配置了基於代理的 STOMP 訊息; * registerStompEndpoints() 方法:新增一個服務端點,來接收客戶端的連線。將 “/chat” 路徑註冊為 STOMP 端點。這個路徑與之前傳送和接收訊息的目的路徑有所不同, 這是一個端點,客戶端在訂閱或釋出訊息到目的地址前,要連線該端點,即使用者傳送請求 :url=’/127.0.0.1:8080/chat’ 與 STOMP server 進行連線,之後再轉發到訂閱url; * configureMessageBroker() 方法:配置了一個 簡單的訊息代理,通俗一點講就是設定訊息連線請求的各種規範資訊。 * * @author linyun * @date 2018/9/13 下午5:15 */
@Configuration @EnableWebSocketMessageBroker public class WebStompConfig implements WebSocketMessageBrokerConfigurer { @Autowired private WebSocketHandleInterceptor interceptor; @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //新增一個/chat端點,客戶端就可以通過這個端點來進行連線;withSockJS作用是新增SockJS支援 registry.addEndpoint("/chat").setAllowedOrigins("*").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //定義了兩個客戶端訂閱地址的字首資訊,也就是客戶端接收服務端傳送訊息的字首資訊 registry.enableSimpleBroker("/message", "/notice"); //定義了服務端接收地址的字首,也即客戶端給服務端發訊息的地址字首 registry.setApplicationDestinationPrefixes("/app"); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { //註冊了一個接受客戶端訊息通道攔截器 registration.interceptors(interceptor); } }

2. 使用者資訊註冊,WebSocketHandleInterceptor

上一篇裡面,使用者資訊我們是直接儲存到session中,然後再通過握手的時候,將使用者資訊存入WebSocketSession。
這次使用stomp的模式也存在一個單對單的傳送訊息,就需要知道對方是誰,所以也要註冊一下使用者資訊。

完整的程式碼

package com.example.websocketdemo1.stomp;

import com.sun.security.auth.UserPrincipal;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.security.Principal;

/**
 * @author linyun
 * @date 2018/9/13 下午5:57
 */
@Component
public class WebSocketHandleInterceptor implements ChannelInterceptor {

    /**
     * 繫結user到websocket conn上
     * @param message
     * @param channel
     * @return
     */
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            String username = accessor.getFirstNativeHeader("username");
            if (StringUtils.isEmpty(username)) {
                return null;
            }
            // 繫結user
            Principal principal = new UserPrincipal(username);
            accessor.setUser(principal);
        }
        return message;
    }
}
注意這裡的username資訊
String username = accessor.getFirstNativeHeader("username");

username是在頁面中傳遞來的,具體的傳遞方式在後面的頁面中,具體的引數名稱可以隨意自定義。
另外一種獲取使用者資訊的方式:

Object raw = message.getHeaders().get(SimpMessageHeaderAccessor.NATIVE_HEADERS);
if (raw instanceof Map) {
    System.out.println(raw);
    // 列印raw之後,可以檢視頭部的引數,包含了username。
}

3.處理訊息的類,GreetingController

用來接收和傳送訊息。
先來一個訊息的model,用來包裝訊息,使用lombok外掛,省去了getset了。
程式碼:

package com.example.websocketdemo1.stomp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author linyun
 * @date 2018/9/13 下午5:44
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Message {
    private String to;
    private Long date;
    private String from;
    private String content;
}

controller的完整程式碼

package com.example.websocketdemo1.stomp;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.*;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.security.Principal;
import java.util.Map;

/**
 * @author linyun
 * @date 2018/9/13 下午5:42
 */
@Slf4j
@Controller
public class GreetingController {

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;


    /**
     * 測試頁面
     * @return
     */
    @RequestMapping("/chat4")
    public String chat4() {
        return "chat4";
    }

    /**
     * 測試頁面2
     * @return
     */
    @RequestMapping("/chat5")
    public String chat5() {
        return "chat5";
    }

    /**
     * 測試訂閱
     * @param message
     * @param messageHeaders
     * @param destination
     * @param headers
     * @param id
     * @param body
     */
    @MessageMapping("/hello/{id}")
    public void hello(Message message,
                      MessageHeaders messageHeaders,
                      @Header("destination") String destination,
                      @Headers Map<String, Object> headers,
                      @DestinationVariable long id,
                      @Payload String body) {
        log.info("message:{}", message);
        log.info("messageHeaders:{}", messageHeaders);
        log.info("destination:{}", destination);
        log.info("headers:{}", headers);
        log.info("id:{}", id);
        log.info("body:{}", body);
    }


    /***  群訊息   ***/

    /**
     * 主動返回訊息。
     * @param message
     */
    @MessageMapping("/hello")
    public void hello(@Payload com.example.websocketdemo1.stomp.Message message) {
        System.out.println(message);
        com.example.websocketdemo1.stomp.Message returnMessage = new com.example.websocketdemo1.stomp.Message();
        returnMessage.setContent("轉發," + message.getContent());
        simpMessagingTemplate.convertAndSend("/message/public", returnMessage);
    }

    /**
     * 使用註解的方式返回訊息
     * @param message
     * @return
     */
    @MessageMapping("/hello1")
    @SendTo("/message/public")
    public com.example.websocketdemo1.stomp.Message hello1(@Payload com.example.websocketdemo1.stomp.Message message) {
        System.out.println(message);
        com.example.websocketdemo1.stomp.Message returnMessage = new com.example.websocketdemo1.stomp.Message();
        returnMessage.setContent("轉發2," + message.getContent());
        return returnMessage;
    }

    /***  點對點   ***/

    /**
     * 點對點發送訊息。接收訊息的人是從訊息中獲取的。
     * @param message
     * @param principal
     */
    @MessageMapping("/hello2")
    public void hello2(@Payload com.example.websocketdemo1.stomp.Message message, Principal principal) {
        System.out.println(message);
        System.out.println(principal);
        com.example.websocketdemo1.stomp.Message returnMessage = new com.example.websocketdemo1.stomp.Message();
        returnMessage.setContent("轉發3," + message.getContent());
        simpMessagingTemplate.convertAndSendToUser(message.getTo(), "/notice/msg", returnMessage);
    }

}

稍微解釋一下程式碼中的幾個方法
第一個方法,/hello/{id},主要是用來測試在一次傳送訊息的請求中能夠獲取到那些引數,合理的利用這些引數於自己的業務中。

    @MessageMapping("/hello/{id}")
    public void hello(Message message,
                      MessageHeaders messageHeaders,
                      @Header("destination") String destination,
                      @Headers Map<String, Object> headers,
                      @DestinationVariable long id,
                      @Payload String body) {
        log.info("message:{}", message);
        log.info("messageHeaders:{}", messageHeaders);
        log.info("destination:{}", destination);
        log.info("headers:{}", headers);
        log.info("id:{}", id);
        log.info("body:{}", body);
    }

4.結合頁面測試

新建2個頁面,頁面中設定使用者的資訊

http://127.0.0.1:8080/chat4
username='tom';
http://127.0.0.1:8080/chat5
username='jerry';

主要是引入 stomp.js和socketjs這2個js。
頁面完整程式碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>測試websocket</title>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.css">
</head>
<body>
<div class="container">
    <button type="button" class="btn btn-primary" onclick="connect()">連結</button>
    <button type="button" class="btn btn-primary" onclick="disconnect()">斷開</button>

</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.js"></script>
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script language=javascript>

    var username = 'tom';
    var sendMessage = null;
    var disConnect = null;

    function connect() {
        var socket = new SockJS("http://127.0.0.1:8080/chat");
        var client = Stomp.over(socket);
        client.connect({
            username: username
        }, function (succ) {
            console.log('client connect success:', succ);

            client.subscribe("/message/public", function (res) {
                console.log('收到訊息---/message/public:',res);
            });

            client.subscribe("/user/notice/msg", function (res) {
                console.log('個人訊息:',res)
            });
        }, function (error) {
            console.log('client connect error:', error);
        });
        sendMessage = function (destination, headers, body) {
            client.send(destination, headers, body)
        };
        disConnect = function () {
            client.disconnect();
            console.log('client connect break')
        }
    }

    function disconnect() {
        disConnect();
    }

    //傳送聊天資訊
    function send(roomId, ct) {
        var messageModel = {};
        messageModel.type = 1;
        messageModel.content = ct;
        messageModel.from = username;
        sendMessage("/app/hello/" + roomId, {}, JSON.stringify(messageModel));
    }

    /**
     * 測試傳送一個訊息,如果訂閱了/sub/public的使用者都會收到訊息。
     */
    function send1() {
        var messageModel = {};
        messageModel.type = 1;
        messageModel.content = '你好,' + new Date().getTime();
        messageModel.from = username;
        sendMessage("/app/hello", {}, JSON.stringify(messageModel));
    }
    function send2() {
        var messageModel = {};
        messageModel.type = 1;
        messageModel.content = 'hello1,' + new Date().getTime();
        messageModel.from = username;
        sendMessage("/app/hello1", {}, JSON.stringify(messageModel));
    }
    /** 傳送訊息給個人,接收者 to **/
    function send3() {
        var messageModel = {};
        messageModel.to = 'jerry';
        messageModel.type = 1;
        messageModel.content = 'hello1,' + new Date().getTime();
        messageModel.from = username;
        sendMessage("/app/hello2", {}, JSON.stringify(messageModel));
    }
    }
</script>
</body>