Spring WebSocket实现实时通信的详细教程

WebSocket 是基于TCP/IP协议,独立于HTTP协议的通信协议。WebSocket 连接允许客户端和服务器之间的全双工通信,以便任何一方都可以通过已建立的连接将数据推送到另一方。

我们常用的HTTP是客户端通过「请求-响应」的方式与服务器建立通信的,必须是客户端主动触发的行为,服务端只是做好接口被动等待请求。而在某些场景下的动作,是需要服务端主动触发的,比如向客户端发送消息、实时通讯、远程控制等。客户端是不知道这些动作几时触发的,假如用HTTP的方式,那么设备端需要不断轮询服务端,这样的方式对服务器压力太大,同时产生很多无效请求,且具有延迟性。于是才采用可以建立双向通讯的长连接协议。通过握手建立连接后,服务端可以实时发送数据与指令到设备端,服务器压力小。

理解java spring boot框架

工厂模式

在学习Spring WebSocket的时候,发现很多教程都是使用工厂模式来创建WebSocketHandler和HandshakeInterceptor,但是不知道工厂模式的具体作用。
所以专门学习了一下

在Spring Boot中,工厂模式主要是通过BeanFactory接口及其实现来体现的。BeanFactory是Spring的核心接口,它是一个高级的工厂,能够维护不同Bean的定义,并负责Bean的创建和管理。ApplicationContext是BeanFactory的一个子接口,提供了更多与Spring整合的功能,比如事件传递、消息解析等。

在Spring Boot中,工厂模式可以通过几种方式实现:

通过FactoryBean接口实现:

FactoryBean是一个专门的工厂接口,用于生成其他Bean的实例。开发者可以自定义FactoryBean,通过实现getObject()方法来返回Bean的实例。这种方式适用于创建复杂的Bean,或者Bean的创建需要进行复杂的初始化过程。

通过@Bean注解配置方法实现:

在Spring配置类中,可以使用@Bean注解标注一个方法,这个方法返回一个对象的实例。Spring容器调用这个方法,并将返回的对象注册为一个Bean。这允许开发者编程方式控制Bean的创建逻辑。

通过@Configuration类的方法实现:

类似于@Bean注解,@Configuration注解的类中定义的方法可以返回Bean的实例。这些方法可以依赖注入其他Bean,实现更复杂的配置逻辑。

BeanFactory

方法有很多,比如 获取别名呀,类型呀,是否是单例,原型等

通过 getBean 去获取对象

主要作用

根据 BeanDefinition 生成相应的 Bean 对象

FactoryBean

FactoryBean是一个专门的工厂接口,用于生成其他Bean的实例。开发者可以自定义FactoryBean,通过实现getObject()方法来返回Bean的实例。这种方式适用于创建复杂的Bean,或者Bean的创建需要进行复杂的初始化过程。
通过 getObject 方法来返回一个对象
获取对象时:

如果 beanName 没有加 & 号,则获取的是泛型T 的对象。
如果添加了 & 号,获取的是实现了 FactoryBean 接口本身的对象,如 EhCacheFactoryBean

而正因为它的小巧,它也被广泛的应用在Spring内部,以及Spring与第三方框架或组件的整合过程中。

BeanFactory 和 FactoryBean 的区别是什么?

BeanFactory 是一个大工厂,是IOC容器的根基,有繁琐的 bean 生命周期处理过程,可以生成出各种各样的 Bean

FactoryBean 是一个小工厂,它自己也是一个 Bean ,但是可以生成其他 Bean

引入依赖

WebSocketConfigurer实例

WebSocketConfig.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.example.websocket.config;

import org.example.websocket.hander.ChatHandler;
import org.example.websocket.hander.ChatHandshakeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;

@SuppressWarnings("ALL")
@Configuration
public class WebSocketConfig {
@Bean
public WebSocketConfigurer webSocketConfigurer(@Autowired ChatHandler chatHandler,
@Autowired ChatHandshakeInterceptor chatInterceptor) {
return new WebSocketConfigurer() {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler((WebSocketHandler) chatHandler, "/chat").addInterceptors((HandshakeInterceptor) chatInterceptor);
}
};

};
}

