WebSocket廣播式和點(diǎn)對(duì)點(diǎn)的通信【原創(chuàng)】

本篇文章主要介紹websocket的兩種通信,廣播式和點(diǎn)對(duì)點(diǎn)的通信。

一、廣播式通訊

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

前端發(fā)出消息,通過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協(xié)議來傳輸基于代理(message broker)的消息
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 16:39
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
?
 /**
 *  注冊(cè)STOMP協(xié)議的節(jié)點(diǎn)(endpoint),并映射的對(duì)應(yīng)的URL。
 *  注冊(cè)一個(gè)STOMP的endpoint,并指定使用SickJS協(xié)議
 * @param registry
 */
 @Override
 public void registerStompEndpoints(StompEndpointRegistry registry) {
 registry.addEndpoint("/endpointSSH").withSockJS();
 registry.addEndpoint("/endpointChat").withSockJS();
 }
?
 /**
 *  配置消息代理(Message Broker)
 *  廣播式應(yīng)配置一個(gè)/topic 消息代理
 *  點(diǎn)對(duì)點(diǎn)配置 /queue
 * @param registry
 */
 @Override
 public void configureMessageBroker(MessageBrokerRegistry registry) {
 registry.enableSimpleBroker("/queue","/topic");
 }
?
}
3、兩個(gè)發(fā)送和接收消息的實(shí)體 ElijahMessage.java ElijahResponse.java
import lombok.AllArgsConstructor;
import lombok.Getter;
?
/**
 * @description: 用于服務(wù)器想向?yàn)g覽器發(fā)生消息
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 17:10
 */
@AllArgsConstructor
@Getter
public class ElijahResponse {
 private String responseMessage;
}
import lombok.Getter;
?
/**
 * @description: 用于接收服務(wù)器發(fā)送的消息
 *
 * @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 向?yàn)g覽器發(fā)生消息
 */
 @Autowired
 private SimpMessagingTemplate messagingTemplate;
?
 /**
 *  當(dāng)瀏覽器向服務(wù)端發(fā)生請(qǐng)求時(shí),通過@MessageMapping映射/welcome這個(gè)地址
 *  注解@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() + "!");
 }
?
 /**
 *  點(diǎn)對(duì)點(diǎn)聊天
 *
 * @param principal 包含當(dāng)前用戶的信息
 * @param msg
 */
 @MessageMapping("/chat")
 public void handleChar(Principal principal, String msg) {
?
 // 判斷發(fā)生給誰
 if (principal.getName().equals("ssh")) {
 // 發(fā)生消息給用戶  接收消息的用戶、瀏覽器訂閱地址和消息內(nèi)容
 messagingTemplate.convertAndSendToUser("elijah",
 "/queue/notifications",
 principal.getName() + "-send: " + msg);
 } else {
 messagingTemplate.convertAndSendToUser("ssh",
 "/queue/notifications",
 principal.getName() + "-send: " + msg);
 }
 }
?
}
5、ws.html 發(fā)送消息和接收消息的頁面
<!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();">發(fā)送</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 子協(xié)議的WebSocket客戶端
 stompClient = Stomp.over(socket);
 // 3、連接websockst服務(wù)端
 stompClient.connect({}, function (frame) {
 setConnected(true);
 console.log('Connected: ' + frame);
 // 4、 通過stomp.subscribe訂閱/topic/getResponse目標(biāo)(destination)發(fā)生的消息,后端在@SendTo定義
 stompClient.subscribe('/topic/getResponse', function (respose) {
 showResponse(JSON.parse(respose.body).responseMessage);
 });
 });
 }
?
 /**
 * 關(guān)閉連接
 */
 function disconnect() {
 if (stompClient != null) {
 stompClient.disconnect();
 }
 setConnected(false);
 console.log("Disconnected");
 }
?
 function sendName() {
 var name = $('#name').val();
 // 5、通過 stompClient.send 向/welcome 目標(biāo)發(fā)送消息 服務(wù)端在@MessageMapping中定義的
 stompClient.send("/welcome", {}, JSON.stringify({'name': name}));
 }
?
 function showResponse(message) {
 var response = $("#response");
 response.html(message);
 }
?
</script>
</body>
</html>
?
展示結(jié)果:
websocket-廣播式.png

在之前學(xué)習(xí)過socket連接對(duì)象也可通過WebSocket(不通過SockJS)連接

var socket = new WebSocket(url);

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

目前的是通過sockjs來

1、連接SockJS的endpoint

2、使用STOMP 子協(xié)議的WebSocket客戶端

3、連接websockst服務(wù)端

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

5、通過 stompClient.send 向/welcome 目標(biāo)發(fā)送消息 服務(wù)端在@MessageMapping中定義的

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

其中可用的COMMAND 包括:

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

數(shù)據(jù)執(zhí)行流程

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

連接成功的返回為:

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

訂閱目標(biāo)(destination)/topic/getResponse:

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

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

SEND destination:/welcome content-length:17

{"name":"elijah"}

從目標(biāo)(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!"}

二、點(diǎn)對(duì)點(diǎn)式通信:

點(diǎn)對(duì)點(diǎn)多用于聊天室,一對(duì)一的通信,這里是基礎(chǔ)的登錄(springsecurity)到聊天室然后進(jìn)行兩天

代碼示例

相關(guān)登錄配置
1、pom.xml 引入jar
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
2、WebSecurityConfig.java 鑒權(quán)配置
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: 登錄時(shí)的鑒權(quán)配置
 *
 * @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")
 // 登錄成功轉(zhuǎn)向/char
 .defaultSuccessUrl("/chat")
 .permitAll()
 .and()
 .logout()
 .permitAll();
?
 }
?
 /**
 * 內(nèi)存中分配兩個(gè)用戶
 * @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");
 }
?
 /**
 * 靜態(tài)資源不攔截
 * @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}">
 無效的賬號(hào)和密碼
 </div>
 <div th:if="${param.logout}">
 你已注銷
 </div>
?
<form th:action="@{/login}" method="post">
 <div>
 <label>
 賬號(hào):  <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、相關(guān)配置

WebSocketConfig.java

WsController.java

方法在上面廣播式代碼里

兩個(gè)頁面也需要配置

2、char.html 聊天窗口
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <title>spring-boot-WebSocket-點(diǎn)對(duì)點(diǎn)式-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 子協(xié)議的WebSocket客戶端
 stomp = Stomp.over(socket);
 // 3、連接websockst服務(wù)端 //默認(rèn)的和STOMP端點(diǎn)連接
 stomp.connect("guest", "guest", function (frame) {
 // 4、 通過stomp.subscribe訂閱/topic/getResponse目標(biāo)(destination)發(fā)生的消息,后端在@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>
?
展示結(jié)果:
websocket-點(diǎn)對(duì)點(diǎn)式.png

三、其他說明:

1、基本概念:

STOMP:

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

如何理解 STOMP 與 WebSocket 的關(guān)系: 1) HTTP協(xié)議解決了 web 瀏覽器發(fā)起請(qǐng)求以及 web 服務(wù)器響應(yīng)請(qǐng)求的細(xì)節(jié),假設(shè) HTTP 協(xié)議 并不存在,只能使用 TCP 套接字來 編寫 web 應(yīng)用,你可能認(rèn)為這是一件瘋狂的事情;

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

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

2、所遇到的坑:

使用springsecurity時(shí)在內(nèi)容中設(shè)置密碼沒有處理會(huì)報(bào)錯(cuò)

There is no PasswordEncoder mapped for the id "null"

是高版本的security所導(dǎo)致

.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實(shí)戰(zhàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,477評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,617評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,807評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,049評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,432評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,769評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容