技术标签: SpringSecurity STOMP SpringBoot WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
WebSocket是一个消息架构,不强制使用任何特定的消息协议,它依赖于应用层解释消息的含义;
与处在应用层的HTTP不同,WebSocket处在TCP上非常薄的一层,会将字节流转换为文本/二进制消息,因此,对于实际应用来说,WebSocket的通信形式层级过低,因此,可以在 WebSocket 之上使用STOMP协议,来为浏览器和server间的通信增加适当的消息语义。
1、直接使用WebSocket(SockJS)就很类似于使用TCP套接字
来编写web应用,因为没有高层协议,就需要我们定义应用间所发送消息的语义,还需要确保连接的两端都能遵循这些语义。
2、同HTTP在TCP套接字
上添加请求-响应模型层
一样,STOMP在WebSocket之上提供了一个基于帧的线路格式层
,用来定义消息语义。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 前端库 -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
也可以直接前端页面引入相关JS文件
jquery的js
sockjs-client的js
stomp-websocket的js
Spring提供了基于WebSocket
的STOMP
支持,STOMP是一个简单的可互操作的协议,通常用于中间服务器与客户端之间进行异步消息传递
。
@Configuration
@EnableWebSocketMessageBroker // 开启WebSocket消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/**
* 设置消息代理的前缀 - '/topic'
* 被设置的前缀的消息会被转发到消息代理
* 消息代理再将消息广播给当前连接的客户端 - 群发
*/
registry.enableSimpleBroker("/topic");
/**
* 配置目标前缀,这里只配置一个,即/app
* 配置了的前缀为/app可以通过@MessageMapping注解的方法处理
* 其他的destination如/topic、/queue将被直接交给broker处理
*/
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/**
* 设置前缀为/chat,可以通过这个/chat建立连接
* withSockJs支持解决WbeSocket兼容问题
*/
registry.addEndpoint("/chat").withSockJS();
}
}
@Data
public class Message {
/** 昵称 */
private String name;
/** 消息内容 */
private String content;
}
这里的Controller
主要是用来处理消息的,前面配置文件中,我们配置了/app
目标前缀,前缀为/app
的会进入到我们下面所说的Controller
层里标注@MessageMapping
的方法里被处理。
@Controller
public class TestController {
@MessageMapping("/hello") // 接收/app/hello路径发来的消息
@SendTo("/topic/greetings") // 转发到/topic/greetings
public Message greeting(Message message){
return message;
}
}
除了@SendTo
注解可以将处理过的消息转发到broker,再由broker进行消息广播外。Spring提供了一个SimpMessagingTemplate
类来让开发者更加灵活地发送消消息。
@Autowired
private SimpMessagingTemplate template;
@MessageMapping("/hello") // 接收/app/hello路径发来的消息
public void greeting(Message message){
template.convertAndSend("/topic/greetings",message);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket群聊</title>
</head>
<body>
<!-- 用户名区域 -->
<div>
<label for="name">请输入用户名:</label>
<input type="text" id="name" placeholder="用户名">
</div>
<!-- 连接区域 -->
<div>
<button id="connect" type="button">连接</button>
<button id="disconnect" type="button" disabled="disabled">断开连接</button>
</div>
<!-- 发送消息区域 -->
<div id="chat" style="display:none">
<div>
<label for="content">请输入聊天内容:</label>
<input type="text" id="content" placeholder="聊天内容">
</div>
<button id="send" type="button">发送</button>
</div>
<!-- 聊天区域 -->
<div id="greetings">
<div id="conversation" style="display: none">群聊中...</div>
</div>
<!-- JS引用区域 -->
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<!-- 自定义JS -->
<script src="/js/app.js"></script>
</body>
</html>
// STOMP客户端
var stompClient = null;
/**
* 是否已经连接,对页面显示进行处理
* @param connected 是否已经连接
*/
function setConnected(connected) {
$("#connect").prop("disabled",connected);
$("#disconnect").prop("disabled",!connected);
if(connected){
$("#conversation").show();
$("#chat").show();
}else{
$("#conversation").hide();
$("#chat").hide();
}
$("#greetings").html("");
}
/**
* 建立WebSocket连接
*/
function connect() {
// 如果名字为空,则不让连接
if(!$("#name").val()){
return;
}
// 通过SockJs建立连接对象
var socket = new SockJS('/chat');
// 也可以通过WebSocket建立连接
// var socket = new WebSocket("/chat");
// 获取STOMP子协议的客户端对象
stompClient = Stomp.over(socket);
// 向服务器发起websocket连接并发送CONNECT镇
stompClient.connect({
},function (frame) {
// 表示连接成功,
setConnected(true);
// 订阅服务端发送的消息
stompClient.subscribe('/topic/greetings',function (greeting) {
// 显示消息
showGreeting(JSON.parse(greeting.body));
});
});
}
/**
* 断开连接
*/
function disconnect() {
if (stompClient != null){
stompClient.disconnect();
}
setConnected(false);
}
/**
* 发送消息
*/
function sendMessage() {
// 发送消息
stompClient.send("/app/hello",{
},
JSON.stringify({
'name':$("#name").val(),'content':$("#content").val()}));
}
/**
* 控制显示消息
* @param message 消息对象
*/
function showGreeting(message) {
$("#greetings").append("<div>" + message.name + ":" + message.content + "</div>");
}
$(function () {
$("#connect").click(function () {
connect();
});
$("#disconnect").click(function () {
disconnect();
});
$("#send").click(function () {
sendMessage();
// 清空聊天框
$("#content").val("");
})
});
运行项目,用两个不同的浏览器打开,分别发送消息。
对app.js
中的STOMP的API解释。
/**
* 通过SockJS建立WebSocket连接对象
* 参数就是我们配置文件中registerStompEndpoints方法里配置的前缀
*/
var socket = new SockJS('/chat');
// 同样,可以用下面这行代码代替,但是就没有SockJS提供的兼容支持
var socket = new WebSocket("/chat");
// 获取STOMP子协议的客户端对象
var stompClient = Stomp.over(socket);
/**
* headers:客户端的认证信息
* connectCallback:连接成功时(服务器响应 CONNECTED 帧)的回调方法
* errorCallback:连接失败时(服务器响应 CONNECTED 帧)的回调方法
* 如果不需要认证,使用{}替代即可
* 失败回调方法可以省略
*/
stompClient.connect(headers, connectCallback, errorCallback);
其中headers
认证信息大概长这个样子:
var headers = {
login: 'mylogin',
passcode: 'mypasscode',
// additional header
'client-id': 'my-client-id'
};
断开连接是异步操作的。
/**
* disconnectCallback:回调方法
* 回调方法在操作完成时调用,可以省略
*/
stompClient.disconnect(disconnectCallback);
STOMP 1.1 版本,默认开启了心跳检测机制,可通过client对象的heartbeat进行配置,默认是10000ms
stompClient.heartbeat.outgoing=20000;
stompClient.heartbeat.incoming=0;
/**
* destinationUrl:服务端Controller中@MessageMapping中匹配的URL
* headers:发送信息的header,JavaScript对象,可选参数,省略可用{}代替
* body:发送信息的body,字符串,可选参数
*/
stompClient.send(destinationUrl, headers, body);
body消息可以使用JSON数据,使用JSON.stringify转换即可
stompClient.send("/app/hello",{
},
JSON.stringify({
'name':$("#name").val(),'content':$("#content").val()}));
STOMP 客户端支持在发送消息时用事务进行处理
// 该方法返回一个包含了事务 id、commit()、abort()的JavaScript 对象
var tx = stompClient.begin();
// 在headers对象中加入事务id,若没有添加,则会直接发送消息,不会以事务进行处理
stompClient.send("/app/hello",{
transaction: tx.id},
JSON.stringify({
'name':$("#name").val(),'content':$("#content").val()}));
// 提交事务
tx.commit();
/**
* destinationUrl:服务端@SendTo匹配的URL
* callback:为每次收到服务器推送的消息时的回调方法,该方法包含参数message(即收到的消息)
* headers:附加的headers,JavaScript对象,可选参数
* headers方法返回一个包含了id属性的JavaScript对象,可作为unsubscribe()方法的参数
*/
var subscription = varstompClient.subscribe(destinationUrl, callback, headers);
subscription.unsubscribe();
了解更多,可以百度,或是参考STOMP 客户端 API 整理
为了更能表现出点对点的用户概念,所以这里引入SpringSecurity
,然后通过内存里的账号登录。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("lcy")
.password("$2a$10$XcigeMfToGQ2bqRToFtUi.sG1V.HhrJV6RBjji1yncXReSNNIPl1K") // 123
.roles("admin")
.and()
.withUser("jyqc")
.password("$2a$10$XcigeMfToGQ2bqRToFtUi.sG1V.HhrJV6RBjji1yncXReSNNIPl1K")
.roles("user")
.and()
.withUser("xbyx")
.password("$2a$10$XcigeMfToGQ2bqRToFtUi.sG1V.HhrJV6RBjji1yncXReSNNIPl1K")
.roles("admin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() // 登录就可以访问
.and()
.formLogin().permitAll(); // 登录相关url都可以访问
}
}
@Configuration
@EnableWebSocketMessageBroker // 开启WebSocket消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic","/queue");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}
}
新增如下代码
@Autowired
private SimpMessagingTemplate template;
@MessageMapping("/chat")
public void chat(Principal principal, Chat chat){
String from = principal.getName();
chat.setFrom(from);
// convertAndSendToUser内部会做处理
// 发送的最终路径是/user/用户名/queue/chat
template.convertAndSendToUser(chat.getTo(),"/queue/chat",chat);
}
convertAndSendToUser
源码查看,可以发现里面对url
进行修改,this.destinationPrefix
默认是/user
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket单聊</title>
</head>
<body>
<div id="chat">
<div id="chatsContent">
</div>
<div>
聊天内容:
<input type="text" id="content" placeholder="请输入聊天内容"><br>
目标用户:
<input type="text" id="to" placeholder="请输入目标用户"><br>
<button id="send" type="button">发送</button>
</div>
</div>
<!-- JS引用区域 -->
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/js/chat.js"></script>
</body>
</html>
// STOMP客户端
var stompClient = null;
function connect() {
var socket = new SockJS("/chat");
stompClient = Stomp.over(socket);
stompClient.connect({
},function (frame) {
// 因为后端的convertAndSendToUser处理了url,所以也就是为什么相比之前的群里多了个/user的原因
stompClient.subscribe('/user/queue/chat',function (chat) {
showGreeting(JSON.parse(chat.body));
});
});
}
function sendMsg() {
stompClient.send("/app/chat",{
},JSON.stringify({
'content':$("#content").val(),'to':$("#to").val()}));
}
function showGreeting(message) {
$("#chatsContent")
.append("<div>" + message.from + ":" + message.content + "</div>");
}
$(function () {
connect();
$("#send").click(function () {
sendMsg();
$("#content").val("");
})
});
开启三个不同的浏览器测试,点对点模式没有问题。
1.简介2.Ergonomics(工效学)3.Generations(辈分) 3.1性能考虑 3.2测量4.调整各Generation的容量 4.1 Heap总量 4.2 Young Generation 4.2.1 Young Generation Guarantee(Young Generat
我的4G套餐,在运营商近一年多次的电话、信息轰炸之下,终于换成了5G的。刚开始推给我说是200多的套餐,我不愿意。后来是168套餐,我也不想。后来又推128元套餐,我再次拒绝。说实话,一是不愿意每月多交资费,二是平时在家用WIFI,回老家吧,小城市根本没有啥5G基站。平时在家用WIFI,偶尔外出,流量其实是用不完的。我的手机4G套餐流量由933G(前22G高速,后面的限速。)再后来,运营商客服又来电说保留108元套餐资费不变、通话时长不变、附卡不变,就是流量变为每月35G。我想着我不...
从彭尼的游戏推算英雄联盟胜利的方式以下内容可能会引起您的轻微不适,请谨慎阅读。首先我们来看一个生活中非常常见的场景(对作者来说很常见。。。)俗话说得好,无兄弟不游戏,网吧五连坐从来没赢过。。。咳咳。。。开个玩笑开个玩笑,以我白银四的超凡实例(如果没有玩过LOL,对后文的概率内容完全不影响,可以直接跳过),再加上是四驱车的无间配合,必然是战无不胜,攻无不克。连输四局?纯属巧合而已,相信后面...
工作中的细节吴言的新公司好像一切都进展得很顺利,经过一周左右的磨合时间,大家彼此都变得熟悉起来,每个人也基本进入了工作状态。吴言对此非常满意,第一次转型做管理的自己,在最关键的第一周并没有出现大的失误,这个小团队已经开始工作了。但是吴言还是注意到了一些问题,虽然这些问题很
不想多说直接看代码#引入必要的包fromseleniumimportwebdriverimporttimeimportthreading_author_='小强测试品牌www....
今天介绍一款linux系统服务器性能检测的工具-nmon及nmon_analyser (生成性能报告的免费工具),亲测可用。一.介绍nmon 工具可以帮助在一个屏幕上显示所有重要的性能优化信息,并动态地对其进行更新。这个高效的工具可以工作于任何哑屏幕、telnet 会话、甚至拨号线路。另外,它并不会消耗大量的 CPU 周期,通常低于百分之二。在更新的计算机上,其 CPU 使用率将低于百分之一。使用...
首先感谢刘铁锰先生的《深入浅出WPF》,学习WPF过程碰上很多新概念,如Data Binding、路由事件,命令、各种模板等。WPF:编写CS端的UI技术。怎么去掉WPF窗体靠上多出黑色的长条?在vs界面的菜单栏点击调试-选项,把启用XAML的UI调试工具勾选去掉即可。(我自己觉得偶尔会用用这个)1 认识WPF1.1 新建WPF项目生成Properties:...
jQuery当中的ready和window.onload又被称为入口函数以及他们两个的区别第一点:jQuery中编写多个入口函数,后面的不会覆盖前面的,并且多个入口函数都会执行,而window.onload则会执行最后一个,因为后面的会覆盖前面的第二点:原生js和jQuery入口函数的加载模式不同。原生js的window.onload会等到DOM元素加载完毕,并且图片也加载完毕才会执行而jQuery的ready是等到DOM元素加载完毕后就执行,不会等到图片CSS等加载完毕后执行。所以一般情况下j
事务是一系列的数据库操作,是数据库应用程序的基本逻辑单元。事务处理技术主要包括数据库恢复技术和并发控制技术。数据库恢复机制和并发控制机制是数据库管理系统的重要组成部分。
2021华为软挑赛题_思路分析——实时更新,做多少更多少一、赛题分析1.输入N,(1≤N≤100)2.输入N行(元组),可以采购的服务器类型数量型号CPU 核数内存大小硬件成本每日能耗成本host0Y6DP300830141730176hostHV3A3290580111689139hostD4V30418412121272152…………………………3.输入M,(1≤M≤1000)4.输入M行(元组),售卖的虚拟机类
1. 排查路径是否正确 (因为我设置listings为true,可以访问目录,但点击子目录时报404,所以不存在拼写等低级错误)2. 子目录名为中文,解析为URI存在编码问题。解决:{tomcat根目录}/conf/server.xml里增加URIEncoding="utf-8" <Connector port="8585" protocol="HTTP/1.1" ...
添加文件创建json文件apple-app-site-association,无后缀名。{ "applinks": { "apps": [], "details": [ { "appID": "TeamID.bundleID", "paths": ["*"] } ] }}将json文件上传到服务器域名根目录下json文.