其中:

1
2
3
@SuppressWarnings("ALL")
@Configuration
public class WebSocketConfig

@SuppressWarnings(“ALL”):抑制所有编译器警告。这通常不是最佳实践,但在这里可能是为了避免某些类型转换或未使用的警告。
@Configuration:这是一个 Spring 的注解,表示该类是一个配置类,Spring 会扫描它并处理其中的 @Bean 定义。

@Bean
表示这个方法会返回一个 Spring Bean,Spring 容器会管理这个 Bean 并将其注入到需要的地方。
这里返回的是一个 WebSocketConfigurer 类型的对象。

之前没接触过@Bean注解,所以不知道这个注解的作用。这里详细了解一下:

@Bean 的含义
在 Spring 框架中,@Bean 是一个方法级别的注解,通常用在 @Configuration 标注的配置类中。它的作用是告诉 Spring 容器:“这个方法会返回一个对象,我希望你(Spring)把这个对象注册为一个 Bean,并由你来管理它。”

Bean 是什么?
在 Spring 中,Bean 是一个由 Spring 容器管理的对象。Spring 容器负责创建这些对象、配置它们、注入依赖关系,并在需要时销毁它们。
简单来说,Bean 就是 Spring 帮你管理的“组件”或“实例”
@Bean 的作用:

当你在一个方法上加上 @Bean 注解时,Spring 会在应用启动时调用这个方法,并将方法返回的对象注册为一个 Bean。
这个 Bean 会被存储在 Spring 的应用上下文(Application Context)中,之后可以通过依赖注入(例如 @Autowired)在其他地方使用。

在代码中:
@Bean 的作用:
这个方法 webSocketConfigurer 返回一个 WebSocketConfigurer 类型的对象。
@Bean 告诉 Spring:“请调用这个方法,把返回的 WebSocketConfigurer 对象注册为一个 Bean,并由你(Spring)来管理它。”

接着学习函数的具体内容:

1
2
3
4
5
6
7
8
9
10
11
@Bean
public WebSocketConfigurer webSocketConfigurer(@Autowired ChatHandler chatHandler,
@Autowired ChatHandshakeInterceptor chatInterceptor) {
return new WebSocketConfigurer() {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler((WebSocketHandler) chatHandler, "/chat")
.addInterceptors((HandshakeInterceptor) chatInterceptor);
}
};
}

其中

1
2
@Autowired ChatHandler chatHandler,
@Autowired ChatHandshakeInterceptor chatInterceptor

@Override 是 Java 中的一个注解(Annotation),用于标记一个方法是 重写(Override) 了父类或接口中定义的方法。我来详细解释它的作用、用法和意义。

@Autowired 的作用:

这是 Spring 提供的一个注解,用于实现 依赖注入(Dependency Injection, DI)。
依赖注入是 Spring 核心特性之一,它允许开发者不必手动创建对象,而是让 Spring 容器自动将需要的对象(Bean)注入到指定位置。
在这里,@Autowired 标注在方法参数上,告诉 Spring:“请自动为我提供 ChatHandler 和 ChatHandshakeInterceptor 的实例。”

注入的时机:

当 Spring 调用 webSocketConfigurer 方法来创建 WebSocketConfigurer Bean 时,会先解析方法参数上的 @Autowired。
Spring 会从它的容器(Application Context)中查找是否有符合类型的 Bean(即 ChatHandler 和 ChatHandshakeInterceptor 的实例),然后将它们注入到方法参数中。

工作原理:

Spring 容器在启动时会扫描所有标注了 @Component(或其他衍生注解,如 @Service、@Repository 等)的类,或者通过 @Bean 定义的 Bean。
如果 ChatHandler 和 ChatHandshakeInterceptor 是 Spring 管理的 Bean(例如,它们可能在其他地方被定义为 @Component 或 @Bean),Spring 就会找到它们并注入。

以及

return new WebSocketConfigurer() {…} 的用法:是 Java 中创建 匿名类(Anonymous Inner Class) 的一种方式。
因为在 Java 中,接口(例如 WebSocketConfigurer)不能直接实例化,必须通过实现它的类来创建对象。

