Files
JChargePointProtocol/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ProtocolSession.java
三丙 b3270c21b0 fix: 修复设备断开后通讯层会话缓存未及时清理的问题
之前设备断开连接时(channelInactive),虽然会关闭会话并通知应用层,
但通讯层的 Caffeine 缓存没有立即清除,导致:
- 下行指令仍能找到"幽灵会话",写入已关闭的 channel 后静默失败
- 应用层无法感知设备已离线,用户体验不佳

改动:
- ProtocolSession 新增 closeCallback 回调机制,close() 时自动通知注册中心
- ProtocolSession 新增 AtomicBoolean closed 状态,防止重复关闭
- DefaultProtocolSessionRegistryProvider.register() 时自动设置回调
- 新增单元测试验证回调机制

现在设备断开后,App 层下发指令时能立即感知到会话不存在,
并在日志中明确提示"充电桩会话不存在"。
2026-01-29 20:38:48 +08:00

161 lines
5.3 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.protocol.domain;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import sanbing.jcpp.proto.gen.DownlinkProto.DownlinkRequestMessage;
import sanbing.jcpp.proto.gen.UplinkProto.SessionCloseEventProto;
import sanbing.jcpp.proto.gen.UplinkProto.SessionCloseReason;
import sanbing.jcpp.proto.gen.UplinkProto.UplinkQueueMessage;
import sanbing.jcpp.protocol.forwarder.Forwarder;
import java.io.Closeable;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* @author 九筒
*/
@Getter
@Slf4j
public abstract class ProtocolSession implements Closeable {
private static final int REQUEST_CACHE_LIMIT = 1000;
protected final String protocolName;
protected final UUID id;
@Setter
protected LocalDateTime lastActivityTime;
protected final Set<String> pileCodeSet;
private final Map<String, ScheduledFuture<?>> scheduledFutures = new ConcurrentHashMap<>();
private final Cache<String, Object> requestCache = Caffeine.newBuilder()
.initialCapacity(REQUEST_CACHE_LIMIT)
.maximumSize(REQUEST_CACHE_LIMIT)
.expireAfterAccess(Duration.ofMinutes(1))
.build();
@Setter
private Forwarder forwarder;
/**
* 会话关闭回调,用于通知注册中心清除缓存
*/
@Setter
private Consumer<UUID> closeCallback;
/**
* 防止重复关闭
*/
private final AtomicBoolean closed = new AtomicBoolean(false);
protected ProtocolSession(String protocolName) {
this.protocolName = protocolName;
this.pileCodeSet = new LinkedHashSet<>();
this.id = UUID.randomUUID();
this.lastActivityTime = LocalDateTime.now();
}
public abstract void onDownlink(DownlinkRequestMessage downlinkMsg);
@Override
public void close() {
close(SessionCloseReason.SESSION_CLOSE_DESTRUCTION);
}
public void close(SessionCloseReason reason) {
// 防止重复关闭
if (!closed.compareAndSet(false, true)) {
log.debug("[{}] Protocol会话已关闭忽略重复关闭请求", this);
return;
}
log.info("[{}] Protocol会话关闭原因: {}", this, reason);
// 1. 取消所有定时任务
scheduledFutures.values().forEach(scheduledFuture -> scheduledFuture.cancel(true));
scheduledFutures.clear();
// 2. 通知注册中心清除缓存
if (closeCallback != null) {
try {
closeCallback.accept(id);
log.debug("[{}] 会话关闭回调执行成功", this);
} catch (Exception e) {
log.error("[{}] 会话关闭回调执行失败", this, e);
}
}
// 3. 转发会话关闭事件到后端
if (forwarder != null && !pileCodeSet.isEmpty()) {
for (String pileCode : pileCodeSet) {
SessionCloseEventProto sessionCloseEvent = SessionCloseEventProto.newBuilder()
.setPileCode(pileCode)
.setReason(reason)
.setAdditionalInfo("Session closed: " + reason)
.build();
UplinkQueueMessage uplinkQueueMessage = UplinkQueueMessage.newBuilder()
.setMessageIdMSB(UUID.randomUUID().getMostSignificantBits())
.setMessageIdLSB(UUID.randomUUID().getLeastSignificantBits())
.setSessionIdMSB(id.getMostSignificantBits())
.setSessionIdLSB(id.getLeastSignificantBits())
.setMessageKey(pileCode + "_session_close")
.setProtocolName(protocolName)
.setSessionCloseEventProto(sessionCloseEvent)
.build();
try {
forwarder.sendMessage(uplinkQueueMessage);
log.debug("[{}] 会话关闭事件已转发,桩编码: {}, 原因: {}", this, pileCode, reason);
} catch (Exception e) {
log.error("[{}] 转发会话关闭事件失败,桩编码: {}", this, pileCode, e);
}
}
}
}
/**
* 检查会话是否已关闭
*/
public boolean isClosed() {
return closed.get();
}
@Override
public String toString() {
return "[" + id + "]" + pileCodeSet;
}
public void addPileCode(String pileCode) {
this.pileCodeSet.add(pileCode);
}
public void addSchedule(String name, Function<String, ScheduledFuture<?>> scheduledFutureFunction) {
scheduledFutures.computeIfAbsent(name, scheduledFutureFunction);
}
}