Spring WebSocket 简介

1、概览

本文将带你学习如何使用 Spring 4 中引入的 WebSocket 功能来实现一个简单的聊天应用。

WebSockets 是 Web 浏览器和服务器之间的一种双向、全双工、持久连接。一旦建立了 WebSocket 连接,该连接就会一直打开,直到客户端或服务器关闭该连接。

2、Maven 依赖

pom.xml 中添加所需的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

此外,还需要添加 Jackson 依赖,用于序列化/反序列化 JSON 格式的消息。

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.10.2</version>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId> 
    <version>2.10.2</version>
</dependency>

3、启用 WebSocket

首先,在配置类上通过 @EnableWebSocketMessageBroker 注解来启用 WebSocket 功能。

配置类需要继承 AbstractWebSocketMessageBrokerConfigurer

顾名思义,它能在 Message Broker 的支持下处理 WebSocket 消息:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

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

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

如上,通过 configureMessageBroker 方法配置 Message Broker。

首先,启用了一个基于内存的 Message Broker,用于将消息返回给以 /topic 为前缀的客户端目标。

设置应用目标(通过 @MessageMapping 注解的方法)的前缀为 /app

registerStompEndpoints 方法注册了/chat 端点,从而启用了 Spring 的 STOMP 支持。这里为了提高灵活性,还添加了 SockJS 的支持,但是本文不会使用。

该端点(前缀为 /app 时)是 ChatController.send() 方法映射处理的端点。

启用 SockJS 回退选项,以便在无法使用 WebSockets 时使用其他消息传递选项。这一点非常有用,因为 WebSocket 在某些浏览器中尚未得到支持,并且可能受到限制性网络代理的阻止。

当需要时,回退功能允许应用在运行时优雅地降级到非 WebSocket 的替代方案,同时仍然可以使用 WebSocket API。

4、创建 Message Model

配置好 WebSocket 后,创建要发送的消息。

端点接受包含发送人和文本的 STOMP 消息,该消息是一个 JSON 对象。

{
    "from": "John",
    "text": "Hello!"
}

其对应的 Model 类如下:

public class Message {

    private String from;
    private String text;

    // get、set 省略
}

默认情况下,Spring 使用 Jackson 将 Model 对象转换为 JSON 或从 JSON 转换为 Model 对象。

5、创建消息处理 Controller

如你所见,Spring 处理 STOMP 消息的方法是将 Controller 方法与配置的端点关联起来。

可以通过 @MessageMapping 注解来做到这一点。

@MessageMapping("/chat")
@SendTo("/topic/messages")
public OutputMessage send(Message message) throws Exception {
    String time = new SimpleDateFormat("HH:mm").format(new Date());
    return new OutputMessage(message.getFrom(), message.getText(), time);
}

在本例中,创建了另一个名为 OutputMessage 的 Model 对象来表示发送到配置目的地的输出消息,并在对象中填充发件人(sender)和从传入消息中提取的消息内容,以及一个时间戳。

处理完消息后,会将其发送到使用 @SendTo 注解定义的适当目的地。/topic/messages 目标的所有订阅者都将收到该消息。

6、创建浏览器客户端

服务器配置完毕后,使用 sockjs-client 库构建一个与消息系统交互的简单 HTML 页面。

首先,需要导入 sockjsstomp JavaScript 客户端库。

接下来,创建一个 connect() 函数来打开与端点的通信,创建一个 sendMessage() 函数来发送 STOMP 消息,创建一个 disconnect() 函数来关闭通信:

<html>
    <head>
        <title>Chat WebSocket</title>
        <script src="resources/js/sockjs-0.3.4.js"></script>
        <script src="resources/js/stomp.js"></script>
        <script type="text/javascript">
            var stompClient = null;
            
            function setConnected(connected) {
                document.getElementById('connect').disabled = connected;
                document.getElementById('disconnect').disabled = !connected;
                document.getElementById('conversationDiv').style.visibility 
                  = connected ? 'visible' : 'hidden';
                document.getElementById('response').innerHTML = '';
            }
            
            function connect() {
                var socket = new SockJS('/chat');
                stompClient = Stomp.over(socket);  
                stompClient.connect({}, function(frame) {
                    setConnected(true);
                    console.log('Connected: ' + frame);
                    stompClient.subscribe('/topic/messages', function(messageOutput) {
                        showMessageOutput(JSON.parse(messageOutput.body));
                    });
                });
            }
            
            function disconnect() {
                if(stompClient != null) {
                    stompClient.disconnect();
                }
                setConnected(false);
                console.log("Disconnected");
            }
            
            function sendMessage() {
                var from = document.getElementById('from').value;
                var text = document.getElementById('text').value;
                stompClient.send("/app/chat", {}, 
                  JSON.stringify({'from':from, 'text':text}));
            }
            
            function showMessageOutput(messageOutput) {
                var response = document.getElementById('response');
                var p = document.createElement('p');
                p.style.wordWrap = 'break-word';
                p.appendChild(document.createTextNode(messageOutput.from + ": " 
                  + messageOutput.text + " (" + messageOutput.time + ")"));
                response.appendChild(p);
            }
        </script>
    </head>
    <body onload="disconnect()">
        <div>
            <div>
                <input type="text" id="from" placeholder="Choose a nickname"/>
            </div>
            <br />
            <div>
                <button id="connect" onclick="connect();">Connect</button>
                <button id="disconnect" disabled="disabled" onclick="disconnect();">
                    Disconnect
                </button>
            </div>
            <br />
            <div id="conversationDiv">
                <input type="text" id="text" placeholder="Write a message..."/>
                <button id="sendMessage" onclick="sendMessage();">Send</button>
                <p id="response"></p>
            </div>
        </div>

    </body>
</html>

7、测试

为了测试,可以多打开几个浏览器窗口并访问聊天页面:

http://localhost:8080

输入昵称并点击连接按钮后,就可以加入聊天了。此时撰写并发送一条消息,就可以在所有已加入聊天的浏览器会话中看到它。

效果如下:

websockets 聊天客户端

8、总结

本文介绍了 Spring 的 WebSocket 支持,以及如何使用 sockjs 和 stomp JavaScript 库构建了一个简单的客户端应用。


Ref:https://www.baeldung.com/websockets-spring