十一、Spring Boot 整合 WebSocket(2)

本章概要

  • 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 给自己发消息
在这里插入图片描述

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小熊猫呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值