本篇文章主要介紹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é)果:
在之前學(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é)果:
三、其他說明:
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)為這是一件瘋狂的事情;
直接使用 WebSocket(SockJS) 就很類似于 使用 TCP 套接字來編寫 web 應(yīng)用,因?yàn)闆]有高層協(xié)議,就需要我們定義應(yīng)用間所發(fā)送消息的語義,還需要確保連接的兩端都能遵循這些語義;
同 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)