Spring Boot整合WebSocket
在HTTP协议中,所有的请求都是由客户端发起的,由服务端进行响应,服务端无法向客户端推送消息,但是在一些需要即时通信的应用中,又不可避免地需要服务端向客户端推送消息,传统的解决方案主要有如下几种。
1. 为什么需要WebSocket
- 轮询
轮询是最简单的一种解决方案,所谓轮询,就是客户端在固定的时间间隔下不停地向服务端发送请求,查看服务端是否有最新的数据,若服务端有最新的数据,则返回给客户端,若服务端没有,则返回一个空的JSON或者XML文档。轮询对开发人员而言实现方便,但是弊端也很明显:客户端每次都要新建HTTP请求,服务端要处理大量的无效请求,在高并发场景下会严重拖慢服务端的运行效率,同时服务端的资源被极大的浪费了,因此这种方式并不可取。
- 长轮询
长轮询是传统轮询的升级版,当聪明的工程师看到轮询所存在的问题后,就开始解决问题,于是有了长轮询。不同于传统轮询,在长轮询中,服务端不是每次都会立即响应客户端的请求,只有在服务端有最新数据的时候才会立即响应客户端的请求,否则服务端会持有这个请求而不返回,直到有新数据时才返回。这种方式可以在一定程度上节省网络资源和服务器资源,但是也存在一些问题,例如:
- 如果浏览器在服务器响应之前有新数据要发送,就只能创建-一个新的并发请求,或者先尝试断掉当前请求,再创建新的请求。
- TCP和HTTP规范中都有连接超时一说,所以所谓的长轮询并不能一直持续, 服务端和客户端的连接需要定期的连接和关闭再连接,这又增大了程序员的工作量,当然也有一些技术能够延长每次连接的时间,但毕竟是非主流解决方案。
- Applet和Flash
Applet和Flash都已经是明日黄花,不过在这两个技术存在的岁月里,除了可以让我们的HTML页面更加绚丽之外,还可以解决消息推送问题。开发者可以使用Applet和Flash来模拟全双工通信,通过创建一一个只有 1个像素点大小的透明的Applet或者Flash,然后将之内嵌在网页中,再从Applet或者Flash的代码中创建一一个Socket连接进行双向通信。这种连接方式消除了HTTP协议中的诸多限制,当服务器有消息发送到客户端的时候,开发者可以在Applet或者Flash中调用JavaScript函数将数据显示在页面上,当浏览器有数据要发送给服务器时也一样,通过Applet或者Flash来传递。这种方式真正地实现了全双工通信,不过也有问题,说明如下:
- 浏览器必须能够运行Java或者Flash。
- 无论是Applet还是Flash 都存在安全问题。
- 随着HTML 5标准被各浏览器厂商广泛支持,Flash 下架已经被提上日程( Adobe宣布2020年正式停止支持Flash)。
其实,传统的解决方案不止这三种,但是无论哪种解决方案都有自身的缺陷,于是有了WebSocket。
2. WebSocket简介
WebSocket是一种在单个TCP连接.上进行全双工通信的协议,已被W3C定为标准。使用WebSocket可以使得客户端和服务器之间的数据交换变得更加简单,它允许服务端主动向客户端推送数据。在WebSocket协议中,浏览器和服务器只需要完成一次握手,两者之间就可以直接创建持久性的连接,并进行双向数据传输。
WebSocket使用了HTTP/1.1的协议升级特性,一个WebSocket请求首先使用非正常的HTTP请求以特定的模式访问一个URL,这个URL有两种模式,分别是ws和wss,对应HTTP协议中的HTTP和HTTPS,在请求头中有一个Connection:Upgrade字段,表示客户端想要对协议进行升级,另外还有一个Upgrade:websocket字段,表示客户端想要将请求协议升级为WebSocket协议。这两个字段共同告诉服务器要将连接升级为WebSocket这样一种全双工协议,如果服务端同意协议升级,那么在握手完成之后,文本消息或者其他二进制消息就可以同时在两个方向上进行发送,而不需要关闭和重建连接。此时的客户端和服务端关系是对等的,它们可以互相向对方主动发送消息。和传统的解决方案相比,WebSocket主要有如下特点:
- WebSocket使用时需要先创建连接,这使得WebSocket成为一种有状态的协议,在之后的通信过程中可以省略部分状态信息( 例如身份认证等)。
- WebSocket连接在端口80 (ws)或者443 (wss)上创建,与HTTP使用的端口相同,这样,基本上所有的防火墙都不会阻止WebSocket连接。
- WebSocket使用HTTP协议进行握手,因此它可以自然而然地集成到网络浏览器和HTTP服务器中,而不需要额外的成本。
- 心跳消息(ping 和pong)将被反复的发送,进而保持WebSocket连接一直处于活跃状态。
- 使用该协议,当消息启动或者到达的时候,服务端和客户端都可以知道。
- WebSocket连接关闭时将发送一个特殊的关闭消息。
- WebSocket支持跨域,可以避免Ajax的限制。
- HTTP规范要求浏览器将并发连接数限制为每个主机名两个连接,但是当我们使用WebSocket的时候,当握手完成之后,该限制就不存在了,因为此时的连接已经不再是HTTP连接了。
- WebSocket协议支持扩展,用户可以扩展协议,实现部分自定义的子协议。
- 更好的二进制支持以及更好的压缩效果。
WebSocket既然具有这么多优势,使用场景当然也是非常广泛的,例如:
- 在线股票网站。
- 即时聊天。
- 多人在线游戏。
- 应用集群通信。
- 系统性能实时监控。
…
在了解了这么多WebSocket的基本信息后,接下来看看在Spring Boot中如何使用WebSocket。
3. Spring Boot整合WebSocket
Spring Boot 对WebSocket 提供了非常友好的支持,可以方便开发者在项目中快速集成WebSocket功能,实现单聊或者群聊。
3.1 消息群发
3.1.1 创建项目
首先创建一个Spring Boot项目,添加如下依赖:
<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.4</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
spring-bot-starter-websocket依赖是Web Socket相关依赖,其他的都是前端库,使用jar包的形式对这些前端库进行统一管理, 使用webjar添加到项目中的前端库,在Spring Boot项目中已经默认添加了静态资源过滤,因此可以直接使用。
3.1.2 配置WebSocket
Spring框架提供了基于WebSocket的STOMP支持,STOMP是一个简单的可互操作的协议,
通常被用于通过中间服务器在客户端之间进行异步消息传递。WebSocket 配置如下:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 设置消息代理的前缀,如果消息的前缀为"/topic",就会将消息转发给消息代理(broker)
// 再由消息代理广播给当前连接的客户端
config.enableSimpleBroker("/topic");
// 下面方法可以配置一个或多个前缀,通过这些前缀过滤出需要被注解方法处理的消息。
// 例如这里表示前缀为"/app"的destination可以通过@MessageMapping注解的方法处理
// 而其他 destination(例如"/topic""/queue")将被直接交给 broker 处理
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 定义一个前缀为"/chart"的endpoint,并开启 sockjs 支持。
// sockjs 可以解决浏览器对WebSocket的兼容性问题,客户端将通过这里配置的URL建立WebSocket连接
registry.addEndpoint("/chat").withSockJS();
}
}
代码解释:
- 自定义类WebSocketConfig 继承自WebSocketMessageBrokerConfigurer 进行WebSocket配置,然后通过@EnableWebSocketMessageBroker注解开启WebSocket消息代理。
- config.enableSimpleBroker(“/topic”)表示设置消息代理的前缀,即如果消息的前缀是“/topic" ,就会将消息转发给消息代理( broker),再由消息代理将消息广播给当前连接的客户端。
- config.setApplicationDestinationPrefixes(“/app”)表示配置一个或多个前缀,通过这些前缀过滤出需要被注解方法处理的消息。例如,前缀为“/app”的destination可以通过@MessageMapping注解的方法处理,而其他destination(例如“/topic”“/queue”)将被直接交给broker处理。
- regitry.addEndpoint(“/chat”).withSockJS()则表示定义一个前缀为“/chat” 的endPoint,并开启sockjs支持,sockjs 可以解决浏览器对WebSocket的兼容性问题,客户端将通过这里配置的URL来建立WebSocket连接。
3.1.3 定义Controller
定义一个Controller用来实现对消息的处理,代码如下:
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Message greeting(Message message) throws Exception {
return message;
}
}
自定义的Message对象代码如下:
@Data
public class Message {
private String name;
private String content;
}
根据第2步的配置,@MessageMapping(“hello”)注解的方法将用来接收“/apphello"路径发送来的消息,在注解方法中对消息进行处理后,再将消息转发到@SendTo定义的路径上,而@SendTo路径是一个前缀为“/topic” 的路径,因此该消息将被交给消息代理broker,再由broker 进行广播。
3.1.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>
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("");
}
// 建立一个WebSocket连接
function connect() {
// 用户名不能为空
if (!$("#name").val()) {
return;
}
// 首先使用 SockJS 建立连接
var socket = new SockJS("/chat");
// 然后创建一个STOMP实例发起连接请求
stompClient = Stomp.over(socket);
// 连接成功回调
stompClient.connect({}, function (frame) {
// 进行页面设置
setConnected(true);
// 订阅服务端发送回来的消息
stompClient.subscribe('/topic/greetings', function (greeting) {
// 将服务端发送回来的消息展示出来
showGreeting(JSON.parse(greeting.body));
});
});
}
// 断开WebSocket连接
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(); });
});
</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>
代码解释:
- connect 方法表示建立一个WebSocket连接,在建立WebSocket连接时,用户必须先输入用户名,然后才能建立连接。
- 第19~26行首先使用SockJS建立连接,然后创建一个STOMP实例发起连接请求,在连接成功的回调方法中,首先调用setConnected(true);方 法进行页面的设置,然后调用STOMP中的subscribe方法订阅服务端发送回来的消息,并将服务端发送来的消息展示出来(使用showGreeting方法)。
- 调用STOMP中的disconnect 方法可以断开一个WebSocket连接。
3.1.5 测试
接下来启动Spring Boot 项目进行测试,在浏览器中输入htp:/ocalhost:8080/chat.html,显示结果如图所示。
用户首先输入用户名,然后单击“连接”按钮,结果如图所示。
然后换一个浏览器,或者使用Chrome浏览器的多用户(注意不是多窗口),重复刚才的步骤,这样就有两个用户连接上了,接下来就可以开始群聊了(当然也可以有更多的用户连接上来),如图所示。
3.2 消息点对点发送
在3.1小节中介绍的消息发送使用到了@SendTo注解,该注解将方法处理过的消息转发到broker,再由broker进行消息广播。除了@SendTo注解外, Spring 还提供了SimpMessagingTemplate类来让开发者更加灵活地发送消息。
3.2.1 添加依赖
既然是点对点发送,就应该有用户的概念,因此,首先在项目中加入Spring Security的依赖,代码如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.2.2 配置Spring Security
对Spring Security进行配置,添加两个用户,同时配置所有地址都认证后才能访问,代码如下:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 指定密码的加密方式
@SuppressWarnings("deprecation")
@Bean
PasswordEncoder passwordEncoder(){
// 不对密码进行加密
return NoOpPasswordEncoder.getInstance();
}
// 配置用户及其对应的角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("123").roles("ADMIN","USER")
.and()
.withUser("suohe").password("123").roles("USER");
}
// 配置 URL 访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 开启 HttpSecurity 配置
.anyRequest().authenticated() // 用户访问所有地址都必须登录认证后访问
.and().formLogin().permitAll(); // 开启表单登录
}
}
这里就是Spring Security的一个常规配置,相关配置含义可以参考我前面文章。
3.2.3 改造WebSocket配置
接下来对WebSocket配置进行改造,代码如下:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 设置消息代理的前缀,如果消息的前缀为"/topic"、"/queue",就会将消息转发给消息代理(broker)
// 再由消息代理广播给当前连接的客户端
config.enableSimpleBroker("/topic","/queue");
// 下面方法可以配置一个或多个前缀,通过这些前缀过滤出需要被注解方法处理的消息。
// 例如这里表示前缀为"/app"的destination可以通过@MessageMapping注解的方法处理
// 而其他 destination(例如"/topic""/queue")将被直接交给 broker 处理
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 定义一个前缀为"/chart"的endpoint,并开启 sockjs 支持。
// sockjs 可以解决浏览器对WebSocket的兼容性问题,客户端将通过这里配置的URL建立WebSocket连接
registry.addEndpoint("/chat").withSockJS();
}
}
这里的修改是在config.enableSimpleBroker(“/topic”);方法的基础 上又增加了一个broker 前缀“/queue”,方便对群发消息和点对点消息进行管理。
3.2.4 配置Controller
对WebSocket的Controller 进行改造,代码如下:
@Controller
public class GreetingController {
@Autowired
SimpMessagingTemplate messagingTemplate;
// 处理来自"/app/hello"路径的消息
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Message greeting(Message message) throws Exception {
return message;
}
// 处理来自"/app/chat"路径的消息
@MessageMapping("/chat")
public void chat(Principal principal, Chat chat) {
// 获取当前登录用户的用户名
String from = principal.getName();
// 将用户设置给chat对象的from属性
chat.setFrom(from);
// 再将消息发送出去,发送的目标用户就是 chat 对象的to属性值
messagingTemplate.convertAndSendToUser(chat.getTo(),
"/queue/chat", chat);
}
}
代码解释:
-
群发消息依然使用@SendTo注解来实现,点对点的消息发送则使用SimpMessagingTemplate来实现。
-
第10~16 行定义了一个新的消息处理接口,@MessageMapping("/chat)注 解表示来自“/app/chat”路径的消息将被chat 方法处理。chat 方法的第一个参数Principal可以用来获取当前登录用户的信息,第二个参数则是客户端发送来的消息。
-
在chat 方法中,首先获取当前用户的用户名,设置给chat对象的from属性,再将消息发送出去,发送的目标用户就是chat对象的to属性值。
-
消息发送使用的方法是convertAndSendToUser,该方法内部调用了convertAndSend方法,并对消息路径做了处理,部分源码如下:
public void convertAndSendToUser(String user, String destination, Object payload) throws MessagingException { this.convertAndSendToUser(user, destination, payload, (MessagePostProcessor)null); } public void convertAndSendToUser(String user, String destination, Object payload) throws MessagingException { this.convertAndSendToUser(user, destination, payload, (MessagePostProcessor)null); }//这里destinationPrefix 的默认值是“/user”, 也就是说消息的最终发送路径是“/user/用户名/queue/chat"。
-
chat是一个普通的JavaBean, to 属性表示消息的目标用户,from 表示消息从哪里来,content则是消息的主体内容。
3.2.5 创建在线聊天页面
在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>
var stompClient = null;
// 建立一个WebSocket连接
function connect() {
// 首先使用 SockJS 建立连接
var socket = new SockJS('/chat');
// 然后创建一个STOMP实例发起连接请求
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(); });
});
</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文件基本与前文的前面js文件内容一致, 差异主要体现在三个地方:
- 连接成功后,订阅的地址为“/user/queue/chat”, 该地址比服务端配置的地址多了“/user” 前缀,这是因为SimpMessagingTemplate类中自动添加了路径前缀。
- 聊天消息发送路径为“/app/chat”。
- 发送的消息内容中有一个to字段,该字段用来描述消息的目标用户。
3.2.6 测试
经过如上几个步骤之后,一个点对点的聊天服务就搭建成功了,接下来直接在浏览器地址栏中输入http://calhost:8080/chat.html,首先会自动跳转到Spring Security的默认登录页面,分别使用一开始配置的两个用户admin/123和suohe/123登录,登录成功后,就可以开始在线聊天了,如图所示。文章来源:https://www.toymoban.com/news/detail-452668.html
文章来源地址https://www.toymoban.com/news/detail-452668.html
到了这里,关于Spring Boot整合WebSocket的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!