前言
在之前的文章中完成了客服对话
的Demo功能,但是现在的连接是无限制
的长时间连接没有做心跳
、失活
、超时断连
等功能,心跳的实现方法有很多种,并且WebSocket
就提供了ping/pong
类型的消息。
心跳的触发方式也分两种:
-
客户端触发:
如果是前端发送心跳,后端需要返回心跳,也就是ping pong的过程会有两次数据传递。
-
服务端触发:
后端来发送心跳的话,就只需要发送ping,前端不需要回应。
这两种后续的处理方式也有各自优缺点。
-
客户端触发:
-
优点
灵活控制
无需设置主动超时
逻辑清晰
服务端简单
-
缺点
两次消息传递
消息内容容易篡改
-
优点
-
服务端触发:
-
优点
节省宽带
服务端控制频率
消息体固定
-
缺点
处理逻辑复杂
需要添加定时任务
考虑稳定性
-
优点
两种方式各有利弊,看具体的
应用场景
选择心跳方式是最好的,这里使用客户端触发心跳进行Demo
实验,前端变更比较容易,服务端也不需要写定时等处理复杂的业务,只需要在收到固定消息后返回对应消息即可。
1. WebSocket心跳
客户端触发心跳的话就是在服务端的OnMessage
事件里进行截获处理,如果是接受参数为String
,就在之前的逻辑之上加上判断健康检查
的逻辑,功能很简单,客户端发送了特点消息直接返回对应的消息即可。
1.1 字符串消息
WebSocket
已经设计了心跳,也就是Ping/Pong
,这个功能可以到达检测链接是否可用,但是如果要携带数据还是需要自己用字符串
、对象
的消息类型进行实现。
代码如下:
@OnMessage
public void onMessage(String message, Session session,@PathParam("clientId") String clientId){
/**
* 持久化
*/
baseWebSocketService.saveClientSendMsg(clientId,message,new Date());
/**
* 处理消息
*/
UserMessageModel userMessageModel = JSONObject.parseObject(message, UserMessageModel.class);
if (userMessageModel == null){
this.sendMessage(BaseResponseMessage.error(null,"传递参数结构异常"));
}
userMessageModel.setSendId(clientId);
/**
* 健康检查
*/
if ("HEALTH".equals(userMessageModel.getMessage())){
this.sendText(WebSocketHealthEnum.HEALTH.result);
return;
}
/**
* 发送消息
*/
HashMap<String,WebSocketClient> hashMap = webSocketClientMap.get(WebSocketTypeEnum.getAcceptType(this.type));
if (!CollectionUtils.isEmpty(hashMap)){
if (StringUtils.isEmpty(bindKfClients.get(this.clientId))){
List<UserMessageModel> list = new ArrayList();
list.addAll(baseWebSocketService.queryClientSendMsg(clientId));
list.forEach(model-> {
this.toCSucceed(model);
});
}else{
this.toCSucceed(userMessageModel);
}
}else{
baseWebSocketService.saveClientCompensateMsg(userMessageModel.getAcceptId(),message,(byte) 0);
log.info("客户端:{} 发送消息到接受端:{} 不在线,放置到代发送列表,当前待发送列表:{}条",clientId,userMessageModel.getAcceptId());
this.sendMessage(BaseResponseMessage.error(null,"接收端不在线"));
}
}
如果客户端发送了内容HEALTH
则回复对应消息,我这里回复了SUCCESS
但是这样有个问题,用户发送了HEALTH
这个字符串服务端会将这个消息当作健康检查进行处理,而不是消息,这样影响了用户端的使用。
还记得之前预留了一个发送类型字段sendType
吗,这时候这个类型就起作用了,如果要做健康检查的操作就将这个sendType
设置为HEALTH
,服务端根据sendType
字段进行判断业务处理,修改一下代码:
/**
* 健康检查
*/
if (WebSocketHealthEnum.HEALTH.msg.equals(userMessageModel.getSendType())){
this.sendText(WebSocketHealthEnum.HEALTH.result);
return;
}
1.2 Ping/Pong消息
- Ping的协议头是0x9,Pong的协议头是0xA
- 控制帧最大载荷为125bytes且不能拆分
服务端可以主动发生Ping/Pong消息,之前文章中写过WebSocket
发送消息的四种类型
,这里将上面发送Text
文本类型换成发送Ping
类型的消息,当然也可以发送Pong类型的消息。
代码如下:
if (WebSocketHealthEnum.HEALTH.msg.equals(userMessageModel.getSendType())){
try {
session.getBasicRemote().sendPing(ByteBuffer.wrap("SUCCESS".getBytes()));
} catch (IOException e) {
throw new RuntimeException(e);
}
return;
}
Ping消息是不会被我们的OnMessage事件接收的,所以不需要特殊处理,如果是Pong消息在服务的接收是可以的。
代码如下:
@OnMessage
public void onPong(PongMessage pongMessage) {
ByteBuffer byteBuffer = pongMessage.getApplicationData();
}
具体的业务可以二次处理
2. 服务心跳
上面的心跳是对每个客户端的心跳监测,服务的心跳也要做,服务的心跳就简单了,前端定时请求HTTP/HTTPS
协议接口。
代码如下:
@Slf4j
@RestController
public class CheckHealthController {
@GetMapping("/health")
public ResponeApi health() {
log.info("健康检查chatroom-IM --> 检查成功!");
return ResponeApi.success(ResponeCodeEnum.SUCCESS,"SUCCESS");
}
}
效果如下: