WebSocket简介
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
WebSocket是一个消息架构,不强制使用任何特定的消息协议,它依赖于应用层解释消息的含义;
与处在应用层的HTTP不同,WebSocket处在TCP上非常薄的一层,会将字节流转换为文本/二进制消息,因此,对于实际应用来说,WebSocket的通信形式层级过低,因此,可以在 WebSocket 之上使用STOMP协议,来为浏览器和server间的通信增加适当的消息语义。
理解STOMP和WebSocket之间的关系
1、直接使用WebSocket(SockJS)就很类似于使用TCP套接字
来编写web应用,因为没有高层协议,就需要我们定义应用间所发送消息的语义,还需要确保连接的两端都能遵循这些语义。
2、同HTTP在TCP套接字
上添加请求-响应模型层
一样,STOMP在WebSocket之上提供了一个基于帧的线路格式层
,用来定义消息语义。
消息群发 - 发布模式
一、相关依赖引入
1、后端pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、前端依赖
<!-- 前端库 -->
<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
二、WebSocket配置文件
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();
}
}
三、创建消息Pojo类
@Data
public class Message {
/** 昵称 */
private String name;
/** 消息内容 */
private String content;
}
三、Controller层
这里的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);
}
四、前端界面及接受和发送消息
1、前端页面
<!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>
2、自定义JS
// 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("");
})
});
五、测试
运行项目,用两个不同的浏览器打开,分别发送消息。
STOMP中的API介绍
对app.js
中的STOMP的API解释。
一、发起连接
1、Socket连接对象
/**
* 通过SockJS建立WebSocket连接对象
* 参数就是我们配置文件中registerStompEndpoints方法里配置的前缀
*/
var socket = new SockJS('/chat');
// 同样,可以用下面这行代码代替,但是就没有SockJS提供的兼容支持
var socket = new WebSocket("/chat");
2、签名方法
// 获取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);
1、JSON支持
body消息可以使用JSON数据,使用JSON.stringify转换即可
stompClient.send("/app/hello",{},
JSON.stringify({'name':$("#name").val(),'content':$("#content").val()}));
2、事务支持
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();
五、订阅和接收消息(含取消订阅和接收消息)
1、订阅和接收
/**
* destinationUrl:服务端@SendTo匹配的URL
* callback:为每次收到服务器推送的消息时的回调方法,该方法包含参数message(即收到的消息)
* headers:附加的headers,JavaScript对象,可选参数
* headers方法返回一个包含了id属性的JavaScript对象,可作为unsubscribe()方法的参数
*/
var subscription = varstompClient.subscribe(destinationUrl, callback, headers);
2、取消订阅和接收
subscription.unsubscribe();
六、了解更多
了解更多,可以百度,或是参考STOMP 客户端 API 整理
消息单发 - 点对点模式
为了更能表现出点对点的用户概念,所以这里引入SpringSecurity
,然后通过内存里的账号登录。
一、引入SpringSecurity依赖并配置
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、配置文件
@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都可以访问
}
}
二、修改WebSocket配置文件以及Controller
1、WebSocket配置文件
@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();
}
}
2、Controller
新增如下代码
@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
。
三、HTML与JS
1、html页面
<!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>
2、chat.js
// 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("");
})
});
测试
开启三个不同的浏览器测试,点对点模式没有问题。