一、WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。
WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
在 WebSocket API 中,瀏覽器和服務器只需要做一個握手的動作,然后,瀏覽器和服務器之間就形成了一條快速通道。兩者之間就直接可以數據互相傳送。
二、STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,簡單(流)文本定向消息協議,它提供了一個可互操作的連接格式,允許STOMP客戶端與任意STOMP消息代理(Broker)進行交互。STOMP協議由於設計簡單,易於開發客戶端,因此在多種語言和多種平台上得到廣泛地應用。
三、首先,我們先理解一下為什么需要STOMP。
1)常規的websocket連接和普通的TCP基本上沒有什么差別的。
2)那我們如果像http一樣加入一些響應和請求層。
3)所以STOMP在websocket上提供了一中基於幀線路格式(frame-based wire format)。
4)簡單一點,就是在我們的websocket(TCP)上面加了一層協議,使雙方遵循這種協議來發送消息。
四、STOMP
1)Frame
例如:
command:CONNECT
其他部分都是headers的一部分。
2)command類別
CONNECT
SEND
SUBSCRIBE
UNSUBSCRIBE
BEGIN
COMMIT
ABORT
ACK
NACK
DISCONNECT
3)客戶端常用連接方式
a、ws
var url = "ws://localhost:8080/websocket"; var client = Stomp.client(url);
b、sockJs
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script> <script> // use SockJS implementation instead of the browser's native implementation var ws = new SockJS(url); var client = Stomp.over(ws); [...] </script>
說明:使用ws協議需要瀏覽器的支持,但是一些老版本的瀏覽器不一定支持。Stomp.over(ws)的凡是就是用來定義服務websocket的協議。
4)服務端的實現過程
a、服務端:/app,這里訪問服務端,前綴通過設定的方式訪問。
b、用戶:/user,這里針對的是用戶消息的傳遞,針對於當前用戶進行傳遞。
c、其他消息:/topic、/queue,這兩種方式。都是定義出來用於訂閱。並且消息只能從這里通過並處理
五、springboot的簡單例子
1)目錄結構
2)依賴包(pom.xml)
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
3)websocket配置(WebSocketConfiguration、SecurityConfiguration)
/** * webSocket配置 */ @Configuration @EnableWebSocketMessageBroker public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { /** * 注冊stomp端點,主要是起到連接作用 * @param stompEndpointRegistry */ @Override public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { stompEndpointRegistry .addEndpoint("/webSocket") //端點名稱 //.setHandshakeHandler() 握手處理,主要是連接的時候認證獲取其他數據驗證等 //.addInterceptors() 攔截處理,和http攔截類似 .setAllowedOrigins("*") //跨域 .withSockJS(); //使用sockJS } /** * 注冊相關服務 * @param registry */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //這里使用的是內存模式,生產環境可以使用rabbitmq或者其他mq。 //這里注冊兩個,主要是目的是將廣播和隊列分開。 //registry.enableStompBrokerRelay().setRelayHost().setRelayPort() 其他方式 registry.enableSimpleBroker("/topic", "/queue"); //客戶端名稱前綴 registry.setApplicationDestinationPrefixes("/app"); //用戶名稱前 registry.setUserDestinationPrefix("/user"); } }
認證配置:
/** * 配置基本登錄 */ @Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter{ /** * 加密方式 */ @Autowired private BCryptPasswordEncoder passwordEncoder; /** * 所有請求過濾,包含webSocket * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests().anyRequest().authenticated() .and() .httpBasic(); } /** * 加入兩個用戶測試不同用的接受情況 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password(passwordEncoder.encode("admin")).roles("ADMIN") .and() .withUser("user").password(passwordEncoder.encode("user")).roles("USER"); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } }
4)服務端
/** * 消息接收處理 */ @RestController public class MessageResource { //spring提供的推送方式 @Autowired private SimpMessagingTemplate messagingTemplate; /** * 廣播模式 * @param requestMsg * @return */ @MessageMapping("/broadcast") @SendTo("/topic/broadcast") public String broadcast(RequestMsg requestMsg) { //這里是有return,如果不寫@SendTo默認和/topic/broadcast一樣 return "server:" + requestMsg.getBody().toString(); } /** * 訂閱模式,只是在訂閱的時候觸發,可以理解為:訪問——>返回數據 * @param id * @return */ @SubscribeMapping("/subscribe/{id}") public String subscribe(@DestinationVariable Long id) { return "success"; } /** * 用戶模式 * @param requestMsg * @param principal */ @MessageMapping("/one") //@SendToUser("/queue/one") 如果存在return,可以使用這種方式 public void one(RequestMsg requestMsg, Principal principal) { //這里使用的是spring的security的認證體系,所以直接使用Principal獲取用戶信息即可。 //注意為什么使用queue,主要目的是為了區分廣播和隊列的方式。實際采用topic,也沒有關系。但是為了好理解 messagingTemplate.convertAndSendToUser(principal.getName(), "/queue/one", requestMsg.getBody()); } }
客戶端(JavaScript):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>webSocket</title> <script src="js/jquery.js"></script> <script src="js/sockjs.min.js"></script> <script src="js/stomp.js"></script> </head> <body> <div> <button id="connect">連接</button> <button id="disconnect" disabled="disabled">斷開</button> </div> <div> <h3>廣播形式</h3> <button id="broadcastButton">發送</button><input id="broadcastText" type="text"> <label>廣播消息:</label><input id="broadcastMsg" type="text" disabled="disabled"> </div> <div> <h3>訂閱形式</h3> <label>訂閱消息:</label><input id="subscribeMsg" type="text" disabled="disabled"> </div> <div> <h3>角色形式</h3> <button id="userButton">發送</button><input id="userText" type="text"> <label>用戶消息:</label><input id="userMsg" type="text" disabled="disabled"> </div> <div> <h3>無APP</h3> <button id="appButton">發送</button><input id="appText" type="text"> <label>前端消息:</label><input id="appMsg" type="text" disabled="disabled"> </div> </body> <script> var stomp = null; $("#connect").click(function () { var url = "http://localhost:8080/webSocket" var socket = new SockJS(url); stomp = Stomp.over(socket); //連接 stomp.connect({}, function (frame) { //訂閱廣播 stomp.subscribe("/topic/broadcast", function (res) { $("#broadcastMsg").val(res.body); }); //訂閱,一般只有訂閱的時候在返回 stomp.subscribe("/app/subscribe/1", function (res) { $("#subscribeMsg").val(res.body); }); //用戶模式 stomp.subscribe("/user/queue/one", function (res) { $("#userMsg").val(res.body); }); //無APP stomp.subscribe("/topic/app", function (res) { $("#appMsg").val(res.body); }); setConnect(true); }); }); $("#disconnect").click(function () { if (stomp != null) { stomp.disconnect(); } setConnect(false); }); //設置按鈕 function setConnect(connectStatus) { $("#connect").attr("disabled", connectStatus); $("#disconnect").attr("disabled", !connectStatus); } //發送廣播消息 $("#broadcastButton").click(function () { stomp.send("/app/broadcast", {}, JSON.stringify({"body":$("#broadcastText").val()})) }); //發送用戶消息 $("#userButton").click(function () { stomp.send("/app/one", {}, JSON.stringify({"body":$("#userText").val()})) }); //發送web消息 $("#appButton").click(function () { stomp.send("/topic/app", {}, JSON.stringify({"body":$("#appText").val()})) }); </script> </html>
5)普通測試
角色測試:
六、相關資料