这里没有显式定义一个独立的类(比如 class MyConfigurer implements WebSocketConfigurer),而是直接在代码中通过匿名类实现接口并实例化。
这种方式适用于只需要一次性使用的简单实现,避免了单独定义一个类的麻烦。

registry.addHandler((WebSocketHandler) chatHandler, “/chat”):
addHandler 是 WebSocketHandlerRegistry 的方法,用于注册一个 WebSocket 处理器。

参数:

(WebSocketHandler) chatHandler:将 chatHandler 转换为 WebSocketHandler 类型,表示它负责处理 WebSocket 请求。

“/chat”:定义 WebSocket 的端点路径,例如客户端可以通过 ws://hostname/chat 连接。
作用:告诉 Spring,当客户端连接到 /chat 时,使用 chatHandler 处理连接和消息。

.addInterceptors((HandshakeInterceptor) chatInterceptor):

addInterceptors 是 addHandler 返回的配置对象(WebSocketHandlerRegistration)的方法,用于添加握手拦截器。

参数:

(HandshakeInterceptor) chatInterceptor:将 chatInterceptor 转换为 HandshakeInterceptor 类型,表示它会在握手阶段拦截请求。
作用:在连接建立的握手阶段,执行 chatInterceptor 的逻辑(例如验证身份)。

代码执行流程

Spring 调用方法:

Spring 容器在启动时发现 @Bean 注解,调用 webSocketConfigurer 方法。
通过 @Autowired,注入 chatHandler 和 chatInterceptor。

创建匿名类实例:

new WebSocketConfigurer() {…} 创建一个实现了 WebSocketConfigurer 的匿名类实例。
返回给 Spring:

这个实例被返回并注册为 Bean。

Spring 使用配置:

Spring 的 WebSocket 基础设施调用这个实例的 registerWebSocketHandlers 方法,将 chatHandler 和 chatInterceptor 注册到 /chat 端点。

运行时行为:

当客户端连接到 /chat 时,先触发 chatInterceptor 的握手逻辑,然后由 chatHandler 处理连接。

处理WebSocket连接

和处理普通HTTP请求不同,没法用一个方法处理一个URL。Spring提供了TextWebSocketHandler和BinaryWebSocketHandler分别处理文本消息和二进制消息,这里我们选择文本消息作为聊天室的协议,因此,ChatHandler需要继承自TextWebSocketHandler

1
2
3
4
@Component
public class ChatHandler extends TextWebSocketHandler {
...
}

当浏览器请求一个WebSocket连接后,如果成功建立连接,Spring会自动调用afterConnectionEstablished()方法,任何原因导致WebSocket连接中断时,Spring会自动调用afterConnectionClosed方法,因此,覆写这两个方法即可处理连接成功和结束后的业务逻辑:

ChatHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example.websocket.hander;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class ChatHandler extends TextWebSocketHandler {
private Map<String, WebSocketSession> clients= new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception{
clients.put(session.getId(),session);
session.getAttributes().put("name","Guest1");
};
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status)throws Exception{
clients.remove(session.getId());
}
}

@Component:这是 Spring 的注解,告诉 Spring 框架这是一个需要管理的组件(bean)。Spring 会自动创建这个类的实例并管理它。
public class ChatHandler:定义了一个名为 ChatHandler 的公开类。
extends TextWebSocketHandler:这个类继承了 Spring 的 TextWebSocketHandler,这是一个专门用来处理基于文本的 WebSocket 消息的抽象类。它提供了处理 WebSocket 连接的方法。
简单理解:这个类是一个 Spring 管理的组件,专门用来处理 WebSocket 连接(比如实时聊天功能)。

  1. 成员变量
    1
    private Map<String, WebSocketSession> clients = new ConcurrentHashMap<>();
    Map<String, WebSocketSession>:定义了一个映射(Map),键是字符串类型(String),值是 WebSocketSession 对象。
    Map 就像一个字典,键是唯一的,可以通过键找到对应的值。
    WebSocketSession 是 Spring 提供的一个类,表示一个 WebSocket 会话(客户端和服务端之间的连接)。
    new ConcurrentHashMap<>():创建了一个线程安全的 Map 实现。
    ConcurrentHashMap 是 Java 提供的一种并发集合,适合多线程环境(比如服务器中多个客户端同时连接)。
    clients:这个变量用来存储所有当前连接的客户端会话,键是会话 ID,值是会话对象。
    简单理解:clients 是一个存储所有连接客户端的“名单表”,可以用会话 ID 找到对应的会话。
    1
    2
    3
    4
    5
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    clients.put(session.getId(), session);
    session.getAttributes().put("name", "Guest1");
    }
    @Override:表示这个方法重写了父类(TextWebSocketHandler)的方法。

