[ 史上最姨母級(jí) ] 3w字帶你揭開WebSocket的神秘面紗~

一. WebSocket 簡介

WebSocket 是一種基于 TCP 的網(wǎng)絡(luò)協(xié)議。在 2009 年誕生,于 2011 年被 IETF 定為標(biāo)準(zhǔn) RFC 6455 通信標(biāo)準(zhǔn),并由 RFC7936 補(bǔ)充規(guī)范。WebSocket API 也被 W3C 定為標(biāo)準(zhǔn)。
WebSocket 也是一種全雙工通信的協(xié)議,既允許客戶端向服務(wù)器主動(dòng)發(fā)送消息,也允許服務(wù)器主動(dòng)向客戶端發(fā)送消息。在 WebSocket 中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就可以建立持久性的連接,進(jìn)行雙向數(shù)據(jù)傳輸。
二. WebSocket 特點(diǎn)
連接握手階段使用 HTTP協(xié)議;協(xié)議標(biāo)識(shí)符是 ws,如果采用加密則是wss;數(shù)據(jù)格式比較輕量,性能開銷小,通信高效; 沒有同源限制,客戶端可以與任意服務(wù)器通信; 建立在 TCP協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易;通過 WebSocket可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù);與 HTTP協(xié)議有著良好的兼容性。默認(rèn)端口也是80和443,并且握手階段采用HTTP協(xié)議,因此握手時(shí)不容易屏蔽,能通過各種HTTP代理服務(wù)器;
三. 為什么需要 WebSocket?
談起為什么需要 WebSocket 前,那得先了解在沒有 WebSocket 那段時(shí)間說起,那時(shí)候基于 Web 的消息基本上是靠 Http 協(xié)議進(jìn)行通信,而經(jīng)常有”聊天室”、”消息推送”、”股票信息實(shí)時(shí)動(dòng)態(tài)”等這樣需求,而實(shí)現(xiàn)這樣的需求常用的有以下幾種解決方案:

1. 短輪詢(Traditional Polling)
短輪詢是指客戶端每隔一段時(shí)間就詢問一次服務(wù)器是否有新的消息,如果有就接收消息。這樣方式會(huì)增加很多次無意義的發(fā)送請(qǐng)求信息,每次都會(huì)耗費(fèi)流量及處理器資源。
優(yōu)點(diǎn):短連接,服務(wù)器處理簡單,支持跨域、瀏覽器兼容性較好。
缺點(diǎn):有一定延遲、服務(wù)器壓力較大,浪費(fèi)帶寬流量、大部分是無效請(qǐng)求。
2. 長輪詢(Long Polling)
長輪詢是段輪詢的改進(jìn),客戶端執(zhí)行 HTTP 請(qǐng)求發(fā)送消息到服務(wù)器后,等待服務(wù)器回應(yīng),如果沒有新的消息就一直等待,知道服務(wù)器有新消息傳回或者超時(shí)。
這也是個(gè)反復(fù)的過程,這種做法只是減小了網(wǎng)絡(luò)帶寬和處理器的消耗,但是帶來的問題是導(dǎo)致消息實(shí)時(shí)性低,延遲嚴(yán)重。而且也是基于循環(huán),最根本的帶寬及處理器資源占用并沒有得到有效的解決。
優(yōu)點(diǎn):減少輪詢次數(shù),低延遲,瀏覽器兼容性較好。
缺點(diǎn):服務(wù)器需要保持大量連接。
3. 服務(wù)器發(fā)送事件(Server-Sent Event)
“目前除了
IE/Edge,其他瀏覽器都支持。
服務(wù)器發(fā)送事件是一種服務(wù)器向?yàn)g覽器客戶端發(fā)起數(shù)據(jù)傳輸?shù)募夹g(shù)。一旦創(chuàng)建了初始連接,事件流將保持打開狀態(tài),直到客戶端關(guān)閉。該技術(shù)通過傳統(tǒng)的 HTTP 發(fā)送,并具有 WebSockets 缺乏的各種功能,例如”自動(dòng)重新連接”、”事件ID” 及 “發(fā)送任意事件”的能力。
“服務(wù)器發(fā)送事件是單向通道,只能服務(wù)器向?yàn)g覽器發(fā)送,因?yàn)榱餍畔⒈举|(zhì)上就是下載。
優(yōu)點(diǎn):適用于更新頻繁、低延遲并且數(shù)據(jù)都是從服務(wù)端發(fā)到客戶端。
缺點(diǎn):瀏覽器兼容難度高。
總結(jié)
顯然,上面這幾種方式都有各自的優(yōu)缺點(diǎn),雖然靠輪詢方式能夠?qū)崿F(xiàn)這些一些功能,但是其對(duì)性能的開銷和低效率是非常致命的,尤其是在移動(dòng)端流行的現(xiàn)在。
現(xiàn)在客戶端與服務(wù)端雙向通信的需求越來越多,且現(xiàn)在的瀏覽器大部分都支持 WebSocket。所以對(duì)實(shí)時(shí)性和雙向通信及其效率有要求的話,比較推薦使用 WebSocket。
四. WebSocket 連接流程
第一步
客戶端先用帶有 Upgrade:Websocket 請(qǐng)求頭的 HTTP 請(qǐng)求,向服務(wù)器端發(fā)起連接請(qǐng)求,實(shí)現(xiàn)握手(HandShake)。
客戶端 HTTP 請(qǐng)求的 Header 頭信息如下:
Connection:?Upgrade
Sec-WebSocket-Extensions:?permessage-deflate;?client_max_window_bits
Sec-WebSocket-Key:?IRQYhWINfX5Fh1zdocDl6Q==
Sec-WebSocket-Version:?13
Upgrade:?websocket
Connection: Upgrade 表示要升級(jí)協(xié)議。Upgrade: Websocket 要升級(jí)協(xié)議到 websocket 協(xié)議。Sec-WebSocket-Extensions: 表示客戶端所希望執(zhí)行的擴(kuò)展(如消息壓縮插件)。Sec-WebSocket-Key: 主要用于WebSocket協(xié)議的校驗(yàn),對(duì)應(yīng)服務(wù)端響應(yīng)頭的Sec-WebSocket-Accept。Sec-WebSocket-Version: 表示websocket的版本。如果服務(wù)端不支持該版本,需要返回一個(gè)Sec-WebSocket-Versionheader,里面包含服務(wù)端支持的版本號(hào)。
第二步
握手成功后,由 HTTP 協(xié)議升級(jí)成 Websocket 協(xié)議,進(jìn)行長連接通信,兩端相互傳遞信息。
服務(wù)端響應(yīng)的 HTTP Header 頭信息如下:
Connection:?upgrade
Sec-Websocket-Accept:?TSF8/KitM+yYRbXmjclgl7DwbHk=
Upgrade:?websocket
Connection: Upgrade 表示要升級(jí)協(xié)議。Upgrade: Websocket 要升級(jí)協(xié)議到 websocket 協(xié)議。Sec-Websocket-Accept: 對(duì)應(yīng)Sec-WebSocket-Key生成的值,主要是返回給客戶端,讓客戶端對(duì)此值進(jìn)行校驗(yàn),證明服務(wù)端支持WebSocket。
五. WebSocket 使用場(chǎng)景
數(shù)據(jù)流狀態(tài): 比如說上傳下載文件,文件進(jìn)度,文件是否上傳成功。
協(xié)同編輯文檔: 同一份文檔,編輯狀態(tài)得同步到所有參與的用戶界面上。
多玩家游戲: 很多游戲都是協(xié)同作戰(zhàn)的,玩家的操作和狀態(tài)肯定需要及時(shí)同步到所有玩家。
多人聊天: 很多場(chǎng)景下都需要多人參與討論聊天,用戶發(fā)送的消息得第一時(shí)間同步到所有用戶。
社交訂閱: 有時(shí)候我們需要及時(shí)收到訂閱消息,比如說開獎(jiǎng)通知,比如說在線邀請(qǐng),支付結(jié)果等。
股票虛擬貨幣價(jià)格: 股票和虛擬貨幣的價(jià)格都是實(shí)時(shí)波動(dòng)的,價(jià)格跟用戶的操作息息相關(guān),及時(shí)推送對(duì)用戶跟盤有很大的幫助。
六. WebSocket 中子協(xié)議支持
WebSocket 確實(shí)指定了一種消息傳遞體系結(jié)構(gòu),但并不強(qiáng)制使用任何特定的消息傳遞協(xié)議。而且它是 TCP 上的一個(gè)非常薄的層,它將字節(jié)流轉(zhuǎn)換為消息流(文本或二進(jìn)制)僅此而已。由應(yīng)用程序來解釋消息的含義。
與 HTTP(它是應(yīng)用程序級(jí)協(xié)議)不同,在 WebSocket 協(xié)議中,傳入消息中根本沒有足夠的信息供框架或容器知道如何路由或處理它。因此,對(duì)于非?,嵥榈膽?yīng)用程序而言 WebSocket 協(xié)議的級(jí)別可以說太低了。
可以做到的是引導(dǎo)在其上面再創(chuàng)建一層框架。這就相當(dāng)于當(dāng)今大多數(shù) Web 應(yīng)用程序使用的是 Web 框架,而不直接僅使用 Servlet API 進(jìn)行編碼一樣。
WebSocket RFC 定義了子協(xié)議的使用。在握手過程中,客戶機(jī)和服務(wù)器可以使用頭 Sec-WebSocket 協(xié)議商定子協(xié)議,即使不需要使用子協(xié)議,而是用更高的應(yīng)用程序級(jí)協(xié)議,但應(yīng)用程序仍需要選擇客戶端和服務(wù)器都可以理解的消息格式。且該格式可以是自定義的、特定于框架的或標(biāo)準(zhǔn)的消息傳遞協(xié)議。
Spring 框架支持使用 STOMP,這是一個(gè)簡單的消息傳遞協(xié)議,最初創(chuàng)建用于腳本語言,框架靈感來自 HTTP。STOMP 被廣泛支持,非常適合在 WebSocket 和 web 上使用。
七. 什么是 STOMP 協(xié)議
(1). STOMP 協(xié)議概述
“STOMP(Simple Text-Orientated Messaging Protocol)是一種簡單的面向文本的消息傳遞協(xié)議。
它提供了一個(gè)可互操作的連接格式,允許 STOMP 客戶端與任意 STOMP 消息代理(Broker)進(jìn)行交互。STOMP 協(xié)議由于設(shè)計(jì)簡單,易于開發(fā)客戶端,因此在多種語言和多種平臺(tái)上得到廣泛地應(yīng)用。
(2). 簡單介紹可以分為以下幾點(diǎn):
STOMP 是基于幀的協(xié)議,其幀以 HTTP 為模型。
STOMP 框架由命令,一組可選的標(biāo)頭和可選的主體組成。
STOMP 基于文本,但也允許傳輸二進(jìn)制消息。
STOMP 的默認(rèn)編碼為 UTF-8,但它支持消息正文的替代編碼的規(guī)范。
(3). STOMP 客戶端是一種用戶代理
作為生產(chǎn)者,通過 SEND 幀將消息發(fā)送到目標(biāo)服務(wù)器上。
作為消費(fèi)者,對(duì)目標(biāo)地址發(fā)送 SUBSCRIBE 幀,并作為 MESSAGE 幀從服務(wù)器接收消息。
(4). STOMP 幀
STOMP 是基于幀的協(xié)議,其幀以 HTTP 為模型。STOMP 結(jié)構(gòu)為:
COMMAND
header1:value1
header2:value2
Body^@
客戶端可以使用 SEND 或 SUBSCRIBE 命令發(fā)送或訂閱消息,還可以使用 “destination” 頭來描述消息的內(nèi)容和接收者。
這支持一種簡單的發(fā)布-訂閱機(jī)制,可用于通過代理將消息發(fā)送到其他連接的客戶端,或?qū)⑾l(fā)送到服務(wù)器以請(qǐng)求執(zhí)行某些工作。
(5). Stomp 常用幀
STOMP 的客戶端和服務(wù)器之間的通信是通過”幀“(Frame)實(shí)現(xiàn)的,每個(gè)幀由多”行“(Line)組成,其包含的幀如下:
Connecting Frames: CONNECT(連接) CONNECTED(成功連接) Client Frames: SEND(發(fā)送) SUBSRIBE(訂閱) UNSUBSCRIBE(取消訂閱) BEGIN(開始) COMMIT(提交) ABORT(中斷) ACK(確認(rèn))) NACK(否認(rèn))) DISCONNECT(斷開連接)) Server Frames: MESSAGE(消息)) RECEIPT(接收)) ERROR(錯(cuò)誤))
(6). Stomp 與 WebSocket 的關(guān)系
直接使用 WebSocket 就很類似于使用 TCP 套接字來編寫 Web 應(yīng)用,因?yàn)闆]有高層級(jí)的應(yīng)用協(xié)議(wire protocol),因而就需要我們定義應(yīng)用之間所發(fā)送消息的語義,還需要確保連接的兩端都能遵循這些語義。
同 HTTP 在 TCP 套接字上添加請(qǐng)求-響應(yīng)模型層一樣,STOMP 在 WebSocket 之上提供了一個(gè)基于幀的線路格式層,用來定義消息語義。
(7). 使用 STOMP 作為 WebSocket 子協(xié)議的好處
無需發(fā)明自定義消息格式 在瀏覽器中 使用現(xiàn)有的stomp.js客戶端 能夠根據(jù)目的地將消息路由到 可以使用成熟的消息代理(例如 RabbitMQ,ActiveMQ等)進(jìn)行廣播的選項(xiàng)使用 STOMP(相對(duì)于普通WebSocket)使Spring Framework能夠?yàn)閼?yīng)用程序級(jí)使用提供編程模型,就像Spring MVC提供基于HTTP的編程模型一樣。
八. Spring 封裝的 STOMP
使用 Spring 的 STOMP 支持時(shí),Spring WebSocket 應(yīng)用程序充當(dāng)客戶端的 STOMP 代理。
消息被路由到 @Controller 消息處理方法或簡單的內(nèi)存中代理,該代理跟蹤訂閱并向訂閱的用戶廣播消息。
還可以將 Spring 配置為與專用的 STOMP 代理(例如 RabbitMQ,ActiveMQ等)一起使用,以實(shí)際廣播消息。在那種情況下,Spring 維護(hù)與代理的 TCP 連接,將消息中繼到該代理,并將消息從該代理向下傳遞到已連接的 WebSocket 客戶端。
因此 Spring Web 應(yīng)用程序可以依賴基于統(tǒng)一 HTTP 的安全性,通用驗(yàn)證以及熟悉的編程模型消息處理工作。
Spring 官方提供的處理流圖:

