WebSocket簡介
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
WebSocket是一個消息架構,不強制使用任何特定的消息協議,它依賴於應用層解釋消息的含義;
與處在應用層的HTTP不同,WebSocket處在TCP上非常薄的一層,會將字節流轉換爲文本/二進制消息,因此,對於實際應用來說,WebSocket的通信形式層級過低,因此,可以在 WebSocket 之上使用STOMP協議,來爲瀏覽器和server間的通信增加適當的消息語義。
理解STOMP和WebSocket之間的關係
1、直接使用WebSocket(SockJS)就很類似於使用TCP套接字
來編寫web應用,因爲沒有高層協議,就需要我們定義應用間所發送消息的語義,還需要確保連接的兩端都能遵循這些語義。
2、同HTTP在TCP套接字
上添加請求-響應模型層
一樣,STOMP在WebSocket之上提供了一個基於幀的線路格式層
,用來定義消息語義。
消息羣發 - 發佈模式
一、相關依賴引入
1、後端pom依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、前端依賴
<!-- 前端庫 -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
也可以直接前端頁面引入相關JS文件
jquery的js
sockjs-client的js
stomp-websocket的js
二、WebSocket配置文件
Spring提供了基於WebSocket
的STOMP
支持,STOMP是一個簡單的可互操作的協議,通常用於中間服務器與客戶端之間進行異步消息傳遞
。
@Configuration
@EnableWebSocketMessageBroker // 開啓WebSocket消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/**
* 設置消息代理的前綴 - '/topic'
* 被設置的前綴的消息會被轉發到消息代理
* 消息代理再將消息廣播給當前連接的客戶端 - 羣發
*/
registry.enableSimpleBroker("/topic");
/**
* 配置目標前綴,這裏只配置一個,即/app
* 配置了的前綴爲/app可以通過@MessageMapping註解的方法處理
* 其他的destination如/topic、/queue將被直接交給broker處理
*/
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/**
* 設置前綴爲/chat,可以通過這個/chat建立連接
* withSockJs支持解決WbeSocket兼容問題
*/
registry.addEndpoint("/chat").withSockJS();
}
}
三、創建消息Pojo類
@Data
public class Message {
/** 暱稱 */
private String name;
/** 消息內容 */
private String content;
}
三、Controller層
這裏的Controller
主要是用來處理消息的,前面配置文件中,我們配置了/app
目標前綴,前綴爲/app
的會進入到我們下面所說的Controller
層裏標註@MessageMapping
的方法裏被處理。
@Controller
public class TestController {
@MessageMapping("/hello") // 接收/app/hello路徑發來的消息
@SendTo("/topic/greetings") // 轉發到/topic/greetings
public Message greeting(Message message){
return message;
}
}
除了@SendTo
註解可以將處理過的消息轉發到broker,再由broker進行消息廣播外。Spring提供了一個SimpMessagingTemplate
類來讓開發者更加靈活地發送消消息。
@Autowired
private SimpMessagingTemplate template;
@MessageMapping("/hello") // 接收/app/hello路徑發來的消息
public void greeting(Message message){
template.convertAndSend("/topic/greetings",message);
}
四、前端界面及接受和發送消息
1、前端頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket羣聊</title>
</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="content">請輸入聊天內容:</label>
<input type="text" id="content" placeholder="聊天內容">
</div>
<button id="send" type="button">發送</button>
</div>
<!-- 聊天區域 -->
<div id="greetings">
<div id="conversation" style="display: none">羣聊中...</div>
</div>
<!-- JS引用區域 -->
<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>
<!-- 自定義JS -->
<script src="/js/app.js"></script>
</body>
</html>
2、自定義JS
// STOMP客戶端
var stompClient = null;
/**
* 是否已經連接,對頁面顯示進行處理
* @param connected 是否已經連接
*/
function setConnected(connected) {
$("#connect").prop("disabled",connected);
$("#disconnect").prop("disabled",!connected);
if(connected){
$("#conversation").show();
$("#chat").show();
}else{
$("#conversation").hide();
$("#chat").hide();
}
$("#greetings").html("");
}
/**
* 建立WebSocket連接
*/
function connect() {
// 如果名字爲空,則不讓連接
if(!$("#name").val()){
return;
}
// 通過SockJs建立連接對象
var socket = new SockJS('/chat');
// 也可以通過WebSocket建立連接
// var socket = new WebSocket("/chat");
// 獲取STOMP子協議的客戶端對象
stompClient = Stomp.over(socket);
// 向服務器發起websocket連接併發送CONNECT鎮
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 sendMessage() {
// 發送消息
stompClient.send("/app/hello",{},
JSON.stringify({'name':$("#name").val(),'content':$("#content").val()}));
}
/**
* 控制顯示消息
* @param message 消息對象
*/
function showGreeting(message) {
$("#greetings").append("<div>" + message.name + ":" + message.content + "</div>");
}
$(function () {
$("#connect").click(function () {
connect();
});
$("#disconnect").click(function () {
disconnect();
});
$("#send").click(function () {
sendMessage();
// 清空聊天框
$("#content").val("");
})
});
五、測試
運行項目,用兩個不同的瀏覽器打開,分別發送消息。
STOMP中的API介紹
對app.js
中的STOMP的API解釋。
一、發起連接
1、Socket連接對象
/**
* 通過SockJS建立WebSocket連接對象
* 參數就是我們配置文件中registerStompEndpoints方法裏配置的前綴
*/
var socket = new SockJS('/chat');
// 同樣,可以用下面這行代碼代替,但是就沒有SockJS提供的兼容支持
var socket = new WebSocket("/chat");
2、簽名方法
// 獲取STOMP子協議的客戶端對象
var stompClient = Stomp.over(socket);
/**
* headers:客戶端的認證信息
* connectCallback:連接成功時(服務器響應 CONNECTED 幀)的回調方法
* errorCallback:連接失敗時(服務器響應 CONNECTED 幀)的回調方法
* 如果不需要認證,使用{}替代即可
* 失敗回調方法可以省略
*/
stompClient.connect(headers, connectCallback, errorCallback);
其中headers
認證信息大概長這個樣子:
var headers = {
login: 'mylogin',
passcode: 'mypasscode',
// additional header
'client-id': 'my-client-id'
};
二、斷開連接
斷開連接是異步操作的。
/**
* disconnectCallback:回調方法
* 回調方法在操作完成時調用,可以省略
*/
stompClient.disconnect(disconnectCallback);
三、心跳機制
STOMP 1.1 版本,默認開啓了心跳檢測機制,可通過client對象的heartbeat進行配置,默認是10000ms
stompClient.heartbeat.outgoing=20000;
stompClient.heartbeat.incoming=0;
四、發送消息
/**
* destinationUrl:服務端Controller中@MessageMapping中匹配的URL
* headers:發送信息的header,JavaScript對象,可選參數,省略可用{}代替
* body:發送信息的body,字符串,可選參數
*/
stompClient.send(destinationUrl, headers, body);
1、JSON支持
body消息可以使用JSON數據,使用JSON.stringify轉換即可
stompClient.send("/app/hello",{},
JSON.stringify({'name':$("#name").val(),'content':$("#content").val()}));
2、事務支持
STOMP 客戶端支持在發送消息時用事務進行處理
// 該方法返回一個包含了事務 id、commit()、abort()的JavaScript 對象
var tx = stompClient.begin();
// 在headers對象中加入事務id,若沒有添加,則會直接發送消息,不會以事務進行處理
stompClient.send("/app/hello",{transaction: tx.id},
JSON.stringify({'name':$("#name").val(),'content':$("#content").val()}));
// 提交事務
tx.commit();
五、訂閱和接收消息(含取消訂閱和接收消息)
1、訂閱和接收
/**
* destinationUrl:服務端@SendTo匹配的URL
* callback:爲每次收到服務器推送的消息時的回調方法,該方法包含參數message(即收到的消息)
* headers:附加的headers,JavaScript對象,可選參數
* headers方法返回一個包含了id屬性的JavaScript對象,可作爲unsubscribe()方法的參數
*/
var subscription = varstompClient.subscribe(destinationUrl, callback, headers);
2、取消訂閱和接收
subscription.unsubscribe();
六、瞭解更多
瞭解更多,可以百度,或是參考STOMP 客戶端 API 整理
消息單發 - 點對點模式
爲了更能表現出點對點的用戶概念,所以這裏引入SpringSecurity
,然後通過內存裏的賬號登錄。
一、引入SpringSecurity依賴並配置
1、引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、配置文件
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("lcy")
.password("$2a$10$XcigeMfToGQ2bqRToFtUi.sG1V.HhrJV6RBjji1yncXReSNNIPl1K") // 123
.roles("admin")
.and()
.withUser("jyqc")
.password("$2a$10$XcigeMfToGQ2bqRToFtUi.sG1V.HhrJV6RBjji1yncXReSNNIPl1K")
.roles("user")
.and()
.withUser("xbyx")
.password("$2a$10$XcigeMfToGQ2bqRToFtUi.sG1V.HhrJV6RBjji1yncXReSNNIPl1K")
.roles("admin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() // 登錄就可以訪問
.and()
.formLogin().permitAll(); // 登錄相關url都可以訪問
}
}
二、修改WebSocket配置文件以及Controller
1、WebSocket配置文件
@Configuration
@EnableWebSocketMessageBroker // 開啓WebSocket消息代理
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();
}
}
2、Controller
新增如下代碼
@Autowired
private SimpMessagingTemplate template;
@MessageMapping("/chat")
public void chat(Principal principal, Chat chat){
String from = principal.getName();
chat.setFrom(from);
// convertAndSendToUser內部會做處理
// 發送的最終路徑是/user/用戶名/queue/chat
template.convertAndSendToUser(chat.getTo(),"/queue/chat",chat);
}
convertAndSendToUser
源碼查看,可以發現裏面對url
進行修改,this.destinationPrefix
默認是/user
。
三、HTML與JS
1、html頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket單聊</title>
</head>
<body>
<div id="chat">
<div id="chatsContent">
</div>
<div>
聊天內容:
<input type="text" id="content" placeholder="請輸入聊天內容"><br>
目標用戶:
<input type="text" id="to" placeholder="請輸入目標用戶"><br>
<button id="send" type="button">發送</button>
</div>
</div>
<!-- JS引用區域 -->
<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="/js/chat.js"></script>
</body>
</html>
2、chat.js
// STOMP客戶端
var stompClient = null;
function connect() {
var socket = new SockJS("/chat");
stompClient = Stomp.over(socket);
stompClient.connect({},function (frame) {
// 因爲後端的convertAndSendToUser處理了url,所以也就是爲什麼相比之前的羣裏多了個/user的原因
stompClient.subscribe('/user/queue/chat',function (chat) {
showGreeting(JSON.parse(chat.body));
});
});
}
function sendMsg() {
stompClient.send("/app/chat",{},JSON.stringify({'content':$("#content").val(),'to':$("#to").val()}));
}
function showGreeting(message) {
$("#chatsContent")
.append("<div>" + message.from + ":" + message.content + "</div>");
}
$(function () {
connect();
$("#send").click(function () {
sendMsg();
$("#content").val("");
})
});
測試
開啓三個不同的瀏覽器測試,點對點模式沒有問題。