public void afterConnectionEstablished(WebSocketSession session):

这是一个生命周期方法,当一个新的 WebSocket 连接建立时自动被调用。
参数 session 是新建立的会话对象。
throws Exception:表示这个方法可能会抛出异常,需要调用者处理。
clients.put(session.getId(), session);:
session.getId():获取这个会话的唯一 ID(字符串类型)。
clients.put(key, value):将键值对放入 clients 这个 Map 中。
这里是将新会话的 ID 和会话对象存入 clients,方便后续查找。

session.getAttributes().put("name", "Guest1");:

session.getAttributes():获取会话的属性集合(类似 Map),可以存储会话相关的自定义数据。
.put(“name”, “Guest1”):在这个会话的属性中添加一个键值对,键是 “name”,值是”Guest1”。
这相当于给这个客户端起了一个默认名字 “Guest1”。
简单理解:当有新客户端连上时,把它的会话存到 clients 名单里,并且给它一个默认名字 “Guest1”。

1
2
3
4
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
clients.remove(session.getId());
}

@Override:同样是重写父类的方法。
public void afterConnectionClosed(WebSocketSession session, CloseStatus status):
这是一个生命周期方法,当 WebSocket 连接关闭时自动被调用。
参数 session 是关闭的会话对象。
参数 status 是关闭状态(包含关闭原因和代码),这里没用到。
throws Exception:可能会抛出异常。
clients.remove(session.getId());:
session.getId():获取关闭会话的 ID。
clients.remove(key):从 clients 这个 Map 中删除指定键对应的条目。
这里是将关闭的会话从 clients 名单中移除。
简单理解:当客户端断开连接时,把它从 clients 名单里删掉。

理解@Bean和@Component

  1. 什么是 Bean?
    在 Spring 中,Bean 是一个被 Spring 容器(Spring IoC 容器)管理的对象。简单来说:

Bean 是一个普通的 Java 对象(比如你的 ChatHandler 类的实例)。
但它不是由你手动用 new 创建的,而是由 Spring 框架自动创建、管理和销毁的。
Bean 的生命周期(创建、初始化、使用、销毁)都由 Spring 容器控制。
Bean 通常会配置一些属性(比如依赖注入),Spring 会帮你把这些依赖关系组装好。
类比:想象 Spring 是一个大厨,Bean 就是一道菜。大厨会按照菜谱(配置)来准备这道菜,你只需要告诉他做什么菜,不用自己动手。

  1. 什么是 @Component?
    @Component 是 Spring 提供的一个注解,作用是标记一个类,表示这个类应该被 Spring 容器识别并管理为一个 Bean。换句话说:

当你给一个类加了 @Component,Spring 会在启动时扫描这个类,自动创建它的实例,并将其注册为一个 Bean。
你不需要显式地写配置文件(比如 XML)来定义这个 Bean,@Component 是一种“自动配置”的方式。
类比:@Component 就像是你告诉大厨:“这个是我要的菜,请你帮我做出来。” 大厨(Spring)看到这个标记后,就会自动把这道菜(Bean)准备好。

  1. @Component 和 Bean 的区别
    虽然 @Component 和 Bean 关系密切,但它们的概念和使用场景有以下区别:

