最近在项目中使用到了socketIo,spring boot集成socketIo作为服务端,需要前端页面vue使用socketIoClient连接服务端并监听消息,结果在连接socketIo服务端的时候出现了反复连接的情况,当时这个问题卡住了一天时间,网上面关于这个的问题特别少,就问题描述及解决过程记录如下,以供参考。
简单介绍spring boot后端集成socketIo步骤:
添加依赖:
<dependency>
<groupId></groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.17</version>
</dependency>
这儿使用的是netty-socketIo,netty-socketio是一个开源的服务器端的一个java的实现,它基于Netty框架,可用于服务端推送消息给客户端。
application配置参数:
# SocketIO配置
socketIo:
host: 0.0.0.0
# SocketIO端口
port: 8083
# 连接数大小
workCount: 100
# 允许客户请求
allowCustomRequests: true
# 协议升级超时时间(毫秒),默认10秒,HTTP握手升级为ws协议超时时间
upgradeTimeout: 10000
# Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
pingTimeout: 60000
# Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
pingInterval: 25000
# 设置HTTP交互最大内容长度
maxHttpContentLength: 1048576
# 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
maxFramePayloadLength: 1048576
config配置代码:
/**logger*/
private static final Logger logger = ();
@Value("${}")
private String host;
@Value("${}")
private Integer port;
@Value("${}")
private int workCount;
@Value("${}")
private boolean allowCustomRequests;
@Value("${}")
private int upgradeTimeout;
@Value("${}")
private int pingTimeout;
@Value("${}")
private int pingInterval;
@Value("${}")
private int maxFramePayloadLength;
@Value("${}")
private int maxHttpContentLength;
/**
* SocketIOServer配置
*/
@Bean("socketIOServer")
public SocketIOServer socketIOServer() {
config = new ();
//配置host
// (host);
//配置端口
(port);
//开启Socket端口复用
socketConfig = new ();
(true);
(socketConfig);
//连接数大小
(workCount);
//允许客户请求
(allowCustomRequests);
//协议升级超时时间(毫秒),默认10秒,HTTP握手升级为ws协议超时时间
(upgradeTimeout);
//Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
(pingTimeout);
//Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
(pingInterval);
//设置HTTP交互最大内容长度
(maxHttpContentLength);
//设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
(maxFramePayloadLength);
(, );
/*("http://localhost:3000");*/
return new SocketIOServer(config);
}
/**
* 开启SocketIOServer注解支持
*/
@Bean
public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
return new SpringAnnotationScanner(socketServer);
}
然后是监听代码:
@Component
public class SocketHandler {
/**logger*/
private Logger logger = ();
/**存已连接的客户端*/
private Map<String, List<UUID>> clientMap = new ConcurrentHashMap<>(16);
private final SocketIOServer socketIOServer;
@Autowired
private RedisUtil redisUtil;
@Autowired
public SocketHandler(SocketIOServer socketIOServer) {
= socketIOServer;
}
/**
* 当客户端发起连接时调用
* @param socketIOClient 客户端
*/
@OnConnect
public void onConnect(SocketIOClient socketIOClient) {
//获取socketClient连接参数
String userName = ().getSingleUrlParam(.SOCKET_USER_NAME);
String appKey = ().getSingleUrlParam(.APP_KEY);
String roomId = ().getSingleUrlParam(.SOCKET_ROOM_ID);
Map<String, Object> headers = new HashMap<>();
for (<String, String> entry : ().getHttpHeaders().entries()){
((), ());
}
("header:"+ (headers));
//clientMap存放连接客户端信息
if ((roomId)) {
("用户{}开启长连接通知, roomId: {}, NettySocketSessionId: {}, NettySocketRemoteAddress: {}", userName, roomId, ().toString(), ().toString());
List<UUID> uuidList = new ArrayList<>();
//clientMap-key为appKey与room编号组合
String clientKey = (appKey, .CON_SIGN, roomId);
if(((clientKey))){
uuidList = (clientKey);
}
(());
(clientKey, uuidList);
((clientMap));
//加入房间
(clientKey);
}
}
/**
* 客户端断开连接时调用,刷新客户端信息
* @param socketIOClient 客户端
*/
@OnDisconnect
public void onDisConnect(SocketIOClient socketIOClient) {
String userName = ().getSingleUrlParam(.SOCKET_USER_NAME);
String roomId = ().getSingleUrlParam(.SOCKET_ROOM_ID);
String appKey = ().getSingleUrlParam(.APP_KEY);
if ((userName)) {
("用户{}断开长连接通知, roomId: {}, NettySocketSessionId: {}, NettySocketRemoteAddress: {}",
userName, roomId, ().toString(), ().toString());
//移除客户端
String clientKey = (appKey, .CON_SIGN, roomId);
for (String key : ()){
if ((clientKey)) {
//移除该房间内的client
(key).remove(());
}
}
}
}
/**
* 监听事件
* @param socketIOClient 客户端
* @param ackRequest ack请求
* @param messageDto 消息主体
*/
@OnEvent("ewsSocketMsg")
public void ewsSocketMsg(SocketIOClient socketIOClient, AckRequest ackRequest,
MessageDto messageDto){
String targetRoom = ();
((key, value) ->{
//通过roomId获取
if ((targetRoom)) {
("ewsSocketMsg: 收到客户{}的消息,发送给{}房间,消息内容是{}", (), (), ());
//判断房间内是否有client
if ((value)) {
//获得该房间内所有广播对象发送事件
(key).sendEvent("ewsSocketMsg", messageDto);
}
}
});
}
/**
* 服务端广播消息到所有客户端,自己除外
* @param socketIOClient 客户端
* @param ackRequest ack请求
* @param messageDto 消息主体
*/
@OnEvent("ewsAllClientsMsg")
public void ewsAllClientsMsg(SocketIOClient socketIOClient, AckRequest ackRequest,
MessageDto messageDto){
("ewsAllClientsMsg:收到客户{}的消息,广播发送给其他客户,消息内容是{}",
(), ());
try {
().sendEvent("ewsAllClientsMsg", messageDto);
} catch (Exception e) {
();
}
}
/**
* 去掉clientMap的无效appKey
* 断开socketClient连接
* @param appKey 校验key
*/
public void invalidWebSocket(String appKey) {
("去掉无效appKey");
((key, value) ->{
if ((appKey)) {
(e -> {
//遍历该房间内的所有uuid,获取socketClient并断开连接
SocketIOClient socketIOClient = (e);
();
});
(key);
}
});
("***无效appKey之后clientMap为: {}", (clientMap));
}
public Map<String, List<UUID>> getClientMap() {
return clientMap;
}
public void setClientMap(Map<String, List<UUID>> clientMap) {
= clientMap;
}
}
最后是socketIoServer启动类:
@Component
@Order(1)
public class SocketServer implements CommandLineRunner {
/**
* logger
*/
private static final Logger logger = ();
/**
* socketIOServer
*/
private final SocketIOServer socketIOServer;
@Autowired
public SocketServer(SocketIOServer socketIOServer) {
= socketIOServer;
}
@Override
public void run(String... args) {
("---------- NettySocket通知服务开始启动 ----------");
();
("---------- NettySocket通知服务启动成功 ----------");
}
}
以上为spring boot整合socketIo的步骤,作为WebSocket服务端使用。接下来介绍vue使用-client连接并监听服务端时间的实现:
引入client:import sio from ‘-client’
在methods内添加connect()方法,方法内为具体实现如下:
connect:function(){
let opts = {
query: 'userName=test&appKey=test&roomId=rabbit'
};
// socketIo连接的服务器信息,就是我们后端配置的信息
let socket = ('http://localhost:8083?',opts);
('connect', function () {
('websocket连接成功');
});
let that = this;
('ewsSocketMsg', function (data) {
(data);
= "消息状态:" + ;
});
('disconnect', function () {
('websocket已经下线');
});
/*('connect_error', (error) => {
();
});*/
}
在mounted内执行connect方法:
mounted() {
();
}
在启动了socketIo服务端的情况下,请求vue页面即可触发connect方法完成连接并监听,但是实际场景vue客户端会一直连接socketIo服务端,找了好久没定位到问题。最后发现很可能是因为的握手机制导致的。在进行握手的时候默认采用的是polling轮询机制进行的,当失败时会持续发送握手请求。
解决方案
在opts内添加transport参数的定义即可:
let opts = {
query: 'userName=test&appKey=test&roomId=rabbit',
transports:['websocket']
};
——————————————
JAVA面试知识点相关:JAVA面试知识点
SpringAOP原理使用相关:SpringAOP原理使用详解