WebSocket廣播式和點對點的通信【原創】

本篇文章主要介紹websocket的兩種通信,廣播式和點對點的通信。

一、廣播式通訊

類似廣播一樣,只要發出,訂閱的人便可以接收到

前端發出消息,通過SockJS連接

代碼示例
1、pom.xml引入jar
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-websocket</artifactId>
 </dependency>
2、WebSocketConfig.java 配置WebSocket
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
?
/**
 * @description: 配置WebSocket
 *          注釋@EnableWebSocketMessageBroker開始使用STOMP協議來傳輸基于代理(message broker)的消息
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 16:39
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
?
 /**
 *  注冊STOMP協議的節點(endpoint),并映射的對應的URL。
 *  注冊一個STOMP的endpoint,并指定使用SickJS協議
 * @param registry
 */
 @Override
 public void registerStompEndpoints(StompEndpointRegistry registry) {
 registry.addEndpoint("/endpointSSH").withSockJS();
 registry.addEndpoint("/endpointChat").withSockJS();
 }
?
 /**
 *  配置消息代理(Message Broker)
 *  廣播式應配置一個/topic 消息代理
 *  點對點配置 /queue
 * @param registry
 */
 @Override
 public void configureMessageBroker(MessageBrokerRegistry registry) {
 registry.enableSimpleBroker("/queue","/topic");
 }
?
}
3、兩個發送和接收消息的實體 ElijahMessage.java ElijahResponse.java
import lombok.AllArgsConstructor;
import lombok.Getter;
?
/**
 * @description: 用于服務器想向瀏覽器發生消息
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 17:10
 */
@AllArgsConstructor
@Getter
public class ElijahResponse {
 private String responseMessage;
}
import lombok.Getter;
?
/**
 * @description: 用于接收服務器發送的消息
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 17:09
 */
@Getter
public class ElijahMessage {
 private String name;
}
4、WsController.java WebSocket 控制器
import com.ch7.domain.ElijahMessage;
import com.ch7.domain.ElijahResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
?
import java.security.Principal;
?
/**
 * @description: WebSocket 控制器
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 17:15
 */
@Controller
@Slf4j
public class WsController {
?
 /**
 * 通過SimpMessagingTemplate 向瀏覽器發生消息
 */
 @Autowired
 private SimpMessagingTemplate messagingTemplate;
?
 /**
 *  當瀏覽器向服務端發生請求時,通過@MessageMapping映射/welcome這個地址
 *  注解@MessageMapping使用方法與@RequestMapping相似
 * @param message
 * @return
 * @throws Exception
 */
 @MessageMapping("welcome")
 @SendTo("/topic/getResponse")
 public ElijahResponse say(ElijahMessage message) throws Exception {
 Thread.sleep(3000);
 return new ElijahResponse("Welcome, " + message.getName() + "!");
 }
?
 /**
 *  點對點聊天
 *
 * @param principal 包含當前用戶的信息
 * @param msg
 */
 @MessageMapping("/chat")
 public void handleChar(Principal principal, String msg) {
?
 // 判斷發生給誰
 if (principal.getName().equals("ssh")) {
 // 發生消息給用戶  接收消息的用戶、瀏覽器訂閱地址和消息內容
 messagingTemplate.convertAndSendToUser("elijah",
 "/queue/notifications",
 principal.getName() + "-send: " + msg);
 } else {
 messagingTemplate.convertAndSendToUser("ssh",
 "/queue/notifications",
 principal.getName() + "-send: " + msg);
 }
 }
?
}
5、ws.html 發送消息和接收消息的頁面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <title>spring-boot-WebSocket-廣播式</title>
 <link rel="stylesheet" type="text/css" value="">
</head>
<body onload="disconnect()">
<noscript>
 <h2 style="color: #ff0000">貌似你的瀏覽器不支持websocket</h2>
</noscript>
?
?
<div>
 <h3>WebSocket</h3>
</div>
?
<div>
 <div>
 <button id="connect" onclick="connect();">連接</button>
 <button id="disconnect" disabled="disabled" onclick="disconnect();">斷開連接</button>
 </div>
?
 <div id="conversationDiv">
 <label>輸入你的名字</label>
 <input type="text" id="name" />
 <button id="sendName" onclick="sendName();">發送</button>
 <p id="response"></p>
?
 </div>
</div>
?
?
?
?
?
<!--<script src="/static/js/sockjs.min.js" charset="utf-8"></script>-->
<!--<script src="/static/js/stomp.min.js" charset="utf-8"></script>-->
?
<script th:src="@{/static/js/jquery.min.js}"></script>
<script th:src="@{/static/js/sockjs.min.js}"></script>
<script th:src="@{/static/js/stomp.min.js}"></script>
<script type="application/javascript">
?
 var stompClient = null;
?
 function setConnected(connected) {
 console.log('Connected status: ' + connected);
 document.getElementById("connect").disabled = connected;
 document.getElementById("disconnect").disabled = !disconnect;
 document.getElementById("conversationDiv").style.visibility= (connected ? 'visible' : 'hidden');
?
 // $("#conversationDiv").style.visibility = (connected ? 'visible' : 'hidden');
 $('response').html();
 }
?
 /**
 * 打開連接
 */
 function connect() {
 // 1、連接SockJS的endpoint
 var socket = new SockJS('/endpointSSH');
 // 2、使用STOMP 子協議的WebSocket客戶端
 stompClient = Stomp.over(socket);
 // 3、連接websockst服務端
 stompClient.connect({}, function (frame) {
 setConnected(true);
 console.log('Connected: ' + frame);
 // 4、 通過stomp.subscribe訂閱/topic/getResponse目標(destination)發生的消息,后端在@SendTo定義
 stompClient.subscribe('/topic/getResponse', function (respose) {
 showResponse(JSON.parse(respose.body).responseMessage);
 });
 });
 }
?
 /**
 * 關閉連接
 */
 function disconnect() {
 if (stompClient != null) {
 stompClient.disconnect();
 }
 setConnected(false);
 console.log("Disconnected");
 }
?
 function sendName() {
 var name = $('#name').val();
 // 5、通過 stompClient.send 向/welcome 目標發送消息 服務端在@MessageMapping中定義的
 stompClient.send("/welcome", {}, JSON.stringify({'name': name}));
 }
?
 function showResponse(message) {
 var response = $("#response");
 response.html(message);
 }
?
</script>
</body>
</html>
?
展示結果:
websocket-廣播式.png

在之前學習過socket連接對象也可通過WebSocket(不通過SockJS)連接

var socket = new WebSocket(url);

http://www.lxweimin.com/p/bd0667b270ca

目前的是通過sockjs來

1、連接SockJS的endpoint

2、使用STOMP 子協議的WebSocket客戶端

3、連接websockst服務端

4、 通過stomp.subscribe訂閱/topic/getResponse目標(destination)發生的消息,后端在@SendTo定義

5、通過 stompClient.send 向/welcome 目標發送消息 服務端在@MessageMapping中定義的

STOMP幀由命令,一個或多個頭信息、一個空行及負載(文本或字節)所組成;

其中可用的COMMAND 包括:

CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、BEGIN、COMMIT、ABORT、ACK、NACK、DISCONNECT;

數據執行流程

CONNECT accept-version:1.1,1.0 heart-beat:10000,10000

連接成功的返回為:

<<< CONNECTED version:1.1 heart-beat:0,0

訂閱目標(destination)/topic/getResponse:

SUBSCRIBE id:sub-0 destination:/topic/getResponse

向目標(destination)/welcome 發生消息的格式為:

SEND destination:/welcome content-length:17

{"name":"elijah"}

從目標(destination)/topic/getResponse接收的格式為:

<<< MESSAGE destination:/topic/getResponse content-type:application/json;charset=UTF-8 subscription:sub-0 message-id:5nd0pfjf-73 content-length:38

{"responseMessage":"Welcome, elijah!"}

二、點對點式通信:

點對點多用于聊天室,一對一的通信,這里是基礎的登錄(springsecurity)到聊天室然后進行兩天

代碼示例

相關登錄配置
1、pom.xml 引入jar
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
2、WebSecurityConfig.java 鑒權配置
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
?
/**
 * @description: 登錄時的鑒權配置
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-15 18:31
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
?
 @Override
 protected void configure(HttpSecurity http) throws Exception {
 http
 .authorizeRequests()
 // /和login不攔截
 .antMatchers("/", "login").permitAll()
 .anyRequest().authenticated()
 .and()
 .formLogin()
 // 頁面訪問路徑
 .loginPage("/login")
 // 登錄成功轉向/char
 .defaultSuccessUrl("/chat")
 .permitAll()
 .and()
 .logout()
 .permitAll();
?
 }
?
 /**
 * 內存中分配兩個用戶
 * @param auth
 * @throws Exception
 */
 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth
 .inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
 .withUser("elijah")
 .password(new BCryptPasswordEncoder().encode("elijah"))
 .roles("USER")
 .and()
 .withUser("ssh")
 .password(new BCryptPasswordEncoder().encode("ssh"))
 .roles("USER");
 }
?
 /**
 * 靜態資源不攔截
 * @param web
 * @throws Exception
 */
 @Override
 public void configure(WebSecurity web) throws Exception {
 web.ignoring().antMatchers("/resource/static/**");
 }
}
3、login.html 登錄頁面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <title>spring-boot-login</title>
 <link rel="stylesheet" type="text/css" value="">
</head>
<body>
?
<div>
 <h3>登錄</h3>
</div>
?
 <div th:if="${param.error}">
 無效的賬號和密碼
 </div>
 <div th:if="${param.logout}">
 你已注銷
 </div>
?
<form th:action="@{/login}" method="post">
 <div>
 <label>
 賬號:  <input type="text" name="username" />
 </label>
 </div>
 <div>
 <label>
 密碼:  <input type="password" name="password" />
 </label>
 </div>
 <div>
 <input type="submit" value="登陸"/>
 </div>
</form>
</body>
</html>
聊天代碼
1、相關配置

WebSocketConfig.java

WsController.java

方法在上面廣播式代碼里

兩個頁面也需要配置

2、char.html 聊天窗口
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <title>spring-boot-WebSocket-點對點式-home</title>
 <link rel="stylesheet" type="text/css" value="">
</head>
<body>
?

<p>聊天室</p>
?
<form id="elijahForm">
 <textarea rows="4" cols="60" name="text"></textarea>
 <input type="submit">
</form>
?
?
?
?
<script th:src="@{/static/js/jquery.min.js}"></script>
<script th:src="@{/static/js/sockjs.min.js}"></script>
<script th:src="@{/static/js/stomp.min.js}"></script>
<script type="application/javascript">
?
?
 $('#elijahForm').submit(function (e) {
 e.preventDefault();
 var text = $('#elijahForm').find('textarea[name="text"]').val();
 sendSpittle(text);
 });
?
 // 1、連接SockJS的endpoint
 var socket = new SockJS('/endpointChat');
 // 2、使用STOMP 子協議的WebSocket客戶端
 stomp = Stomp.over(socket);
 // 3、連接websockst服務端 //默認的和STOMP端點連接
 stomp.connect("guest", "guest", function (frame) {
 // 4、 通過stomp.subscribe訂閱/topic/getResponse目標(destination)發生的消息,后端在@SendTo定義
 stomp.subscribe("/user/queue/notifications", function (message) {
 debugger;
 var content = message.body;
 var obj = JSON.parse(content);
 console.log("admin用戶特定的消息1:" + obj.message)
 console.log("收到一條新消息:" + JSON.parse(respose.message).responseMessage)
 $('#output').append("<b>Received: " + message.body + "</b><br/>")
 });
 });
?
?
 function sendSpittle(text) {
 stomp.send("/chat", {}, text);
 }
?
 $('#stop').click(function () {
 socket.close();
 })
</script>
?
<div id="output"></div>
?
</body>
</html>
?
展示結果:
websocket-點對點式.png

三、其他說明:

1、基本概念:

STOMP:

STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的簡單文本協議

如何理解 STOMP 與 WebSocket 的關系: 1) HTTP協議解決了 web 瀏覽器發起請求以及 web 服務器響應請求的細節,假設 HTTP 協議 并不存在,只能使用 TCP 套接字來 編寫 web 應用,你可能認為這是一件瘋狂的事情;

  1. 直接使用 WebSocket(SockJS) 就很類似于 使用 TCP 套接字來編寫 web 應用,因為沒有高層協議,就需要我們定義應用間所發送消息的語義,還需要確保連接的兩端都能遵循這些語義;

  2. 同 HTTP 在 TCP 套接字上添加請求-響應模型層一樣,STOMP 在 WebSocket 之上提供了一個基于幀的線路格式層,用來定義消息語義;

2、所遇到的坑:

使用springsecurity時在內容中設置密碼沒有處理會報錯

There is no PasswordEncoder mapped for the id "null"

是高版本的security所導致

.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
 .withUser("elijah")
 .password(new BCryptPasswordEncoder().encode("elijah"))
 .roles("USER")

參考文檔:

https://blog.csdn.net/jqsad/article/details/77745379

http://www.lxweimin.com/p/bd0667b270ca

參考書籍汪云飛 SpringBoot實戰

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容