Spring Boot整合WebSocket及Spring Security實例

一.爲什麼需要WebSocket
在HTTP協議中,所有請求都是由客戶端發起的,由服務端進行響應,服務端無法向客戶推送消息,但是在一些需要即時通信的應用中,有不可避免的需要服務端向客戶端推送消息,傳統的解決方案有如下幾種
1.輪詢
輪詢是最簡單的解決方案,其意義在於,客戶端在固定的時間間隔下不停地向服務端發送請求,查看服務端是否有新的數據,若服務端有新的數據,則返回給客戶端,若是服務端沒有新的數據,則返回一個空的JSON或者XML文檔,輪詢對於開發者來說實現方便,但弊端明顯:客戶端每次都要建立新的HTTP請求,服務端要處理大量的無效請求,在高併發的情景下會嚴重拖慢服務端的運行效率,同時服務端的資源被極大地浪費了,因此,此種方式並不可取
2.長輪詢
長輪詢對於輪詢存在的問題做了部分解決,在長輪詢中,在服務端接收到客戶端的請求後不會立即去響應客戶端,而是會等到服務端有新的數據時纔會立即響應客戶端的請求,否則服務端會持有這個請求而不返回,直到有新數據時才返回,這種方式在一定程度上節省了服務端的資源,但是也存在一些問題,例如:
(1)如果瀏覽器在服務響應之前有新的數據要發送就只能創建一個新的併發請求,或者嘗試先斷掉當前請求,在創建新的請求
(2)TCP和HTTP規範中都有連接超時一說,所以所謂的長輪詢並不能一直持續,服務端和客戶端的連接需要定期的連接和關閉,這就增大了開發者的工作量,有技術可以延長連接時間,但是這並不是主流的解決方案
3.Applet和Flash(即將下架)
二.WebSocket簡介
WebSocket是一種在單個TCP連接上進行雙全工通信的協議,已被W3C定爲標準,使用WebSocket可以使得客戶端和服務器之間的數據交換變得更加簡單,他允許服務端主動向客戶端推送數據,在WebSocket協議中,瀏覽器和服務端只需要完成一次握手,兩者之間就可以建立持久性的連接,並進行雙向數據傳輸
WebSocket使用了HTTP/1.1的協議升級特性,一個WebSocket請求首先使用非正常的HTTP請求以特定的模式訪問一個URL,這個URL有兩種模式,分別是ws和wss,對應HTTP協議中的HTTP以及HTTPS,在請求頭有一個Connection:Upgrade字段,表示客戶端想要對協議進行升級,另外還有一個Upgrade:websocket字段,表示客戶端想要將請求協議升級爲WebSocket協議,這兩個字段共同告訴服務器要將連接升級爲WebSocket這樣一個雙全工協議,如果服務端同意協議升級,那麼在握手完成之後,文本消息或者其他二進制的消息就可以同時在兩個方向上進行發送,而不需要關閉和重新連接,此時的客戶端可服務端的關係是對等的,他們可以互相向對方主動發送消息,和傳統的解決方案相比,WebSocket具有如下特點:
(1)WebSocket使用時需要先創建連接,這使得WebSocket成爲一種有狀態的協議,在之後的通行過程中可以省略部分狀態信息(例如身份認證等)
(2)WebSocket連接在端口80(ws)或者443(wss)上連接,與HTTP使用的端口相同,這樣基本所有的防火牆都不會阻止WebSocket的連接
(3)WebSocket使用HTTP協議進行握手,因此可以直接集成到網絡瀏覽器和HTTP服務器中,不需要額外的成本
(4)心跳消息(ping和pong)將被反覆推送,保持WebSocket一致處於活躍狀態
(5)使用該協議,當消息啓動或者到達時,服務端和客戶端都可以知道
(6)Websocket連接關閉時將發送一個特殊的關閉消息
(7)WebSocket支持跨域,可以避免Ajax的限制
(8)HTTP規範要求瀏覽器將併發連接限制爲每個主機名兩個連接,但是當我們使用WebSocket的時候,當握手完成後,該限制就不存在了,因爲此時的連接已經不再是HTTP連接了
(9)WebSocket協議支持擴展,用戶可以擴展協議,實現部分自定義的子協議
(10)更好的二進制支持以及更好的壓縮效果
三.Spring Boot整合WebSocket
1.消息羣發
創建項目:首先創建Spring Boot項目,添加如下依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.webjars/webjars-locator-core -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator-core</artifactId>
            <version>0.35</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.webjars/sockjs-client -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.1.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.webjars/stomp-websocket -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3-1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.webjars/jquery -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.3.1-1</version>
        </dependency>