上面中的一些概念關(guān)鍵詞:
Message: 消息,里面帶有header和payload。MessageHandler: 處理client消息的實(shí)體。MessageChannel: 解耦消息發(fā)送者與消息接收者的實(shí)體clientInboundChannel:用于從 WebSocket 客戶端接收消息。clientOutboundChannel:用于將服務(wù)器消息發(fā)送給 WebSocket 客戶端。brokerChannel:用于從服務(wù)器端、應(yīng)用程序中向消息代理發(fā)送消息Broker: 存放消息的中間件,client可以訂閱broker中的消息。
上面的設(shè)置包括3個(gè)消息通道:
clientInboundChannel: 用于來自WebSocket客戶端的消息。clientOutboundChannel: 用于向WebSocket客戶端發(fā)送消息。brokerChannel: 從應(yīng)用程序內(nèi)部發(fā)送給代理的消息。
九. 示例一:實(shí)現(xiàn)簡單的廣播模式
WebSocket 常分為廣播與隊(duì)列模式,廣播模式是向訂閱廣播的用戶發(fā)送信息,只要訂閱相關(guān)廣播就能收到對(duì)應(yīng)信息。
隊(duì)列模式常用于點(diǎn)對(duì)點(diǎn)模式,為單個(gè)用戶向另一個(gè)用戶發(fā)送信息,這里先介紹下廣播模式的實(shí)現(xiàn)示例。
1. Maven 引入相關(guān)依賴
這里使用 Maven 工具管理依賴包,分別引入下面依賴:
lombok: Lombok 工具依賴,便于生成實(shí)體對(duì)象的 Get 與 Set 方法。spring-boot-starter-websocket:SpringBoot 實(shí)現(xiàn) WebSocket 的依賴,里面對(duì) WebSocket 進(jìn)行了一些列封裝,并且也包含了 SpringBoot Web 依賴。
<dependencies>
????????
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-websocketartifactId>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.projectlombokgroupId>
????????????<artifactId>lombokartifactId>
????????????<scope>providedscope>
????????dependency>
dependencies>
2. 創(chuàng)建測(cè)試實(shí)體類
創(chuàng)建便于傳輸消息的實(shí)體類,里面字段內(nèi)容如下:
import?lombok.Data;
@Data
public?class?MessageBody?{
????/**?消息內(nèi)容?*/
????private?String?content;
????/**?廣播轉(zhuǎn)發(fā)的目標(biāo)地址(告知?STOMP?代理轉(zhuǎn)發(fā)到哪個(gè)地方)?*/
????private?String?destination;
}
3. 創(chuàng)建 WebSocket 配置類
創(chuàng)建 WebSocket 配置類,配置進(jìn)行連接注冊(cè)的端點(diǎn) /mydlq 和消息代理前綴 /topic 及接收客戶端發(fā)送消息的前綴 /app。
@Configuration
@EnableWebSocketMessageBroker
public?class?WebSocketConfig?implements?WebSocketMessageBrokerConfigurer?{
????/**
?????*?配置?WebSocket?進(jìn)入點(diǎn),及開啟使用?SockJS,這些配置主要用配置連接端點(diǎn),用于?WebSocket?連接
?????*
?????*?@param?registry?STOMP?端點(diǎn)
?????*/
????@Override
????public?void?registerStompEndpoints(StompEndpointRegistry?registry)?{
????????registry.addEndpoint("/mydlq").withSockJS();
????}
????/**
?????*?配置消息代理選項(xiàng)
?????*
?????*?@param?registry?消息代理注冊(cè)配置
?????*/
????@Override
????public?void?configureMessageBroker(MessageBrokerRegistry?registry)?{
????????//?設(shè)置一個(gè)或者多個(gè)代理前綴,在?Controller?類中的方法里面發(fā)生的消息,會(huì)首先轉(zhuǎn)發(fā)到代理從而發(fā)送到對(duì)應(yīng)廣播或者隊(duì)列中。
????????registry.enableSimpleBroker("/topic");
????????//?配置客戶端發(fā)送請(qǐng)求消息的一個(gè)或多個(gè)前綴,該前綴會(huì)篩選消息目標(biāo)轉(zhuǎn)發(fā)到?Controller?類中注解對(duì)應(yīng)的方法里
????????registry.setApplicationDestinationPrefixes("/app");
????}
}
4. 創(chuàng)建測(cè)試 Controller 類
創(chuàng)建 Controller 類,該類也類似于正常 Web 項(xiàng)目中 Controller 寫法一樣,在方法上面添加 @MessageMapping 注解,當(dāng)客戶端發(fā)送消息請(qǐng)求的前綴匹配上 WebSocket 配置類中的 /app 前綴后,會(huì)進(jìn)入到 Controller 類中進(jìn)行匹配,如果匹配成功則執(zhí)行注解所在的方法內(nèi)容。
@Controller
public?class?MessageController?{
????/**?消息發(fā)送工具對(duì)象?*/
????@Autowired
????private?SimpMessageSendingOperations?simpMessageSendingOperations;
????/**?廣播發(fā)送消息,將消息發(fā)送到指定的目標(biāo)地址?*/
????@MessageMapping("/test")
????public?void?sendTopicMessage(MessageBody?messageBody)?{
????????//?將消息發(fā)送到?WebSocket?配置類中配置的代理中(/topic)進(jìn)行消息轉(zhuǎn)發(fā)
????????simpMessageSendingOperations.convertAndSend(messageBody.getDestination(),?messageBody);
????}
}
5. 創(chuàng)建測(cè)試腳本
創(chuàng)建用于操作 WebSocket 的 JS 文件 app-websocket.js,內(nèi)容如下:
//?設(shè)置?STOMP?客戶端
var?stompClient?=?null;
//?設(shè)置?WebSocket?進(jìn)入端點(diǎn)
var?SOCKET_ENDPOINT?=?"/mydlq";
//?設(shè)置訂閱消息的請(qǐng)求前綴
var?SUBSCRIBE_PREFIX?=?"/topic"
//?設(shè)置訂閱消息的請(qǐng)求地址
var?SUBSCRIBE?=?"";
//?設(shè)置服務(wù)器端點(diǎn),訪問服務(wù)器中哪個(gè)接口
var?SEND_ENDPOINT?=?"/app/test";
/*?進(jìn)行連接?*/
function?connect()?{
????//?設(shè)置?SOCKET
????var?socket?=?new?SockJS(SOCKET_ENDPOINT);
????//?配置?STOMP?客戶端
????stompClient?=?Stomp.over(socket);
????//?STOMP?客戶端連接
????stompClient.connect({},?function?(frame)?{
????????alert("連接成功");
????});
}
/*?訂閱信息?*/
function?subscribeSocket(){
????//?設(shè)置訂閱地址
????SUBSCRIBE?=?SUBSCRIBE_PREFIX?+?$("#subscribe").val();
????//?輸出訂閱地址
????alert("設(shè)置訂閱地址為:"?+?SUBSCRIBE);
????//?執(zhí)行訂閱消息
????stompClient.subscribe(SUBSCRIBE,?function?(responseBody)?{
????????var?receiveMessage?=?JSON.parse(responseBody.body);
????????$("#information").append(""?+?receiveMessage.content?+?" ");
????});
}
/*?斷開連接?*/
function?disconnect()?{
????stompClient.disconnect(function()?{
????????alert("斷開連接");
????});
}
/*?發(fā)送消息并指定目標(biāo)地址(這里設(shè)置的目標(biāo)地址為自身訂閱消息的地址,當(dāng)然也可以設(shè)置為其它地址)?*/
function?sendMessageNoParameter()?{
????//?設(shè)置發(fā)送的內(nèi)容
????var?sendContent?=?$("#content").val();
????//?設(shè)置待發(fā)送的消息內(nèi)容
????var?message?=?'{"destination":?"'?+?SUBSCRIBE?+?'",?"content":?"'?+?sendContent?+?'"}';
????//?發(fā)送消息
????stompClient.send(SEND_ENDPOINT,?{},?message);
}
6. 創(chuàng)建 WebSocket HTML
創(chuàng)建用于展示 WebSocket 相關(guān)功能的 WEB HTML 頁面 index.html,內(nèi)容如下:
html>
<html>
<head>
????<title>Hello?WebSockettitle>
????<link?href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css"?rel="stylesheet">
????<meta?http-equiv="Content-Type"?content="text/html;?charset=utf-8"/>
????<script?src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js">script>
????<script?src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js">script>
????<script?src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js">script>
????<script?src="app-websocket.js">script>
head>
<body>
????<div?id="main-content"?class="container"?style="margin-top:?10px;">
????????<div?class="row">
????????????<form?class="navbar-form"?style="margin-left:0px">
????????????????<div?class="col-md-12">
????????????????????<div?class="form-group">
????????????????????????<label>WebSocket?連接:label>
????????????????????????<button?class="btn?btn-primary"?type="button"?onclick="connect();">進(jìn)行連接button>
????????????????????????<button?class="btn?btn-danger"?type="button"?onclick="disconnect();">斷開連接button>
????????????????????div>
????????????????????<label>訂閱地址:label>
????????????????????<div?class="form-group">
????????????????????????<input?type="text"?id="subscribe"?class="form-control"?placeholder="訂閱地址">
????????????????????div>
????????????????????<button?class="btn?btn-warning"?onclick="subscribeSocket();"?type="button">訂閱button>
????????????????div>
????????????form>
????????div>
????????br>
????????<div?class="row">
????????????<div?class="form-group">
????????????????<label?for="content">發(fā)送的消息內(nèi)容:label>
????????????????<input?type="text"?id="content"?class="form-control"?placeholder="消息內(nèi)容">
????????????div>
????????????<button?class="btn?btn-info"?onclick="sendMessageNoParameter();"?type="button">發(fā)送button>
????????div>
????????br>
????????<div?class="row">
????????????<div?class="col-md-12">
????????????????<h5?class="page-header"?style="font-weight:bold">接收到的消息:h5>
????????????????<table?class="table?table-striped">
????????????????????<tbody?id="information">tbody>
????????????????table>
????????????div>
????????div>
????div>
body>
html>
7. 啟動(dòng)并進(jìn)行測(cè)試
輸入地址 http://localhost:8080/index.html 訪問測(cè)試的前端頁面,然后執(zhí)行下面步驟進(jìn)行測(cè)試:
點(diǎn)擊進(jìn)行連接按鈕,連接 WebSocket 服務(wù)端; 在訂閱地址欄輸入訂閱地址(因?yàn)楸救嗽O(shè)置的訂閱地址和接收消息的地址是一個(gè),所以隨意輸入); 點(diǎn)擊訂閱按鈕訂閱對(duì)應(yīng)地址的消息; 在發(fā)送消息內(nèi)容的輸入框中輸入 hello world!,然后點(diǎn)擊發(fā)送按鈕發(fā)送消息;
執(zhí)行完上面步驟成后,可以觀察到成功接收到訂閱地址的消息,如下:

