一.為什麼需要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省略............
}
<!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>
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();
});
});