本篇文章主要介紹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>
?
展示結果:
在之前學習過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>
?
展示結果:
三、其他說明:
1、基本概念:
STOMP:
STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的簡單文本協議
如何理解 STOMP 與 WebSocket 的關系: 1) HTTP協議解決了 web 瀏覽器發起請求以及 web 服務器響應請求的細節,假設 HTTP 協議 并不存在,只能使用 TCP 套接字來 編寫 web 應用,你可能認為這是一件瘋狂的事情;
直接使用 WebSocket(SockJS) 就很類似于 使用 TCP 套接字來編寫 web 應用,因為沒有高層協議,就需要我們定義應用間所發送消息的語義,還需要確保連接的兩端都能遵循這些語義;
同 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實戰