背景
前面我们在一文带你快速入门websocket中给大家分享了如何springboot项目中使用websocket,但是这样的websocket是有缺陷的,那就是它缺乏我们在工作中需要的一些高级特性,比如心跳检测,自动重连,以及自适应降级和消息路由等功能。这也就意味着如果我们需要使用这些功能,就需要开发者在websocket的基础上手动实现,而实现这些并不简单。
说下这些高级功能的作用:
- 自动重连:当websocket客户端和服务端断开连接后,客户端能够自动重试,重新连接websocket服务端。
- 心跳检测:用于检测websocket服务端是否还活着,是否能继续提供服务。
- 自适应降级:当浏览器不支持websocket协议时,会自动将协议降级为HTTP长轮训,也就是通过HTTP长轮训来模拟websocket,从而让我们的应用有更好的兼容性,可以支持各种客户端。
- 消息路由:这个有点像spring中的
@RequestMapping
注解,即将请求转发给哪个handler去处理。消息路由就是客户端可以通过不同的路由将消息发送给不同的websocket处理器。
那么有没有什么好用的第三方库在websocket的基础上将这些功能封装好了供我们开箱即用呢?这就是我们今天要讲的主题,那就是如何在springboot项目中使用升级版的websocket。
经过一番调研,我发现有两个技术满足需求,一个是socket.io,另一个则是STOMP协议。这两个框架都实现了自动重连,消息路由,以及自适应降级的功能,区别在于STOMP协议需要用到消息中间件,而socket.io不需要。我的理解是:如果业务中需要使用消息中间件来实现业务解耦,则使用STOMP协议,否则就使用socket.io。今天我们要讲的是socket.io,主要是带大家快速入门,了解socket.io的基本使用。
使用socket.io
引入socket.io依赖
首先我们需要引入socket.io的依赖,如下:
<!-- 添加 Socket.IO 依赖, 仅支持socket-io server 2.x 版本 -->
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.25</version>
</dependency>
将socket.io服务器注册到spring容器中
引入依赖后,我们需要创建socket.io服务器,考虑到我们只需要创建一个实例对象,所以我们将其注册为spring中的bean,这是使用单例模式最简单的方式,而且后面我们可以通过@Autowire
在任何地方注入这个socket实例对象,用起来也更方便。
package com.lizemin.socketIo.config;
import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置和管理 Socket.IO 服务器
*/
@Configuration
public class SocketIoConfig {
@Bean
public SocketIOServer socketIOServer() {
// 创建 SocketConfig 实例,用于配置底层 Socket 连接
SocketConfig socketConfig = new SocketConfig();
// 设置 TCP 无延迟,禁用 Nagle 算法,数据会立即发送,减少延迟
socketConfig.setTcpNoDelay(true);
// 设置 Socket 关闭时的延迟时间为 0 秒,即立即关闭
socketConfig.setSoLinger(0);
// 创建 netty - socketio 的 Configuration 实例,用于配置 Socket.IO 服务器
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
// 将 SocketConfig 配置应用到 Socket.IO 服务器配置中
config.setSocketConfig(socketConfig);
// 设置 Socket.IO 服务器监听的主机名
config.setHostname("localhost");
// 设置 Socket.IO 服务器监听的端口号
config.setPort(9092);
// 设置跨域
config.setOrigin("http://localhost:8401");
// 使用配置创建并返回一个 SocketIOServer 实例
return new SocketIOServer(config);
}
/**
* 创建 SpringAnnotationScanner 实例,用于扫描@OnConnect, @OnEvent, @OnDisconnect等注解
*
* @param socketServer SocketIOServer 实例
* @return SpringAnnotationScanner 实例
*/
@Bean
public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
return new SpringAnnotationScanner(socketServer);
}
}
启动socket.io服务器
然后我们需要在启动springboot应用的同时,也启动socket.io服务器,具体实现如下:
package com.lizemin.socketIo;
import com.corundumstudio.socketio.SocketIOServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import javax.annotation.PreDestroy;
@Slf4j
@SpringBootApplication
public class SocketIoApplication implements CommandLineRunner {
@Autowired
private SocketIOServer socketIOServer;
public static void main(String[] args) {
SpringApplication.run(SocketIoApplication.class, args);
}
@Override
public void run(String... args) {
// 启动 SocketIOServer
socketIOServer.start();
log.info("Socket.IO 服务器已启动,端口: 9092");
}
@PreDestroy
public void stop() {
// 关闭 SocketIOServer
if (socketIOServer != null) {
socketIOServer.stop();
}
}
}
org.springframework.boot.CommandLineRunner
接口就是专门用来做项目启动后的一些初始化工作的,我们实现这个接口中的方法,用来启动socket.io服务器。
接下来我们还需要编写websocket消息的处理逻辑,比如监听客户端的消息发送事件,建立连接的事件,断开连接的事件,这些和我们之前在springboot中使用原生的websocket是一样的。
编写socket.io服务端处理消息的逻辑
下面是我编写的websocket服务端的消息处理逻辑:
package com.lizemin.socketIo.simple;
import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIONamespace;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Component
public class MessageHandler {
/**
* 构造函数,用于定时发送消息
*
* @param server SocketIOServer 实例
*/
@Autowired
public MessageHandler(SocketIOServer server) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String currentTime = dateFormat.format(new Date());
// 向所有连接的客户端发送时间
server.getAllClients().forEach(client -> {
// 往time事件发送消息,只有监听time事件的socket.io客户端才会收到这条消息
client.sendEvent("time", currentTime);
});
}, 0, 1, TimeUnit.SECONDS); // 初始延迟0秒,之后每隔1秒执行
}
// 监听连接建立的事件
@OnConnect
public void onConnect(SocketIOClient client) {
System.out.println("新客户端连接: " + client.getSessionId());
}
// 监听断开连接的事件
@OnDisconnect
public void onDisconnect(SocketIOClient client) {
System.out.println("客户端断开: " + client.getSessionId());
}
// 监听客户端发过来的消息, 监听message事件
@OnEvent("message")
public void onMessage(SocketIOClient client, AckRequest ackRequest, String message) {
System.out.println("收到消息: " + message);
// 给所有socket-io的客户端发送消息
client.getNamespace().getAllClients().forEach(cli -> {
// 给message事件发消息,只有监听message事件的socket.io客户端才会收到这条消息
cli.sendEvent("message", "服务器收到: " + message);
});
// 如果客户端要求服务器回复,则给客户端一个确认消息
if (ackRequest.isAckRequested()) {
ackRequest.sendAckData("消息已处理");
}
}
// 监听onPlayGames事件
@OnEvent("onPlayGames")
public void onPlayGames(SocketIOClient client, AckRequest ackRequest, String message) {
System.out.println("收到消息: " + message);
// 给所有socket-io的客户端发送消息
SocketIONamespace namespace = client.getNamespace();
namespace.getAllClients().forEach(cli -> {
// 给onPlayGames事件发消息,只有监听onPlayGames事件的socket.io客户端才会收到这条消息
cli.sendEvent("onPlayGames", "服务器收到: " + message);
});
// 如果客户端要求服务器回复,则给客户端一个确认消息
if (ackRequest.isAckRequested()) {
// 回复客户端
ackRequest.sendAckData("消息已处理");
}
}
}
简单说下这个类的代码逻辑:
@OnEvent("事件名称")
:表示监听客户端的哪个事件,有点类似于springMVC中的@RequestMapping
注解,当用户往这个事件发消息时,请求会转发给监听该事件的方法。- 通过ScheduledExecutorService和socker.io server的实例对象创建了一个定时任务,每秒中往
time
事件发送消息,这样监听time
事件的socket.io客户端就会每秒中收到服务端发过来的时间。
编写socket.io客户端
使用socket.io客户端同样需要引入依赖,这里我引入的客户端的版本是2.4.0
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.4.0/socket.io.js"></script>
需要注意的是:引入的socket.io的客户端和服务端的版本要兼容,不然会出问题。
下面是socket.io客户端的完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Netty-SocketIO Demo</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.4.0/socket.io.js"></script>
<meta charset="UTF-8">
</head>
<body>
<label for="message">输入消息:</label><input type="text" id="message" placeholder="输入消息">
<button onclick="sendMessage()">发送</button>
<div id="output"></div>
<script>
// 连接服务器
const socket = io('http://localhost:9092');
// 监听服务端发过来的消息,监听message事件,对其他事件漠不关心
socket.on('message', (data) => {
const div = document.createElement('div');
div.textContent = data;
document.getElementById('output').appendChild(div);
});
// 给socket-io服务端发送消息
function sendMessage() {
const input = document.getElementById('message');
socket.emit('message', input.value);
input.value = '';
}
</script>
</body>
</html>
接下来,咱们测试一下上面的功能是否正常。
测试使用socket.io
首先我们启动springboot项目
然后咱们访问socket.io的客户端页面,输入消息,点击发送
可以看到服务器立刻就回复了我们
而且控制台的日志也符合代码的处理逻辑。
我们编写的socket.io客户端只监听了message
事件,对于服务端的time
事件和onPlayGames
事件都没有监听,接下来咱们使用postman来测试下这两个事件,最新版的postman也可以作为socket.io的客户端。
首先通过如下几步创建socket.io客户端
下面是我的socket.io客户端的配置,我为这个客户端添加了几个它监听的事件,其中就包括咱们前面还没测过的time
事件和onPlayGames
事件
由于咱们的socket.io服务端使用的2.xx版本的,所以客户端也要设置使用socket.io 2版本的,不然是连不上的,如下图所示:
最后咱们点击connect
按钮,可以看到服务端每隔一秒发过来的时间,如下图所示:
另外,咱们还可以通过postman给服务端的某个事件发送消息,如下图所示:
在上图中,我们通过postman给onPlayGames
事件发送消息,消息的内容是:“玩游戏吗”,下面是服务器返回的结果:
这和我们的代码处理逻辑也是一致的
文章中完整示例代码的地址
最后的总结
今天这篇文章中,我们主要带大家学习如何使用升级版的websocket,也就是socket.io,它相当于是在websocket的基础上增加了很多高级功能,比如自动重连,自适应降级,消息路由等等。
最后我们还手把手带大家如何通过postman来测试socket.io发送和接受消息的功能。
觉得有收获的朋友可以点个赞,您的鼓励就是我最大的动力!