方面 Bean @Component
定义 Bean 是 Spring 容器管理的对象实例。 @Component 是一个注解,用来标记类,让 Spring 将其变为 Bean。
范围 Bean 是一个更广义的概念,任何被 Spring 管理的对象都是 Bean。 @Component 是创建 Bean 的一种具体方式。
创建方式 可以通过多种方式定义 Bean:
- 注解(如 @Component)
- XML 配置
- Java 配置(@Bean 注解) 只能通过在类上加 @Component 注解,由 Spring 自动扫描生成 Bean。
灵活性 更灵活,可以通过配置文件或方法手动指定 Bean 的创建逻辑。 相对简单,适合标准的类,Spring 自动管理。
使用场景 Bean 是结果,描述的是对象本身。 @Component 是手段,用来声明类。
4. 代码中的例子

1
2
3
4
5
@Component
public class ChatHandler extends TextWebSocketHandler {
// ...
}

@Component:告诉 Spring,“ChatHandler 这个类需要你来管理,请创建它的实例”。
Bean:Spring 扫描到 @Component 后,会创建一个 ChatHandler 的实例,这个实例就是 Bean,被 Spring 容器管理。
Spring 的工作流程:

启动时扫描所有带有 @Component 的类。
为 ChatHandler 创建一个实例(Bean)。
将这个 Bean 放入容器中,供其他地方使用(比如依赖注入)。

处理握手拦截器

ChatHandshakeInterceptor.java:

1
2
3
4
5
6
7
8
9
10
11
12
package org.example.websocket.hander;

import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import org.springframework.web.socket.WebSocketSession;

import java.util.List;

public class ChatHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
public ChatHandshakeInterceptor() {
super(List.of("user"));
}
}

这是一个自定义的类 ChatHandshakeInterceptor,继承了 Spring 的 HttpSessionHandshakeInterceptor。
它通过构造函数调用父类的构造方法,传递了一个包含 “user” 的列表

什么是握手拦截器(Handshake Interceptor)?

在 WebSocket 连接中,客户端和服务端建立连接时会进行一次“握手”(Handshake),类似于 TCP 的三次握手,但基于 HTTP 协议。握手拦截器是 Spring 提供的一种机制,允许你在握手过程中插入自定义逻辑,比如:

检查请求头、参数。
从 HTTP 会话(HttpSession)中提取数据。
修改 WebSocket 会话(WebSocketSession)的属性。

简单理解:握手拦截器就像一个“门卫”,在客户端敲门(建立 WebSocket 连接)时检查身份或传递信息。

前端部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Chat</title>
</head>
<body>

<h1>WebSocket Chat</h1>
<div id="message-container"></div> <!-- 用来显示消息 -->
<input type="text" id="message-input" placeholder="Type a message"> <!-- 输入框 -->
<!-- 只修改了按钮的文本内容 -->
<button id="send-button">发送消息</button> <!-- 发送按钮 -->

<script>
var ws = new WebSocket('ws://' + location.host + '/chat');
ws.addEventListener('open', function(event) {
console.log("WebSocket connected.");
});
ws.addEventListener('message', function(event) {
console.log("Message received: " + event.data);

var messageContainer = document.getElementById('message-container');
var newMessage = document.createElement('div');
newMessage.classList.add('message');
newMessage.textContent = 'Server: ' + event.data;
messageContainer.appendChild(newMessage);
});

// 连接关闭时
ws.addEventListener('close', function() {
console.log("WebSocket closed.");
});
document.getElementById('send-button').addEventListener('click', function() {
var messageInput = document.getElementById('message-input');
var message = messageInput.value;

if (message) {
ws.send(message);
console.log("Sent message: " + message);
var messageContainer = document.getElementById('message-container');
var newMessage = document.createElement('div');
newMessage.classList.add('message');
newMessage.textContent = 'You: ' + message;
messageContainer.appendChild(newMessage);
messageInput.value = '';
}
});
ws.addEventListener('error', function(event) {
console.log("WebSocket error:", event);
});
window.chatWs = ws;
</script>

</body>
</html>

部署成功


可以看到浏览器a发送的消息会广播到浏览器b

总结

感觉学到了一些关于java spring boot的框架的知识,以及关于websocket的基础,收获很大,也算是着手用spring boot来写一个项目了