十. 示例二:實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)模式(引入 Spring Security 實(shí)現(xiàn)鑒權(quán))
1. Maven 引入相關(guān)依賴
這里使用 Maven 工具管理依賴包,分別引入下面依賴:
lombok: Lombok 工具依賴,便于生成實(shí)體對(duì)象的 Get 與 Set 方法。spring-boot-starter-websocket:SpringBoot 實(shí)現(xiàn) WebSocket 的依賴,里面對(duì) WebSocket 進(jìn)行了一些列封裝,并且也包含了 SpringBoot Web 依賴。spring-boot-starter-security:Spring Security,這是一種基于 Spring AOP 和 Servlet 過濾器的安全框架。它提供全面的安全性解決方案,同時(shí)在 Web 請(qǐng)求級(jí)和方法調(diào)用級(jí)處理身份確認(rèn)和授權(quán)。
<dependencies>
????????
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-websocketartifactId>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-securityartifactId>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.projectlombokgroupId>
????????????<artifactId>lombokartifactId>
????????????<scope>providedscope>
????????dependency>
????dependencies>
2. 創(chuàng)建測(cè)試實(shí)體類
創(chuàng)建便于傳輸消息的實(shí)體類,里面字段內(nèi)容如下:
@Data
public?class?MessageBody?{
????/**?發(fā)送消息的用戶?*/
????private?String?from;
????/**?消息內(nèi)容?*/
????private?String?content;
????/**?目標(biāo)用戶(告知?STOMP?代理轉(zhuǎn)發(fā)到哪個(gè)用戶)?*/
????private?String?targetUser;
????/**?廣播轉(zhuǎn)發(fā)的目標(biāo)地址(告知?STOMP?代理轉(zhuǎn)發(fā)到哪個(gè)地方)?*/
????private?String?destination;
}
3. 創(chuàng)建 WebSocket 配置類
創(chuàng)建 WebSocket 配置類,配置進(jìn)行連接注冊(cè)的端點(diǎn)/mydlq 和消息代理前綴 /queue 及接收客戶端發(fā)送消息的前綴 /app。
@Configuration
@EnableWebSocketMessageBroker
public?class?WebSocketConfig?implements?WebSocketMessageBrokerConfigurer?{
????/**
?????*?配置?WebSocket?進(jìn)入點(diǎn),及開啟使用?SockJS,這些配置主要用配置連接端點(diǎn),用于?WebSocket?連接
?????*
?????*?@param?registry?STOMP?端點(diǎn)
?????*/
????@Override
????public?void?registerStompEndpoints(StompEndpointRegistry?registry)?{
????????registry.addEndpoint("/mydlq").withSockJS();
????}
????/**
?????*?配置消息代理選項(xiàng)
?????*
?????*?@param?registry?消息代理注冊(cè)配置
?????*/
????@Override
????public?void?configureMessageBroker(MessageBrokerRegistry?registry)?{
????????//?設(shè)置一個(gè)或者多個(gè)代理前綴,在?Controller?類中的方法里面發(fā)生的消息,會(huì)首先轉(zhuǎn)發(fā)到代理從而發(fā)送到對(duì)應(yīng)廣播或者隊(duì)列中。
????????registry.enableSimpleBroker("/queue");
????????//?配置客戶端發(fā)送請(qǐng)求消息的一個(gè)或多個(gè)前綴,該前綴會(huì)篩選消息目標(biāo)轉(zhuǎn)發(fā)到?Controller?類中注解對(duì)應(yīng)的方法里
????????registry.setApplicationDestinationPrefixes("/app");
????????//?服務(wù)端通知特定用戶客戶端的前綴,可以不設(shè)置,默認(rèn)為user
????????registry.setUserDestinationPrefix("/user");
????}
}
5. 創(chuàng)建 Security 配置
Spring Security 的配置類,可以在該類中配置權(quán)限認(rèn)證及測(cè)試的兩個(gè)用戶相關(guān)信息:
測(cè)試用戶名/密碼1:mydlq1/123456 測(cè)試用戶名/密碼2:mydlq2/123456
@Configuration
public?class?SecurityConfig?extends?WebSecurityConfigurerAdapter?{
????/**
?????*?設(shè)置密碼編碼的配置參數(shù),這里設(shè)置為?NoOpPasswordEncoder,不配置密碼加密,方便測(cè)試。
?????*
?????*?@return?密碼編碼實(shí)例
?????*/
????@Bean
????PasswordEncoder?passwordEncoder()?{
????????return?NoOpPasswordEncoder.getInstance();
????}
????/**
?????*?設(shè)置權(quán)限認(rèn)證參數(shù),這里用于創(chuàng)建兩個(gè)用于測(cè)試的用戶信息。
?????*
?????*?@param?auth?SecurityBuilder?用于創(chuàng)建?AuthenticationManager。
?????*?@throws?Exception?拋出的異常
?????*/
????@Override
????protected?void?configure(AuthenticationManagerBuilder?auth)?throws?Exception?{
????????auth.inMemoryAuthentication()
????????????????.withUser("mydlq1")
????????????????.password("123456")
????????????????.roles("admin")
????????????????.and()
????????????????.withUser("mydlq2")
????????????????.password("123456")
????????????????.roles("admin");
????}
????/**
?????*?設(shè)置?HTTP?安全相關(guān)配置參數(shù)
?????*
?????*?@param?http?HTTP?Security?對(duì)象
?????*?@throws?Exception?拋出的異常信息
?????*/
????@Override
????protected?void?configure(HttpSecurity?http)?throws?Exception?{
????????http.authorizeRequests()
????????????????.anyRequest().authenticated()
????????????????.and()
????????????????.formLogin()
????????????????.permitAll();
????}
}
5. 創(chuàng)建測(cè)試 Controller 類
跟上面介紹廣播模式一樣,作用也是根據(jù) WebSocket 配置類中 /app 前綴匹配后進(jìn)入 Controller 類進(jìn)行邏輯處理操作。
@Controller
public?class?MessageController?{
????@Autowired
????private?SimpMessageSendingOperations?simpMessageSendingOperations;
????/**
?????*?點(diǎn)對(duì)點(diǎn)發(fā)送消息,將消息發(fā)送到指定用戶
?????*/
????@MessageMapping("/test")
????public?void?sendUserMessage(Principal?principal,?MessageBody?messageBody)?{
????????//?設(shè)置發(fā)送消息的用戶
????????messageBody.setFrom(principal.getName());
????????//?調(diào)用?STOMP?代理進(jìn)行消息轉(zhuǎn)發(fā)
????????simpMessageSendingOperations.convertAndSendToUser(messageBody.getTargetUser(),?messageBody.getDestination(),?messageBody);
????}
}
6. 創(chuàng)建 WebSocket JS
創(chuàng)建用于操作 WebSocket 的 JS 文件 app-websocket.js,內(nèi)容如下:
//?設(shè)置?STOMP?客戶端
var?stompClient?=?null;
//?設(shè)置?WebSocket?進(jìn)入端點(diǎn)
var?SOCKET_ENDPOINT?=?"/mydlq";
//?設(shè)置訂閱消息的請(qǐng)求前綴
var?SUBSCRIBE_PREFIX?=?"/topic"
//?設(shè)置訂閱消息的請(qǐng)求地址
var?SUBSCRIBE?=?"";
//?設(shè)置服務(wù)器端點(diǎn),訪問服務(wù)器中哪個(gè)接口
var?SEND_ENDPOINT?=?"/app/test";
/*?進(jìn)行連接?*/
function?connect()?{
????//?設(shè)置?SOCKET
????var?socket?=?new?SockJS(SOCKET_ENDPOINT);
????//?配置?STOMP?客戶端
????stompClient?=?Stomp.over(socket);
????//?STOMP?客戶端連接
????stompClient.connect({},?function?(frame)?{
????????alert("連接成功");
????});
}
/*?訂閱信息?*/
function?subscribeSocket(){
????//?設(shè)置訂閱地址
????SUBSCRIBE?=?SUBSCRIBE_PREFIX?+?$("#subscribe").val();
????//?輸出訂閱地址
????alert("設(shè)置訂閱地址為:"?+?SUBSCRIBE);
????//?執(zhí)行訂閱消息
????stompClient.subscribe(SUBSCRIBE,?function?(responseBody)?{
????????var?receiveMessage?=?JSON.parse(responseBody.body);
????????$("#information").append(""?+?receiveMessage.content?+?" ");
????});
}
/*?斷開連接?*/
function?disconnect()?{
????stompClient.disconnect(function()?{
????????alert("斷開連接");
????});
}
/*?發(fā)送消息并指定目標(biāo)地址?*/
function?sendMessageNoParameter()?{
????//?設(shè)置發(fā)送的內(nèi)容
????var?sendContent?=?$("#content").val();
????//?設(shè)置待發(fā)送的消息內(nèi)容
????var?message?=?'{"destination":?"'?+?SUBSCRIBE?+?'",?"content":?"'?+?sendContent?+?'"}';
????//?發(fā)送消息
????stompClient.send(SEND_ENDPOINT,?{},?message);
}
7. 創(chuàng)建 WebSocket HTML
創(chuàng)建用于展示 WebSocket 相關(guān)功能的 WEB HTML 頁面 index.html,內(nèi)容如下:
html>
<html>
<head>
????<title>Hello?WebSockettitle>
????<link?href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css"?rel="stylesheet">
????<meta?http-equiv="Content-Type"?content="text/html;?charset=utf-8"/>
????<script?src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js">script>
????<script?src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js">script>
????<script?src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js">script>
????<script?src="app-websocket.js">script>
head>
<body>
????<div?id="main-content"?class="container"?style="margin-top:?10px;">
????????<div?class="row">
????????????<form?class="navbar-form"?style="margin-left:0px">
????????????????<div?class="col-md-12">
????????????????????<div?class="form-group">
????????????????????????<label>WebSocket?連接:label>
????????????????????????<button?class="btn?btn-primary"?type="button"?onclick="connect();">進(jìn)行連接button>
????????????????????????<button?class="btn?btn-danger"?type="button"?onclick="disconnect();">斷開連接button>
????????????????????div>
????????????????????<label>訂閱地址:label>
????????????????????<div?class="form-group">
????????????????????????<input?type="text"?id="subscribe"?class="form-control"?placeholder="訂閱地址">
????????????????????div>
????????????????????<button?class="btn?btn-warning"?onclick="subscribeSocket();"?type="button">訂閱button>
????????????????div>
????????????form>
????????div>
????????br>
????????<div?class="row">
????????????<div?class="form-group">
????????????????<label?for="content">發(fā)送的消息內(nèi)容:label>
????????????????<input?type="text"?id="content"?class="form-control"?placeholder="消息內(nèi)容">
????????????div>
????????????<button?class="btn?btn-info"?onclick="sendMessageNoParameter();"?type="button">發(fā)送button>
????????div>
????????br>
????????<div?class="row">
????????????<div?class="col-md-12">
????????????????<h5?class="page-header"?style="font-weight:bold">接收到的消息:h5>
????????????????<table?class="table?table-striped">
????????????????????<tbody?id="information">tbody>
????????????????table>
????????????div>
????????div>
????div>
body>
html>
8. 啟動(dòng)并進(jìn)行測(cè)試
為了方便測(cè)試,需要打開兩個(gè)不同類型瀏覽器(因?yàn)橛脩舻卿浐髸?huì)存 Session,如果一個(gè)瀏覽器不同用戶登錄會(huì)使之前 Session 失效)來進(jìn)行測(cè)試,兩個(gè)瀏覽器同時(shí)輸入地址 http://localhost:8080/index.html 訪問測(cè)試的前端頁面,然后可以看到并沒有進(jìn)入 /index.html 頁面,而是跳轉(zhuǎn)到Spring Security 提供的登錄的 /login 頁面,如下:

兩個(gè)瀏覽器中都輸入用戶名/密碼 mydlq1/123456 與 mydlq2/123456 進(jìn)行登錄,然后會(huì)回到 /index.html 頁面,然后執(zhí)行下面步驟進(jìn)行測(cè)試:
”瀏覽器1”和”瀏覽器2”點(diǎn)擊”進(jìn)行連接”按鈕,連接 WebSocket 服務(wù)端;
”瀏覽器1”和”瀏覽器2”中同時(shí)設(shè)置訂閱地址為”/abc”,然后點(diǎn)擊訂閱按鈕進(jìn)行消息訂閱;
”瀏覽器1”(用戶
mydlq1)設(shè)置發(fā)送目標(biāo)用戶為”/mydlq2”,”瀏覽器2”(用戶mydlq2)設(shè)置發(fā)送目標(biāo)用戶為”/mydlq1”;”瀏覽器1”(用戶
mydlq1)設(shè)置發(fā)送消息為Hi, I’m mydlq1,”瀏覽器2”(用戶mydlq2)設(shè)置發(fā)送消息為Hi, I’m mydlq2;點(diǎn)擊
發(fā)送按鈕發(fā)送消息;
執(zhí)行完上面步驟成后,可以在兩個(gè)不同瀏覽器中觀察到如下內(nèi)容:

十一. 示例三:實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)模式(根據(jù)請(qǐng)求頭 Header 實(shí)現(xiàn)鑒權(quán))
1. Maven 引入相關(guān)依賴
“同示例二
2. 創(chuàng)建測(cè)試實(shí)體類
@Data
public?class?MessageBody?{
????/**?發(fā)送消息的用戶?*/
????private?String?from;
????/**?消息內(nèi)容?*/
????private?String?content;
????/**?目標(biāo)用戶(告知?STOMP?代理轉(zhuǎn)發(fā)到哪個(gè)用戶)?*/
????private?String?targetUser;
????/**?廣播轉(zhuǎn)發(fā)的目標(biāo)地址(告知?STOMP?代理轉(zhuǎn)發(fā)到哪個(gè)地方)?*/
????private?String?destination;
}
@Data
@AllArgsConstructor
public?class?User?{
????private?String?username;
????private?String?token;
}
3. 配置 WebSocket 通道攔截器
配置 WebSocket 通道攔截器,里面添加兩個(gè)模擬用戶:
用戶 mydlq1,Token:123456-1用戶 mydlq2,Token:123456-2
/**
?*?WebSocket?通道攔截器(這里模擬兩個(gè)測(cè)試?Token?方便測(cè)試,不做具體?Token?鑒權(quán)實(shí)現(xiàn))
?*
?*?@author?mydlq
?*/
public?class?MyChannelInterceptor?implements?ChannelInterceptor?{
????/**?測(cè)試用戶與?token?1?*/
????private?User?mydlq1?=?new?User("","123456-1");
????/**?測(cè)試用戶與?token?2?*/
????private?User?mydlq2?=?new?User("","123456-2");
????/**
?????*?從?Header?中獲取?Token?進(jìn)行驗(yàn)證,根據(jù)不同的?Token?區(qū)別用戶
?????*
?????*?@param?message?消息對(duì)象
?????*?@param?channel?通道對(duì)象
?????*?@return?驗(yàn)證后的用戶信息
?????*/
????@Override
????public?Message>?preSend(Message>?message,?MessageChannel?channel)?{
????????StompHeaderAccessor?accessor?=?MessageHeaderAccessor.getAccessor(message,?StompHeaderAccessor.class);
????????String?token?=?getToken(message);
????????if?(token!=null?&&?accessor?!=?null?&&?StompCommand.CONNECT.equals(accessor.getCommand()))?{
????????????Principal?user?=?null;
????????????//?提前創(chuàng)建好兩個(gè)測(cè)試?token?進(jìn)行匹配,方便測(cè)試
????????????if?(mydlq1.getToken().equals(token)){
????????????????user?=?()?->?mydlq1.getUsername();
????????????}?else?if?(mydlq2.getToken().equals(token)){
????????????????user?=?()?->?mydlq2.getUsername();
????????????}
????????????accessor.setUser(user);
????????}
????????return?message;
????}
????/**
?????*?從?Header?中獲取?TOKEN
?????*
?????*?@param?message?消息對(duì)象
?????*?@return?TOKEN
?????*/
????private?String?getToken(Message>?message){
????????Map?headers?=?(Map)?message.getHeaders().get("nativeHeaders");
????????if?(headers?!=null?&&?headers.containsKey("token")){
????????????List?token?=?(List)headers.get("token");
????????????return?String.valueOf(token.get(0));
????????}
????????return?null;
????}
}
4. 創(chuàng)建 WebSocket 配置類
創(chuàng)建 WebSocket 配置類,配置進(jìn)行連接注冊(cè)的端點(diǎn) /mydlq 和消息代理前綴 /queue 及接收客戶端發(fā)送消息的前綴 /app。
@Configuration
@EnableWebSocketMessageBroker
public?class?WebSocketConfig?implements?WebSocketMessageBrokerConfigurer?{
????/**
?????*?配置?WebSocket?進(jìn)入點(diǎn),及開啟使用?SockJS,這些配置主要用配置連接端點(diǎn),用于?WebSocket?連接
?????*
?????*?@param?registry?STOMP?端點(diǎn)
?????*/
????@Override
????public?void?registerStompEndpoints(StompEndpointRegistry?registry)?{
????????registry.addEndpoint("/mydlq").withSockJS();
????}
????/**
?????*?配置消息代理選項(xiàng)
?????*
?????*?@param?registry?消息代理注冊(cè)配置
?????*/
????@Override
????public?void?configureMessageBroker(MessageBrokerRegistry?registry)?{
????????//?設(shè)置一個(gè)或者多個(gè)代理前綴,在?Controller?類中的方法里面發(fā)生的消息,會(huì)首先轉(zhuǎn)發(fā)到代理從而發(fā)送到對(duì)應(yīng)廣播或者隊(duì)列中。
????????registry.enableSimpleBroker("/queue");
????????//?配置客戶端發(fā)送請(qǐng)求消息的一個(gè)或多個(gè)前綴,該前綴會(huì)篩選消息目標(biāo)轉(zhuǎn)發(fā)到?Controller?類中注解對(duì)應(yīng)的方法里
????????registry.setApplicationDestinationPrefixes("/app");
????????//?服務(wù)端通知特定用戶客戶端的前綴,可以不設(shè)置,默認(rèn)為user
????????registry.setUserDestinationPrefix("/user");
????}
????/**
?????*?配置通道攔截器,用于獲取?Header?的?Token?進(jìn)行鑒權(quán)
?????*
?????*?@param?registration?注冊(cè)通道配置類
?????*/
????@Override
????public?void?configureClientInboundChannel(ChannelRegistration?registration)?{
????????registration.interceptors(new?MyChannelInterceptor());
????}
}
5. 創(chuàng)建測(cè)試 Controller 類
@Controller
public?class?MessageController?{
????@Autowired
????private?SimpMessageSendingOperations?simpMessageSendingOperations;
????/**
?????*?點(diǎn)對(duì)點(diǎn)發(fā)送消息,將消息發(fā)送到指定用戶
?????*/
????@MessageMapping("/test")
????public?void?sendUserMessage(Principal?principal,?MessageBody?messageBody)?{
????????//?設(shè)置發(fā)送消息的用戶
????????messageBody.setFrom(principal.getName());
????????//?調(diào)用?STOMP?代理進(jìn)行消息轉(zhuǎn)發(fā)
????????simpMessageSendingOperations.convertAndSendToUser(messageBody.getTargetUser(),?messageBody.getDestination(),?messageBody);
????}
}
6. 創(chuàng)建 WebSocket JS
創(chuàng)建用于操作 WebSocket 的 JS 文件 app-websocket.js,內(nèi)容如下:
//?設(shè)置?STOMP?客戶端
var?stompClient?=?null;
//?設(shè)置?WebSocket?進(jìn)入端點(diǎn)
var?SOCKET_ENDPOINT?=?"/mydlq";
//?設(shè)置訂閱消息的請(qǐng)求地址前綴
var?SUBSCRIBE_PREFIX??=?"/queue";
//?設(shè)置訂閱地址
var?SUBSCRIBE?=?"";
//?設(shè)置服務(wù)器端點(diǎn),訪問服務(wù)器中哪個(gè)接口
var?SEND_ENDPOINT?=?"/app/test";
/*?進(jìn)行連接?*/
function?connect()?{
????//?設(shè)置?SOCKET
????var?socket?=?new?SockJS(SOCKET_ENDPOINT);
????//?配置?STOMP?客戶端
????stompClient?=?Stomp.over(socket);
????//?獲取?TOKEN
????var?myToken?=?$("#myToken").val();
????//?STOMP?客戶端連接
????stompClient.connect({token:?myToken},?function?(frame)?{
????????alert("連接成功");
????});
}
/*?訂閱信息?*/
function?subscribeSocket(){
????//?設(shè)置訂閱地址
????SUBSCRIBE?=?SUBSCRIBE_PREFIX?+?$("#subscribe").val();
????//?輸出訂閱地址
????alert("設(shè)置訂閱地址為:"?+?SUBSCRIBE);
????//?執(zhí)行訂閱消息
????stompClient.subscribe("/user"?+?SUBSCRIBE,?function?(responseBody)?{
????????var?receiveMessage?=?JSON.parse(responseBody.body);
????????console.log(receiveMessage);
????????$("#information").append(""?+?receiveMessage.content?+?" ");
????});
}
/*?斷開連接?*/
function?disconnect()?{
????stompClient.disconnect(function()?{
????????alert("斷開連接");
????});
}
/*?發(fā)送消息并指定目標(biāo)地址?*/
function?sendMessageNoParameter()?{
????//?設(shè)置發(fā)送的內(nèi)容
????var?sendContent?=?$("#content").val();
????//?設(shè)置發(fā)送的用戶
????var?sendUser?=?$("#targetUser").val();
????//?設(shè)置待發(fā)送的消息內(nèi)容
????var?message?=?'{"targetUser":"'?+?sendUser?+?'",?"destination":?"'?+?SUBSCRIBE?+?'",?"content":?"'?+?sendContent?+?'"}';
????//?發(fā)送消息
????stompClient.send(SEND_ENDPOINT,?{},?message);
}
7. 創(chuàng)建 WebSocket HTML
創(chuàng)建用于展示 WebSocket 相關(guān)功能的 WEB HTML 頁面 index.html,內(nèi)容如下:
html>
<html>
<head>
????<title>Hello?WebSockettitle>
????<link?href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css"?rel="stylesheet">
????<meta?http-equiv="Content-Type"?content="text/html;?charset=utf-8"/>
????<script?src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js">script>
????<script?src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js">script>
????<script?src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js">script>
????<script?src="app-websocket.js">script>
head>
<body>
<div?id="main-content"?class="container"?style="margin-top:?10px;">
????<div?class="row">
????????<form?class="navbar-form"?style="margin-left:0px">
????????????<div?class="col-md-12">
????????????????<div?class="form-group">
????????????????????<label>WebSocket?連接:label>
????????????????????<button?class="btn?btn-primary"?type="button"?onclick="connect();">進(jìn)行連接button>
????????????????????<button?class="btn?btn-danger"?type="button"?onclick="disconnect();">斷開連接button>
????????????????div>
????????????????<label>訂閱地址:label>
????????????????<div?class="form-group">
????????????????????<input?type="text"?id="subscribe"?class="form-control"?placeholder="訂閱地址">
????????????????div>
????????????????<button?class="btn?btn-warning"?onclick="subscribeSocket();"?type="button">訂閱button>
????????????div>
????????form>
????div>
????br>
????<div?class="row">
????????<div?class="form-group">
????????????<label>TOKEN?信息:label>
????????????<input?type="text"?id="myToken"?class="form-control"?placeholder="TOKEN?信息">
????????????<label>發(fā)送的目標(biāo)用戶:label>
????????????<input?type="text"?id="targetUser"?class="form-control"?placeholder="發(fā)送的用戶">
????????????<label?for="content">發(fā)送的消息內(nèi)容:label>
????????????<input?type="text"?id="content"?class="form-control"?placeholder="消息的內(nèi)容">
????????div>
????????<button?class="btn?btn-info"?onclick="sendMessageNoParameter();"?type="button">發(fā)送button>
????div>
????br>
????<div?class="row">
????????<div?class="col-md-12">
????????????<h5?class="page-header"?style="font-weight:bold">接收到的消息:h5>
????????????<table?class="table?table-striped">
????????????????<tbody?id="information">tbody>
????????????table>
????????div>
????div>
div>
body>
html>
8. 啟動(dòng)并進(jìn)行測(cè)試
為了方便測(cè)試,需要打開兩個(gè)不同類型瀏覽器(這里模擬通過 Header 傳 Token 的方式進(jìn)行用戶驗(yàn)證,具體登錄邏輯不實(shí)現(xiàn),而是直接使用事先配置好的兩個(gè)用戶 Token 進(jìn)行模擬)來進(jìn)行測(cè)試,兩個(gè)瀏覽器同時(shí)輸入地址 http://localhost:8080/index.html 訪問測(cè)試的前端頁面 ``/index.html` 如下:
瀏覽器1: 用戶:mydlq1 Token:123456789-1 瀏覽器2: 登錄的用戶:mydlq2 Token:123456789-2
兩個(gè)瀏覽器中都執(zhí)行下面步驟進(jìn)行測(cè)試:
瀏覽器1和瀏覽器2點(diǎn)擊進(jìn)行連接按鈕,連接 WebSocket 服務(wù)端; 瀏覽器1和瀏覽器2中同時(shí)設(shè)置訂閱地址為 /abc,然后點(diǎn)擊訂閱按鈕進(jìn)行消息訂閱;瀏覽器1(用戶 mydlq1)在 TOken 信息一欄中填寫模擬用戶 mydlq1 的 Token 串,瀏覽器2(用戶 mydlq2)填寫模擬用戶 mydlq2 的 Token 串; 瀏覽器1(用戶 mydlq1)設(shè)置發(fā)送目標(biāo)用戶為 /mydlq2,瀏覽器2(用戶 mydlq2)設(shè)置發(fā)送目標(biāo)用戶為/mydlq1;瀏覽器1(用戶 mydlq1)設(shè)置發(fā)送消息為Hi, I’m mydlq1,瀏覽器2(用戶 mydlq2)設(shè)置發(fā)送消息為Hi, I’m mydlq2; 點(diǎn)擊發(fā)送按鈕發(fā)送消息;
執(zhí)行完上面步驟成后,可以在兩個(gè)不同瀏覽器中觀察到如下內(nèi)容:

十二. SpringBoot 結(jié)合 WebSocket 的常用方法示例
1. WebSocket 開啟跨域選項(xiàng)
WebSocket 配置類,里面設(shè)置允許跨域,內(nèi)容如下:
@Configuration
@EnableWebSocketMessageBroker
public?class?WebSocketConfig?implements?WebSocketMessageBrokerConfigurer?{
????@Override
????public?void?configureMessageBroker(MessageBrokerRegistry?registry)?{
????????registry.enableSimpleBroker("/queue");
????????registry.setApplicationDestinationPrefixes("/app");
????}
????@Override
????public?void?registerStompEndpoints(StompEndpointRegistry?registry)?{
????????registry.addEndpoint("/mydlq")
????????//?設(shè)置允許跨域,設(shè)置為"*"則為允許全部域名
????????.setAllowedOrigins("*")
????????.withSockJS();
????}
}
2. WebSocket 用戶上、下線監(jiān)聽
創(chuàng)建 WebSocket 用戶上線、下線處理器,內(nèi)容如下:
@Configuration
public?class?HttpWebSocketHandlerDecoratorFactory?implements?WebSocketHandlerDecoratorFactory?{
????/**
?????*?配置?webSocket?處理器
?????*
?????*?@param?webSocketHandler?webSocket?處理器
?????*?@return?webSocket?處理器
?????*/
????@Override
????public?WebSocketHandler?decorate(WebSocketHandler?webSocketHandler)?{
????????return?new?WebSocketHandlerDecorator(webSocketHandler)?{
????????????/**
?????????????*?websocket?連接時(shí)執(zhí)行的動(dòng)作
?????????????*?@param?session????websocket?session?對(duì)象
?????????????*?@throws?Exception?異常對(duì)象
?????????????*/
????????????@Override
????????????public?void?afterConnectionEstablished(final?WebSocketSession?session)?throws?Exception?{
????????????????//?輸出進(jìn)行?websocket?連接的用戶信息
????????????????if?(session.getPrincipal()?!=?null)?{
????????????????????String?username?=?session.getPrincipal().getName();
????????????????????System.out.println("用戶:"?+?username?+?"上線");
????????????????????super.afterConnectionEstablished(session);
????????????????}
????????????}
????????????/**
?????????????*?websocket?關(guān)閉連接時(shí)執(zhí)行的動(dòng)作
?????????????*?@param?session?websocket?session?對(duì)象
?????????????*?@param?closeStatus?關(guān)閉狀態(tài)對(duì)象
?????????????*?@throws?Exception?異常對(duì)象
?????????????*/
????????????@Override
????????????public?void?afterConnectionClosed(final?WebSocketSession?session,?CloseStatus?closeStatus)?throws?Exception?{
????????????????//?輸出關(guān)閉?websocket?連接的用戶信息
????????????????if?(session.getPrincipal()?!=?null)?{
????????????????????String?username?=?session.getPrincipal().getName();
????????????????????System.out.println("用戶:"?+?username?+?"下線");
????????????????????super.afterConnectionClosed(session,?closeStatus);
????????????????}
????????????}
????????};
????}
}
WebSocket 配置類中實(shí)現(xiàn) configureWebSocketTransport() 方法,將上面 WebSocket 處理器加到其中,如下:
@Configuration
@EnableWebSocketMessageBroker
public?class?WebSocketConfig?implements?WebSocketMessageBrokerConfigurer?{
????@Override
????public?void?configureMessageBroker(MessageBrokerRegistry?registry)?{
????????registry.enableSimpleBroker("/queue");
????????registry.setApplicationDestinationPrefixes("/app");
????}
????@Override
????public?void?registerStompEndpoints(StompEndpointRegistry?registry)?{
????????registry.addEndpoint("/mydlq").withSockJS();
????}
????/**
?????*?添加?WebSocket?用戶上、下線監(jiān)聽器
?????*/
????@Override
????public?void?configureWebSocketTransport(WebSocketTransportRegistration?registry)?{
????????registry.addDecoratorFactory(new?HttpWebSocketHandlerDecoratorFactory());
????}
}
十三. 總結(jié)
本文從原理到實(shí)踐詳細(xì)的介紹了WebSocket,希望你們喜歡.....
最后別忘了點(diǎn)個(gè)贊,哈哈...........
后臺(tái)回復(fù)?學(xué)習(xí)資料?領(lǐng)取學(xué)習(xí)視頻
如有收獲,點(diǎn)個(gè)在看,誠摯感謝
