本章概要
- Spring Boot 整合 WebSocket
11.3 Spring Boot 整合 WebSocket
Spring Boot 对 WebSocket 提供了非常友好的支持,可以方便开发者在项目中快速集成 WebSocket 功能,实现单聊或者群聊。
11.3.1 消息群发
1. 创建项目
依赖
<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>
spring-boot-starter-websocket 依赖是 Web Socket 相关依赖,其它的都是前端库,使用 jar 包的形式对这些前端库进行统一管理,使用 webjars 添加到项目中的前端库,在 Spring Boot 项目中已经默认添加了静态资源过滤,因此可以直接用。
2. 配置 WebSocket
Spring 框架提供了基于 WebSocket 的 STOMP 支持,STOMP 是一个简单的可互操作的协议,通常被用于通过中间服务器在客户端之间进行异步消息传递。WebSocket 配置如下:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}
}
代码解释:
- 自定义类 WebSocketConfig 继承自 WebSocketMessageBrokerConfigurer 进行 WebSocket 配置,然后通过 @EnableWebSocketMessageBroker 注解开启 WebSocket 消息代理
- config.enableSimpleBroker(“/topic”); 表示设置消息代理的前缀,即如果消息的前缀是 “/topic”,就会将消息转发给消息代理(broker),再由消息代理将消息广播给当前连接的客户端
- config.setApplicationDestinationPrefixes(“/app”); 表示配置一个或多个前缀,通过这些前缀过滤出需要备注接方法处理的消息。例如,前缀为 “/app” 的 destination 可以通过 @MessageMapping 注解方法处理,而其它的 destination (例如“/topic”“/queue”)将被直接交给 broker 处理
- registry.addEndpoint(“/chat”).withSockJS(); 则表示定义一个前缀为“/chat”的 endPoint ,并开启 sockjs 支持,sockjs 可以解决浏览器对 WebSocket 的兼容问题,客户端将通过这里配置的 URL 来建立 WebSocket 连接
3. 定义 Controller
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Message greeting(Message message) throws Exception {
return message;
}
}
@MessageMapping(“/hello”) 注解将用来接收 “/app/hello”路径发来的消息,在注解方法中对消息进行处理后,再将消息转发到 @SendTo 定义的路径上,而 @SendTo 路径前缀是一个前缀为“/topic”的路径,因此该消息将被交给消息代理 broker ,再由 broker 进行广播。
public class Message {
private String name;
private String content;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
4. 构建聊天页面
在 resources/static 目录下创建 chat.html 页面作为聊天页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>群聊</title>
<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="/app.js"></script>
</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="name">请输入聊天内容:</label>
<input type="text" id="content" placeholder="聊天内容">
</div>
<button id="send" type="button">发送</button>
<div id="greetings">
<div id="conversation" style="display: none">群聊进行中...</div>
</div>
</div>
</body>
</html>
引入外部的 JS 库 ,这些 JS 库在 pom.xml 文件中通过依赖加入进来。app.js 为自定义 JS ,如下:
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
$("#chat").show();
}
else {
$("#conversation").hide();
$("#chat").hide();
}
$("#greetings").html("");
}
function connect() {
if (!$("#name").val()) {
return;
}
var socket = new SockJS('/chat');
stompClient = Stomp.over(socket);
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 sendName() {
stompClient.send("/app/hello",
{},
JSON.stringify({'name': $("#name").val(),'content':$("#content").val()}));
}
function showGreeting(message) {
$("#greetings")
.append("<div>" + message.name+":"+message.content + "</div>");
}
$(function () {
$( "#connect" ).click(function() { connect(); });
$( "#disconnect" ).click(function() { disconnect(); });
$( "#send" ).click(function() { sendName(); });
});
代码解释:
- connect 方法表示建立一个 WebSocket 连接,在建立WebSocket 连接时,用户必须先输入用户名,然后才能建立连接
- new SockJS(‘/chat’); 建立连接,然后创建一个 STOMP 实例发起连接请求,在连接成功的回调方法中,首先调用 setConnected(true); 方法进行页面设置,然后调用 STOMP 中的 subscribe 方法订阅服务端发送回来的消息,并将服务端发送来的消息展示出来(使用 showGreeting 方法)
- 调用 STOMP 中的 disconnect 方法断开一个 WebSocket 连接
5.测试
启动项目,http://localhost:8080/chat.html
输入用户名,然后点击连接按钮
然后换一个浏览器,重复刚才的步骤,这样就有两个用户连接上了,接下来便可开始群聊了
最后连接上的不显示之前的聊天内容
11.3.2 消息点对点发送
在 11.3.1节 中消息发送用了 @SendTo 注解,该注解将方法处理过的消息转发到 broker ,再由 broker 广播。除了 @SendTo 注解,Spring 还提供了 SimpMessagingTemplate 类来让开发者更加灵活地发送消息,使用 SimpMessagingTemplate 可以对 11.3.1小节的 Controller 进行如下改造
@Controller
public class GreetingController {
@Autowired
SimpMessagingTemplate messagingTemplate;
@MessageMapping("/hello")
public void greeting(Message message) throws Exception {
messagingTemplate.convertAndSend("/topic/greetings",message);
}
}
改造完成直接运行,运行结果与 11.3.1小节的运行结果一致。这里使用 SimpMessagingTemplate 进行消息的发送,在 Spring Boot 中,SimpMessagingTemplate 已经配置好,开发者直接注入进来即可。
使用 SimpMessagingTemplate,开发者可以在任何地方发消息到 broker ,也可以发送消息给某一个用户,这就是点对点的消息发送,在 11.3.1 的基础上实现。
1. 添加依赖
既然是点对点发送,就应该有用户的概念,因此加入 Spring Security 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3. 配置 Spring Security
对 Spring Security 进行配置,添加两个用户,同时配置所有地址认证后才能访问
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
.roles("admin")
.and()
.withUser("sang")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
.roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
相关配置含义参考10.1节《十、Spring Boot 安全管理(1)》
3. 改造 WebSocket 配置
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}
}
这里的修改是在 config.enableSimpleBroker(“/topic”); 方法的基础上又增加了一个 broker 前缀 “/queue” 方便对群发消息和点对点消息进行管理。
4. 配置 Controller
改造 Controller,如下
@Controller
public class GreetingController {
@Autowired
SimpMessagingTemplate messagingTemplate;
@MessageMapping("/hello")
public void greeting(Message message) throws Exception {
messagingTemplate.convertAndSend("/topic/greetings");
}
@MessageMapping("/chat")
public void chat(Principal principal, Chat chat) {
String from = principal.getName();
chat.setFrom(from);
messagingTemplate.convertAndSendToUser(chat.getTo(),"/queue/chat", chat);
}
}
代码解释:
- @MessageMapping(“/chat”) 注解表示来自 “/app/chat”路径的消息将被 chat 方法处理,chat 方法的第一个参数 Principal 可以用来获取当前登录用户的信息,第二个参数则是客户端发送来的消息
- 在 chat 方法中,首先获取当前用户的用户名,设置给 chat 对象的 from 属性,再将消息发送出去,发送的目标用户就是 chat 对象的 to 属性值
- 消息发送使用的方法是 convertAndSendToUser ,该方法内部调用了 convertAndSend 方法,并对消息路径做了处理,这里 destinationPrefix 的默认值为 “/user”,也就是消息的最终发送路径为“/user/用户名/queue/chat”。部分源码如下:
public void convertAndSendToUser(String user, String destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
Assert.notNull(user, "User must not be null");
user = StringUtils.replace(user, "/", "%2F");
destination = destination.startsWith("/") ? destination : "/" + destination;
super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
}
- chat 是一个普通的 JavaBean,to 属性表示消息的目标用户,from 表示消息从哪里来,content 则是消息的主体内容
5. 创建在线聊天页面
在 resources/static 目录下创建 onlinechat.html 页面作为在线聊天页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单聊</title>
<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="/chat.js"></script>
</head>
<body>
<div id="chat">
<div id="chatsContent">
</div>
<div>
请输入聊天内容:
<input type="text" id="content" placeholder="聊天内容">
目标用户:
<input type="text" id="to" placeholder="目标用户">
<button id="send" type="button">发送</button>
</div>
</div>
</body>
</html>
为了演示方便,这里需要手动输入目标用户名。相关 js 如下:
var stompClient = null;
function connect() {
var socket = new SockJS('/chat');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
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(); });
});
chat.js 与 app.js 的差异:
- 连接成功后,订阅地址为“/user/queue/chat”,该地址比服务端配置的地址多了“/user”前缀,这是因为 SimpMessagingTemplate 类中自动添加了路径前缀
- 聊天消息发送路径为“/app/chat”
- 发送的消息内容中有一个 to 字段,该字段用来描述消息的目标用户
6. 测试
http://localhost:8080/onlinechat.html,在谷歌登录 admin/123 用户,在 Microsoft Edge 中登录 sang/123 用户,然后admin发送消息给sang
接着 sang 发消息给amdin
sang 给自己发消息