MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Spring Boot中的WebSocket应用实例

2023-11-154.7k 阅读

1. WebSocket 基础概述

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。与传统的 HTTP 协议不同,HTTP 协议是基于请求 - 响应模式的,客户端发起请求,服务器响应,这种模式在实时性要求高的场景下存在局限性,比如实时聊天、实时监控等。而 WebSocket 允许服务器主动向客户端推送消息,实现双向通信,极大地提高了实时交互能力。

WebSocket 的握手过程基于 HTTP 协议,客户端通过 HTTP 升级请求将协议升级到 WebSocket 协议。例如,客户端发送如下请求头:

GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec - WebSocket - Key: dGhlIHNhbXBsZSBub25jZQ==
Sec - WebSocket - Version: 13

服务器如果支持 WebSocket,会返回类似如下响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec - WebSocket - Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

这样就完成了从 HTTP 到 WebSocket 协议的升级,之后客户端和服务器就可以通过这个 TCP 连接进行双向数据传输。

2. Spring Boot 对 WebSocket 的支持

Spring Boot 为 WebSocket 提供了强大且便捷的支持,通过集成 Spring WebSocket 模块,开发者可以轻松地在 Spring Boot 应用中使用 WebSocket 功能。Spring Boot 对 WebSocket 的支持主要包括以下几个方面:

2.1 依赖引入

pom.xml 文件中添加 Spring Boot WebSocket 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring - boot - starter - websocket</artifactId>
</dependency>

这个依赖会引入 Spring WebSocket 相关的库,包括服务器端和客户端的支持。

2.2 配置 WebSocket

Spring Boot 提供了 WebSocketConfigurer 接口来配置 WebSocket。通过实现这个接口,我们可以定义 WebSocket 的处理器、拦截器等。例如:

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic", "/queue");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket - endpoint").withSockJS();
    }
}

在上述代码中:

  • @EnableWebSocketMessageBroker 注解开启了使用 STOMP 协议来传输基于代理的消息。STOMP(Simple Text - Oriented Messaging Protocol)是一种简单的、文本导向的消息协议,常用于 WebSocket 应用中。
  • configureMessageBroker 方法配置了消息代理。setApplicationDestinationPrefixes("/app") 表示所有以 /app 开头的消息目的地会被路由到带有 @MessageMapping 注解的方法。enableSimpleBroker("/topic", "/queue") 表示启用一个简单的内存消息代理,处理以 /topic/queue 开头的消息目的地。
  • registerStompEndpoints 方法注册了一个 STOMP 端点 /websocket - endpoint,并启用了 SockJS fallback 机制。SockJS 是一个 JavaScript 库,它提供了一个跨浏览器的 WebSocket 抽象层,当浏览器不支持 WebSocket 时,SockJS 可以使用其他传输方式(如 HTTP 长轮询)来模拟 WebSocket 的功能。

3. Spring Boot WebSocket 应用实例 - 实时聊天系统

接下来我们通过一个实时聊天系统的实例来深入了解 Spring Boot 中 WebSocket 的应用。

3.1 项目结构

我们的项目结构如下:

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           └── websocketchat/
│   │               ├── WebSocketChatApplication.java
│   │               ├── config/
│   │               │   └── WebSocketConfig.java
│   │               ├── controller/
│   │               │   └── ChatController.java
│   │               ├── model/
│   │               │   └── ChatMessage.java
│   │               └── service/
│   │                   └── ChatService.java
│   └── resources/
│       ├── application.properties
│       └── static/
│           └── index.html
│       └── templates/
└── test/
    └── java/
        └── com/
            └── example/
                └── websocketchat/
                    └── WebSocketChatApplicationTests.java

3.2 定义消息模型

首先,我们定义一个 ChatMessage 类来表示聊天消息:

public class ChatMessage {
    private String content;
    private String sender;
    private MessageType type;

    public ChatMessage() {
    }

    public ChatMessage(String content, String sender, MessageType type) {
        this.content = content;
        this.sender = sender;
        this.type = type;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

    public MessageType getType() {
        return type;
    }

    public void setType(MessageType type) {
        this.type = type;
    }

    public enum MessageType {
        CHAT, JOIN, LEAVE
    }
}

ChatMessage 类包含消息内容 content、发送者 sender 和消息类型 typeMessageType 枚举定义了三种消息类型:聊天消息 CHAT、加入房间消息 JOIN 和离开房间消息 LEAVE

3.3 聊天服务层

创建一个 ChatService 来处理聊天消息的发送:

import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

@Service
public class ChatService {
    private final SimpMessagingTemplate messagingTemplate;