Spring-boot-starter-websocket依賴時WebSocket相關依賴,其他的都是前端庫,使用jar包的形式對這些前端庫進行統一管理,使用webjsr添加到項目中的前端庫,在Spring Boot項目中已經默認添’加了靜態資源,因此可以直接使用
2.配置WebSocket
Spring框架提供了基於WebSocket的STOMP支持,STOMP是一個簡單的可交互操作的協議,通常被用於通過中間服務器在客戶端之間進行異步消息傳遞,WebSocket配置如下:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat").withSockJS();
    }
}

代碼解釋:

(1)自定義WebSocketConfig繼承自WebSocketMessageBrokerConfigurer進行WebSocket配置,然後通過@EnableWebSocketMessageBroker註解開啓了WebSocket消息代理
(2)registry.enableSimpleBroker("/topic")表示設置消息代理的前綴,即如果消息的前綴是”/topic”,就會將消息轉發給代理(broker),再由消息代理將消息廣播給當前連接的客戶端
(3)registry.setApplicationDestinationPrefixes("/app")表示配置一個或多個前綴,通過這些前綴過濾出需要被註解方法處理的消息
(4)registry.addEndpoint("/chat").withSockJS()表示定義一個前綴爲”/chat”的edPoint,並開啓sockjs支持,sockjs可以解決瀏覽器對WebSocket的兼容性問題,客戶端將通過這裏配置的URL來建立WebSocket連接
3.定義Controller
定義Controller用來實現對消息的處理

@RestController
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Message greeting(Message message)throws Exception{
        return message;     
    }
}

根據第二部配置,@MessageMapping("/hello")註解的方法將用來接收”/app/hello”路徑發送來的消息,在註解方法中對 對消息進行處理後,再將消息轉發到@SendTo定義的路徑上,而@SendTo的路徑是一個以”/topic”的路徑,因此該消息將被交給消息代理broker,再由broker進行廣播
4.前臺頁面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>單聊</title>
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/chat.js"></script>
</head><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>羣聊</title>
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/app.js"></script>
</head>
<body>
    <div>
        <label for="name">請輸入用戶名:</label> 
        <input type="text" id="name" placeholder="用戶名">
    </div>
    <div>
        <button id="connect" type="button">連接</button>
        <button id="disconnect" type="button" disabled="disabled">斷開連接</button>
    </div>
    <div id="chat" style="display: none;">
        <div>
            <label for="name">請輸入聊天內容:</label> 
            <input type="text" id="content" placeholder="聊天內容">
        </div>
        <button id="send" type="button">發送</button>
        <div id="greetings">
            <div id="conversation" style="display: none">羣聊進行中...</div>
        </div>
    </div>
</body>
</html>
<body>
    <div id="chat">
        <div id="chatsContent"></div>
        <div>
            請輸入聊天內容: <input type="text" id="content" placeholder="聊天內容">
            目標用戶: <input type="text" id="to" placeholder="目標用戶">
            <button id="send" type="button">發送</button>
        </div>
    </div>
</body>
</html>

5.頁面以及websocket的js邏輯

var stompClient = null;
function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
        $("#chat").show();
    } else {
        $("#conversation").hide();
        $("#chat").hide();
    }
    $("#greetings").html("");
}
function connect() {
    if (!$("#name").val()) {
        return;
    }
    var socket = new SockJS('/chat');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function(frame) {
        setConnected(true);
        stompClient.subscribe('/topic/greetings', function(greeting) {
            showGreeting(JSON.parse(greeting.body));
        });
    });
}
function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
}
function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({
        'name' : $("#name").val(),
        'content' : $("#content").val()
    }));
}
function showGreeting(message) {
    $("#greetings").append(
            "<div>" + message.name + ":" + message.content + "</div>");
}

$(function() {
    $("#connect").click(function() {
        connect();
    });
    $("#disconnect").click(function() {
        disconnect();
    });
    $("#send").click(function() {
        sendName();
    });
});

代碼解釋:
(1)connect方法表示建立一個WebSocket連接,在建立WebSocket連接時,用戶必須先輸入用戶名,然後才能建立連接
(2)方法體中的意思:使用SockJS建立連接,然後創建一個STOMP實例發起請求,在連接成功回調方法中,首先調用setConnected(true);方法進行頁面設置,然後調用STOPM中的subscribe方法訂閱服務端發送回來的消息,並將服務端發送來的消息展示出來(使用showGreeting方法)
(3)調用STOMP中的disconnect方法可以斷開一個WebScoket連接
5.實體類

public class Message {
    private String name;
    private String content;
........getter,setter..............
}

6.改造消息發送Controller
消息發送使用到了@SendTo註解,該註解講方法處理過的消息轉發到broker,再由broker進行廣播,除了@SendTo註解外,Spring還提供了SimpMessagingTemplate類來讓開發者更加靈活的發送消息,使用SimpMessagingTemlate可以對上面的Controller進行改造,改造結果如下:

@RestController
public class GreetingController {
    @Autowired
    SimpMessagingTemplate simpMessagingTemplate;
    @MessageMapping("/hello")
    public void greeting(Message message)throws Exception{
        simpMessagingTemplate.convertAndSend("/topic/greetings",message);
    }
}

四.消息點對點發送
1.添加依賴
既然是點對點發送,就應該有用戶的觀念,因此首先在項目中加入Spring Security的依賴,代碼如下:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2.配置Spring Security
對spring security進行配置,添加兩個用戶,同時配置所有地址都認證後才能訪問,代碼如下:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 密碼加密過:123
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        .withUser("齊**")
        .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
        .roles("admin")
        .and()
        .withUser("辛**")
        .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
        .roles("user")
        .and()
        .withUser("李**")
        .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
        .roles("user")
        .and()
        .withUser("嶽**")
        .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
        .roles("user")
        .and()
        .withUser("尚**")
        .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
        .roles("user");     
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .anyRequest()
        .authenticated()
        .and()
        .formLogin()
        .permitAll();
    }
}

3.改造WebSocket配置代碼如下:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic","/queue");
        registry.setApplicationDestinationPrefixes("/app");
    }

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

代碼解釋:
這裏修改了registry.enableSimpleBroker("/topic"),又增加了個前綴"/queue",方便對羣發消息和點對點消息進行管理
4.配置Controller
對WebSocket的Controller進行改造,代碼如下:

@RestController
public class GreetingController {

    @Autowired
    SimpMessagingTemplate simpMessagingTemplate;

    /**
     * 消息羣發
     * @param message
     * @return
     * @throws Exception
     */
    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Message greeting(Message message) throws Exception {
        return message;

    }

    /**
     * 點對點發送
     * @param principal
     * @param chat
     * @throws Exception
     */
    @MessageMapping("/chat")
    public void chat(Principal principal, Chat chat) throws Exception {
        String from=principal.getName();
        chat.setFrom(from);
        simpMessagingTemplate.convertAndSendToUser(chat.getTo(), "/queue/chat", chat);;
    }
}

代碼解釋:

(1)羣發消息依然使用@SendTo來實現,點對點則用SimpMessagingTemplate來實現

(2)@MessageMapping("/chat")表示來自”/app/chat”路徑的消息將被chat方法處理,chat方法的第一個參數Principal可以用來獲取當前登錄用戶的信息,第二個參數則是客戶端發送來的消息

(3)在chat方法中,首先獲取當前登錄用戶的用戶名,設置給chat對象的from屬性,再將消息發送出去,發送的目標就是chat的to屬性

(4)消息發送使用的方法是convertAndSendToUser,該方法內部調用了convertAndSend方法,並對消息路徑做了處理

5.消息實體類:

public class Chat {
    private String to;
    private String from;
    private String content;
    ........getter,setter省略............
}

代碼解釋:
Chat是一個普通的javaBean,to屬性表示消息的目標用戶,form表示消息從哪裏來,content則是消息的主體內容
6.前臺頁面——在線聊天頁面(創建在onlinechat.html)

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>單聊</title>
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/chat.js"></script>
</head>
<body>
    <div id="chat">
        <div id="chatsContent"></div>
        <div>
            請輸入聊天內容: <input type="text" id="content" placeholder="聊天內容">
            目標用戶: <input type="text" id="to" placeholder="目標用戶">
            <button id="send" type="button">發送</button>
        </div>
    </div>
</body>
</html>

5.頁面以及websocket的js邏輯

var stompClient = null;
function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
        $("#chat").show();
    } else {
        $("#conversation").hide();
        $("#chat").hide();
    }
    $("#greetings").html("");
}
function connect() {
    if (!$("#name").val()) {
        return;
    }
    var socket = new SockJS('/chat');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function(frame) {
        setConnected(true);
        stompClient.subscribe('/topic/greetings', function(greeting) {
            showGreeting(JSON.parse(greeting.body));
        });
    });
}
function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
}
function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({
        'name' : $("#name").val(),
        'content' : $("#content").val()
    }));
}
function showGreeting(message) {
    $("#greetings").append(
            "<div>" + message.name + ":" + message.content + "</div>");
}

$(function() {
    $("#connect").click(function() {
        connect();
    });
    $("#disconnect").click(function() {
        disconnect();
    });
    $("#send").click(function() {
        sendName();
    });
});

代碼解釋:
(1)連接成功後,訂閱地址爲”/user/queue/chat”,該地址比服務端配置的地址多了”/user”前綴,這是因爲,SimpMessagingTemplate類中自動添加了路徑前綴
(2)聊天消息發送路徑爲”/app/chat”
(3)發送的消息內容中有一個to字段,該字段用來描述消息發送的目標用戶

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章