最近在项目中使用到了socketIo,spring boot集成socketIo作为服务端,需要前端页面vue使用socketIoClient连接服务端并监听消息,结果在连接socketIo服务端的时候出现了反复连接的情况,当时这个问题卡住了一天时间,网上面关于这个的问题特别少,就问题描述及解决过程记录如下,以供参考。
简单介绍spring boot后端集成socketIo步骤:
添加依赖:
com.corundumstudio.socketio
netty-socketio
1.7.17
这儿使用的是netty-socketIo,netty-socketio是一个开源的Socket.io服务器端的一个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 = LoggerFactory.getLogger(SocketConfig.class);
@Value("${socketIo.host}")
private String host;
@Value("${socketIo.port}")
private Integer port;
@Value("${socketIo.workCount}")
private int workCount;
@Value("${socketIo.allowCustomRequests}")
private boolean allowCustomRequests;
@Value("${socketIo.upgradeTimeout}")
private int upgradeTimeout;
@Value("${socketIo.pingTimeout}")
private int pingTimeout;
@Value("${socketIo.pingInterval}")
private int pingInterval;
@Value("${socketIo.maxFramePayloadLength}")
private int maxFramePayloadLength;
@Value("${socketIo.maxHttpContentLength}")
private int maxHttpContentLength;
/**
* SocketIOServer配置
*/
@Bean("socketIOServer")
public SocketIOServer socketIOServer() {
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
//配置host
// config.setHostname(host);
//配置端口
config.setPort(port);
//开启Socket端口复用
com.corundumstudio.socketio.SocketConfig socketConfig = new com.corundumstudio.socketio.SocketConfig();
socketConfig.setReuseAddress(true);
config.setSocketConfig(socketConfig);
//连接数大小
config.setWorkerThreads(workCount);
//允许客户请求
config.setAllowCustomRequests(allowCustomRequests);
//协议升级超时时间(毫秒),默认10秒,HTTP握手升级为ws协议超时时间
config.setUpgradeTimeout(upgradeTimeout);
//Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
config.setPingTimeout(pingTimeout);
//Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
config.setPingInterval(pingInterval);
//设置HTTP交互最大内容长度
config.setMaxHttpContentLength(maxHttpContentLength);
//设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
config.setMaxFramePayloadLength(maxFramePayloadLength);
config.setTransports(Transport.POLLING, Transport.WEBSOCKET);
/*config.setOrigin("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 = LoggerFactory.getLogger(SocketHandler.class);
/**存已连接的客户端*/
private Map> clientMap = new ConcurrentHashMap<>(16);
private final SocketIOServer socketIOServer;
@Autowired
private RedisUtil redisUtil;
@Autowired
public SocketHandler(SocketIOServer socketIOServer) {
this.socketIOServer = socketIOServer;
}
/**
* 当客户端发起连接时调用
* @param socketIOClient 客户端
*/
@OnConnect
public void onConnect(SocketIOClient socketIOClient) {
//获取socketClient连接参数
String userName = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.SOCKET_USER_NAME);
String appKey = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.APP_KEY);
String roomId = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.SOCKET_ROOM_ID);
Map headers = new HashMap<>();
for (Map.Entry entry : socketIOClient.getHandshakeData().getHttpHeaders().entries()){
headers.put(entry.getKey(), entry.getValue());
}
logger.info("header:"+ JSON.toJSONString(headers));
//clientMap存放连接客户端信息
if (StringUtils.isNotBlank(roomId)) {
logger.info("用户{}开启长连接通知, roomId: {}, NettySocketSessionId: {}, NettySocketRemoteAddress: {}", userName, roomId, socketIOClient.getSessionId().toString(), socketIOClient.getRemoteAddress().toString());
List uuidList = new ArrayList<>();
//clientMap-key为appKey与room编号组合
String clientKey = StringUtils.concatStr(appKey, EwsCommonConstants.SystemParam.CON_SIGN, roomId);
if(CollectionUtils.isNotEmpty(clientMap.get(clientKey))){
uuidList = clientMap.get(clientKey);
}
uuidList.add(socketIOClient.getSessionId());
clientMap.put(clientKey, uuidList);
logger.info(JSON.toJSONString(clientMap));
//加入房间
socketIOClient.joinRoom(clientKey);
}
}
/**
* 客户端断开连接时调用,刷新客户端信息
* @param socketIOClient 客户端
*/
@OnDisconnect
public void onDisConnect(SocketIOClient socketIOClient) {
String userName = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.SOCKET_USER_NAME);
String roomId = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.SOCKET_ROOM_ID);
String appKey = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.APP_KEY);
if (StringUtils.isNotBlank(userName)) {
logger.info("用户{}断开长连接通知, roomId: {}, NettySocketSessionId: {}, NettySocketRemoteAddress: {}",
userName, roomId, socketIOClient.getSessionId().toString(), socketIOClient.getRemoteAddress().toString());
//移除客户端
String clientKey = StringUtils.concatStr(appKey, EwsCommonConstants.SystemParam.CON_SIGN, roomId);
for (String key : clientMap.keySet()){
if (key.equals(clientKey)) {
//移除该房间内的client
clientMap.get(key).remove(socketIOClient.getSessionId());
}
}
}
}
/**
* 监听事件
* @param socketIOClient 客户端
* @param ackRequest ack请求
* @param messageDto 消息主体
*/
@OnEvent("ewsSocketMsg")
public void ewsSocketMsg(SocketIOClient socketIOClient, AckRequest ackRequest,
MessageDto messageDto){
String targetRoom = messageDto.getTargetRoom();
clientMap.forEach((key, value) ->{
//通过roomId获取
if (key.contains(targetRoom)) {
logger.info("ewsSocketMsg: 收到客户{}的消息,发送给{}房间,消息内容是{}", messageDto.getSourceUserName(), messageDto.getTargetRoom(), messageDto.getMsgContent());
//判断房间内是否有client
if (CollectionUtils.isNotEmpty(value)) {
//获得该房间内所有广播对象发送事件
socketIOServer.getRoomOperations(key).sendEvent("ewsSocketMsg", messageDto);
}
}
});
}
/**
* 服务端广播消息到所有客户端,自己除外
* @param socketIOClient 客户端
* @param ackRequest ack请求
* @param messageDto 消息主体
*/
@OnEvent("ewsAllClientsMsg")
public void ewsAllClientsMsg(SocketIOClient socketIOClient, AckRequest ackRequest,
MessageDto messageDto){
logger.info("ewsAllClientsMsg:收到客户{}的消息,广播发送给其他客户,消息内容是{}",
messageDto.getSourceUserName(), messageDto.getMsgContent());
try {
socketIOServer.getBroadcastOperations().sendEvent("ewsAllClientsMsg", messageDto);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 去掉clientMap的无效appKey
* 断开socketClient连接
* @param appKey 校验key
*/
public void invalidWebSocket(String appKey) {
logger.info("去掉无效appKey");
clientMap.forEach((key, value) ->{
if (key.contains(appKey)) {
value.forEach(e -> {
//遍历该房间内的所有uuid,获取socketClient并断开连接
SocketIOClient socketIOClient = socketIOServer.getClient(e);
socketIOClient.disconnect();
});
clientMap.remove(key);
}
});
logger.info("***无效appKey之后clientMap为: {}", JSON.toJSONString(clientMap));
}
public Map> getClientMap() {
return clientMap;
}
public void setClientMap(Map> clientMap) {
this.clientMap = clientMap;
}
}
最后是socketIoServer启动类:
@Component
@Order(1)
public class SocketServer implements CommandLineRunner {
/**
* logger
*/
private static final Logger logger = LoggerFactory.getLogger(ServerRunner.class);
/**
* socketIOServer
*/
private final SocketIOServer socketIOServer;
@Autowired
public SocketServer(SocketIOServer socketIOServer) {
this.socketIOServer = socketIOServer;
}
@Override
public void run(String... args) {
logger.info("---------- NettySocket通知服务开始启动 ----------");
socketIOServer.start();
logger.info("---------- NettySocket通知服务启动成功 ----------");
}
}
以上为spring boot整合socketIo的步骤,作为WebSocket服务端使用。接下来介绍vue使用socket.io-client连接并监听服务端时间的实现:
引入client:import sio from ‘socket.io-client’
在methods内添加connect()方法,方法内为具体实现如下:
connect:function(){
let opts = {
query: 'userName=test&appKey=test&roomId=rabbit'
};
// socketIo连接的服务器信息,就是我们后端配置的信息
let socket = sio.connect('http://localhost:8083?',opts);
socket.on('connect', function () {
console.log('websocket连接成功');
});
let that = this;
socket.on('ewsSocketMsg', function (data) {
console.log(data);
that.messageStatus = "消息状态:" + data.msgContent;
});
socket.on('disconnect', function () {
console.log('websocket已经下线');
});
/*socket.on('connect_error', (error) => {
socket.close();
});*/
}
在mounted内执行connect方法:
mounted() {
this.connect();
}
在启动了socketIo服务端的情况下,请求vue页面即可触发connect方法完成连接并监听,但是实际场景vue客户端会一直连接socketIo服务端,找了好久没定位到问题。最后发现很可能是因为socket.io的握手机制导致的。socket.io在进行握手的时候默认采用的是polling轮询机制进行的,当失败时会持续发送握手请求。
解决方案
在opts内添加transport参数的定义即可:
let opts = {
query: 'userName=test&appKey=test&roomId=rabbit',
transports:['websocket']
};