    public ChatService(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    public void sendMessage(ChatMessage message) {
        switch (message.getType()) {
            case JOIN:
            case LEAVE:
                messagingTemplate.convertAndSend("/topic/public", message);
                break;
            case CHAT:
                messagingTemplate.convertAndSendToUser(message.getSender(), "/queue/private", message);
                break;
        }
    }
}

ChatService 中,通过构造函数注入 SimpMessagingTemplate,它用于向 WebSocket 客户端发送消息。sendMessage 方法根据消息类型决定将消息发送到不同的目的地。对于 JOINLEAVE 类型的消息,发送到 /topic/public,表示所有订阅了这个主题的客户端都能收到;对于 CHAT 类型的消息,发送到以发送者用户名命名的私有队列 /queue/private,只有发送者能收到。

3.4 控制器层

创建一个 ChatController 来处理客户端发送的消息:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

import com.example.websocketchat.model.ChatMessage;
import com.example.websocketchat.service.ChatService;

@Controller
public class ChatController {
    @Autowired
    private ChatService chatService;

    @MessageMapping("/chat.sendMessage")
    public void sendMessage(@Payload ChatMessage chatMessage) {
        chatService.sendMessage(chatMessage);
    }

    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public ChatMessage addUser(@Payload ChatMessage chatMessage,
                               SimpMessageHeaderAccessor headerAccessor) {
        headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
        return chatMessage;
    }
}
  • @MessageMapping("/chat.sendMessage") 注解的方法处理客户端发送的聊天消息,调用 ChatServicesendMessage 方法将消息发送出去。
  • @MessageMapping("/chat.addUser") 注解的方法处理用户加入房间的消息。@SendTo("/topic/public") 表示将返回的消息发送到 /topic/public 主题,所有订阅该主题的客户端都能收到。同时,将用户名存储在会话属性中。

3.5 前端页面

resources/static/index.html 中编写前端页面:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>WebSocket Chat</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs - client/1.5.1/sockjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
    <style>
        body {
            font - family: Arial, sans - serif;
        }

        #chat - messages {
            border: 1px solid #ccc;
            height: 300px;
            overflow - y: scroll;
            padding: 10px;
        }

        #message - input {
            width: 80%;
            padding: 10px;
            margin - right: 10px;
        }
    </style>
</head>
<body>
    <h1>WebSocket Chat</h1>
    <div id="chat - messages"></div>
    <input type="text" id="message - input" placeholder="Type your message...">
    <input type="text" id="username - input" placeholder="Enter your username...">
    <button id="send - message - button">Send Message</button>
    <button id="join - button">Join Chat</button>

    <script>
        var socket = new SockJS('/websocket - endpoint');
        var stompClient = Stomp.over(socket);

        stompClient.connect({}, function (frame) {
            console.log('Connected: ', frame);

            stompClient.subscribe('/topic/public', function (message) {
                var chatMessage = JSON.parse(message.body);
                if (chatMessage.type === 'JOIN' || chatMessage.type === 'LEAVE') {
                    $('#chat - messages').append('<p><strong>' + chatMessage.sender + '</strong> ' + chatMessage.content + '</p>');
                }
            });

            stompClient.subscribe('/queue/private', function (message) {
                var chatMessage = JSON.parse(message.body);
                if (chatMessage.type === 'CHAT') {
                    $('#chat - messages').append('<p><strong>' + chatMessage.sender + '</strong>: ' + chatMessage.content + '</p>');
                }
            });
        });

        $('#join - button').click(function () {
            var username = $('#username - input').val();
            if (username) {
                stompClient.send('/app/chat.addUser', {}, JSON.stringify({sender: username, type: 'JOIN', content: 'has joined the chat'}));
            }
        });

        $('#send - message - button').click(function () {
            var message = $('#message - input').val();
            var username = $('#username - input').val();
            if (message && username) {
                stompClient.send('/app/chat.sendMessage', {}, JSON.stringify({sender: username, type: 'CHAT', content: message}));
                $('#message - input').val('');
            }
        });
    </script>
</body>
</html>

在这个 HTML 页面中:

  • 通过 SockJS 和 STOMP.js 库建立与后端的 WebSocket 连接。
  • 连接成功后,订阅 /topic/public/queue/private 主题,分别处理公共消息(如用户加入、离开)和私有聊天消息。
  • 点击 “Join Chat” 按钮时,向 /app/chat.addUser 发送加入消息。
  • 点击 “Send Message” 按钮时,向 /app/chat.sendMessage 发送聊天消息。

4. 深入理解 Spring Boot WebSocket 的原理

4.1 WebSocket 消息处理流程

当客户端通过 WebSocket 发送消息到服务器时,Spring Boot 的处理流程如下:

  1. 消息接收:WebSocket 服务器接收到消息,首先经过一系列的 WebSocket 处理器和拦截器。这些处理器和拦截器可以对消息进行预处理,比如验证消息格式、检查用户权限等。
  2. 消息路由:根据消息的目的地(destination),Spring Boot 使用消息代理(message broker)将消息路由到对应的处理方法。例如,如果消息的目的地以 /app 开头,Spring Boot 会查找带有 @MessageMapping 注解且映射路径匹配的方法来处理消息。
  3. 方法调用:调用对应的处理方法,处理方法可以对消息进行业务逻辑处理,比如在我们的聊天系统中,ChatControllersendMessage 方法调用 ChatServicesendMessage 方法来发送消息。
  4. 消息发送:处理方法处理完消息后,可能会将消息发送回客户端。Spring Boot 使用 SimpMessagingTemplate 来发送消息,根据消息的类型和目的地,将消息发送到合适的主题(topic)或队列(queue),客户端订阅相应的主题或队列来接收消息。

4.2 消息代理机制

Spring Boot 支持多种消息代理,如 RabbitMQ、ActiveMQ 等,也支持简单的内存消息代理。在我们的实例中,使用了简单的内存消息代理。消息代理的作用是在服务器端管理消息的分发,它维护了主题和队列的概念。

  • 主题(Topic):类似于发布 - 订阅模式,多个客户端可以订阅同一个主题,当有消息发送到该主题时,所有订阅的客户端都会收到消息。在我们的聊天系统中,/topic/public 主题用于广播用户加入和离开的消息。
  • 队列(Queue):消息会被发送到特定的队列,只有订阅该队列的客户端能收到消息。在我们的聊天系统中,/queue/private 队列用于发送私有聊天消息,只有发送者能收到。

5. 性能优化与注意事项

5.1 性能优化

  1. 连接管理:合理管理 WebSocket 连接,避免过多的无效连接占用资源。可以设置连接的超时时间,对于长时间不活跃的连接进行关闭。例如,在 Tomcat 服务器中,可以通过 server.servlet.session.timeout 属性设置会话超时时间,间接影响 WebSocket 连接的超时。
  2. 消息处理优化:在处理大量消息时,优化消息处理逻辑。避免在消息处理方法中进行复杂的、耗时的操作。可以将一些耗时操作异步化,使用 Spring 的异步任务机制来处理。例如,将消息持久化到数据库的操作放在异步任务中执行,避免阻塞 WebSocket 消息处理线程。
  3. 负载均衡:在高并发场景下,使用负载均衡器(如 Nginx)来分发 WebSocket 连接请求。负载均衡器可以根据服务器的负载情况,将请求分配到不同的服务器实例上,提高系统的整体性能和可用性。

5.2 注意事项

  1. 安全性:WebSocket 连接可能面临多种安全风险,如跨站WebSocket劫持(CSWSH)、恶意连接等。要对 WebSocket 连接进行身份验证和授权,确保只有合法用户能够建立连接和发送消息。可以使用 Spring Security 来实现 WebSocket 的安全认证,比如在 WebSocket 配置中添加认证和授权逻辑。
  2. 兼容性:虽然大部分现代浏览器都支持 WebSocket,但仍需考虑一些旧版本浏览器的兼容性。使用 SockJS 可以提供 WebSocket 的 fallback 机制,在不支持 WebSocket 的浏览器中使用其他传输方式(如 HTTP 长轮询)来模拟 WebSocket 的功能。但要注意,不同的 fallback 方式可能在性能和功能上存在一定差异。
  3. 内存管理:在使用简单内存消息代理时,要注意内存的使用情况。如果消息量过大,可能会导致内存溢出。对于生产环境,建议使用专业的消息代理服务器(如 RabbitMQ),它们具有更好的内存管理和消息持久化机制。

通过以上对 Spring Boot 中 WebSocket 的详细介绍、实例展示以及原理分析和性能优化等内容,相信开发者能够全面掌握 Spring Boot 中 WebSocket 的应用,开发出高效、稳定的实时交互应用。