云快充1.5.0 初始化

This commit is contained in:
3god
2024-10-08 09:38:54 +08:00
parent dea6774942
commit cb19b45919
297 changed files with 18020 additions and 28 deletions

50
jcpp-protocol-api/pom.xml Normal file
View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
抖音关注:程序员三丙
知识星球https://t.zsxq.com/j9b21
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>sanbing</groupId>
<artifactId>jcpp-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jcpp-protocol-api</artifactId>
<packaging>jar</packaging>
<name>JChargePointProtocol Protocol Api Module</name>
<description>协议API</description>
<properties>
<main.dir>${basedir}/..</main.dir>
</properties>
<dependencies>
<dependency>
<groupId>sanbing</groupId>
<artifactId>jcpp-infrastructure-util</artifactId>
</dependency>
<dependency>
<groupId>sanbing</groupId>
<artifactId>jcpp-infrastructure-queue</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-reactor-netty</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,125 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import sanbing.jcpp.protocol.cfg.ForwarderCfg;
import sanbing.jcpp.protocol.cfg.ProtocolCfg;
import sanbing.jcpp.protocol.cfg.TcpCfg;
import sanbing.jcpp.protocol.cfg.enums.ForwarderType;
import sanbing.jcpp.protocol.forwarder.Forwarder;
import sanbing.jcpp.protocol.forwarder.KafkaForwarder;
import sanbing.jcpp.protocol.forwarder.MemoryForwarder;
import sanbing.jcpp.protocol.listener.Listener;
import sanbing.jcpp.protocol.listener.tcp.TcpListener;
import static org.springframework.boot.actuate.health.Status.UP;
/**
* @author baigod
*/
@Slf4j
public abstract class ProtocolBootstrap implements HealthIndicator {
@Resource
protected ProtocolContext protocolContext;
protected ProtocolCfg protocolCfg;
protected Listener listener;
protected Forwarder forwarder;
@PostConstruct
public void init() throws InterruptedException {
String protocolName = getProtocolName();
log.info("Protocol Service [{}] Initializing...", protocolName);
protocolCfg = protocolContext.getProtocolsConfigProvider().loadConfig(protocolName);
ForwarderCfg forwarderCfg = protocolCfg.getForwarder();
if (protocolContext.getServiceInfoProvider().isMonolith() && forwarderCfg.getType() == ForwarderType.memory) {
forwarder = new MemoryForwarder(getProtocolName(), forwarderCfg,
protocolContext.getStatsFactory(),
protocolContext.getAppQueueFactory(),
protocolContext.getPartitionProvider(),
protocolContext.getServiceInfoProvider());
} else if (forwarderCfg.getType() == ForwarderType.kafka) {
forwarder = new KafkaForwarder(getProtocolName(), forwarderCfg,
protocolContext.getStatsFactory(),
protocolContext.getAppQueueFactory(),
protocolContext.getPartitionProvider(),
protocolContext.getServiceInfoProvider());
} else {
throw new IllegalArgumentException("Unknown Forwarder type: " + forwarderCfg.getType());
}
TcpCfg tcpCfg = protocolCfg.getListener().getTcp();
if (tcpCfg != null) {
listener = new TcpListener<>(protocolName, tcpCfg, messageProcessor(), protocolContext.getStatsFactory());
}
_init();
}
@PreDestroy
public void destroy() throws InterruptedException {
log.info("{} destroy...", getProtocolName());
if (listener != null) {
listener.destroy();
}
if (forwarder != null) {
forwarder.destroy();
}
_destroy();
}
@Override
public Health health() {
Health.Builder healthBuilder;
if (listener != null && listener.health().getStatus() == UP && forwarder != null && forwarder.health().getStatus() == UP) {
healthBuilder = Health.up();
} else {
healthBuilder = Health.down();
}
if (listener != null) {
healthBuilder.withDetail("listener", listener.health().getStatus());
}
if (forwarder != null) {
healthBuilder.withDetail("forwarder", forwarder.health().getStatus());
}
return healthBuilder.build();
}
protected abstract String getProtocolName();
protected abstract void _init();
protected abstract void _destroy();
protected abstract ProtocolMessageProcessor messageProcessor();
}

View File

@@ -0,0 +1,64 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol;
import io.netty.util.ResourceLeakDetector;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider;
import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider;
import sanbing.jcpp.infrastructure.queue.provider.AppQueueFactory;
import sanbing.jcpp.infrastructure.stats.StatsFactory;
import sanbing.jcpp.infrastructure.util.config.ShardingThreadPool;
import sanbing.jcpp.protocol.provider.ProtocolSessionRegistryProvider;
import sanbing.jcpp.protocol.provider.ProtocolsConfigProvider;
/**
* @author baigod
*/
@Component
@Getter
@Slf4j
public class ProtocolContext {
private final StatsFactory statsFactory;
private final ProtocolsConfigProvider protocolsConfigProvider;
private final ProtocolSessionRegistryProvider protocolSessionRegistryProvider;
private final ServiceInfoProvider serviceInfoProvider;
private final PartitionProvider partitionProvider;
private final AppQueueFactory appQueueFactory;
private final ShardingThreadPool shardingThreadPool;
public ProtocolContext(StatsFactory statsFactory,
ProtocolsConfigProvider protocolsConfigProvider,
ProtocolSessionRegistryProvider protocolSessionRegistryProvider,
ServiceInfoProvider serviceInfoProvider,
@Autowired(required = false) PartitionProvider partitionProvider,
@Autowired(required = false) AppQueueFactory appQueueFactory,
ShardingThreadPool shardingThreadPool) {
this.statsFactory = statsFactory;
this.protocolsConfigProvider = protocolsConfigProvider;
this.protocolSessionRegistryProvider = protocolSessionRegistryProvider;
this.serviceInfoProvider = serviceInfoProvider;
this.partitionProvider = partitionProvider;
this.appQueueFactory = appQueueFactory;
this.shardingThreadPool = shardingThreadPool;
}
@PostConstruct
public void init() {
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED);
log.info("Setting resource leak detector level to {}", ResourceLeakDetector.Level.DISABLED);
}
}

View File

@@ -0,0 +1,66 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol;
import lombok.extern.slf4j.Slf4j;
import sanbing.jcpp.infrastructure.stats.MessagesStats;
import sanbing.jcpp.infrastructure.util.exception.DownlinkException;
import sanbing.jcpp.infrastructure.util.trace.TracerRunnable;
import sanbing.jcpp.protocol.domain.ListenerToHandlerMsg;
import sanbing.jcpp.protocol.domain.SessionToHandlerMsg;
import sanbing.jcpp.protocol.forwarder.Forwarder;
import java.util.UUID;
/**
* @author baigod
*/
@Slf4j
public abstract class ProtocolMessageProcessor {
protected final Forwarder forwarder;
protected final ProtocolContext protocolContext;
protected ProtocolMessageProcessor(Forwarder forwarder, ProtocolContext protocolContext) {
this.forwarder = forwarder;
this.protocolContext = protocolContext;
}
public void uplinkHandleAsync(ListenerToHandlerMsg listenerToHandlerMsg, MessagesStats uplinkMsgStats) {
UUID id = listenerToHandlerMsg.session().getId();
protocolContext.getShardingThreadPool().execute(id, new TracerRunnable(() -> {
try {
listenerToHandlerMsg.session().setForwarder(forwarder);
uplinkHandle(listenerToHandlerMsg);
} catch (Exception e) {
uplinkMsgStats.incrementFailed();
log.error("{} 消息处理器处理报文异常", listenerToHandlerMsg.session(), e);
}
}));
}
protected abstract void uplinkHandle(ListenerToHandlerMsg listenerToHandlerMsg) throws Exception;
public void downlinkHandle(SessionToHandlerMsg sessionToHandlerMsg, MessagesStats downlinkMsgStats) throws DownlinkException {
try {
downlinkHandle(sessionToHandlerMsg);
} catch (Exception e) {
downlinkMsgStats.incrementFailed();
throw new DownlinkException(e.getMessage(), e);
}
}
protected abstract void downlinkHandle(SessionToHandlerMsg sessionToHandlerMsg) throws Exception;
}

View File

@@ -0,0 +1,78 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.adapter;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage;
import sanbing.jcpp.protocol.domain.ProtocolSession;
import sanbing.jcpp.protocol.provider.ProtocolSessionRegistryProvider;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* @author baigod
*/
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class DownlinkController {
@Value("${api.timeout.onDownlink:3000}")
public long onDownlinkTimeout;
@Resource
ProtocolSessionRegistryProvider protocolSessionRegistryProvider;
@PostMapping(value = "/onDownlink", consumes = "application/x-protobuf", produces = "application/x-protobuf")
public DeferredResult<ResponseEntity<String>> onDownlink(@RequestBody DownlinkRestMessage downlinkMsg) {
log.info("收到REST下行请求 {}", downlinkMsg);
final DeferredResult<ResponseEntity<String>> response = new DeferredResult<>(onDownlinkTimeout,
ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).build());
UUID protocolSessionId = new UUID(downlinkMsg.getSessionIdMSB(),downlinkMsg.getSessionIdLSB()) ;
CompletableFuture<ProtocolSession> protocolSessionCompletableFuture = protocolSessionRegistryProvider.get(protocolSessionId);
protocolSessionCompletableFuture.thenAccept(session -> {
if (session != null) {
session.onDownlink(downlinkMsg);
response.setResult(ResponseEntity.status(HttpStatus.OK).build());
} else {
log.warn("下发报文时Session未找到 sessionId: {}", protocolSessionId);
response.setResult(ResponseEntity.status(HttpStatus.NOT_FOUND).body("Protocol Session not found for ID:" + protocolSessionId));
}
}).whenComplete((unused, throwable) -> {
if (throwable != null) {
log.warn("下发报文时处理失败 sessionId: {}", protocolSessionId, throwable);
if (!response.isSetOrExpired()) {
response.setResult(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(throwable.getMessage()));
}
}
});
return response;
}
}

View File

@@ -0,0 +1,41 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.adapter.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import sanbing.jcpp.infrastructure.util.mdc.MDCUtils;
import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil;
import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.*;
@Component
public class TracerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tracerId = request.getHeader(JCPP_TRACER_ID);
String tracerOrigin = request.getHeader(JCPP_TRACER_ORIGIN);
String tracerTsStr = request.getHeader(JCPP_TRACER_TS);
long tracerTs;
if (tracerTsStr != null) {
try {
tracerTs = Long.parseLong(tracerTsStr);
} catch (NumberFormatException e) {
tracerTs = System.currentTimeMillis();
}
} else {
tracerTs = System.currentTimeMillis();
}
TracerContextUtil.newTracer(tracerId, tracerOrigin, tracerTs);
MDCUtils.recordTracer();
return true;
}
}

View File

@@ -0,0 +1,24 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.adapter.config;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
@Component
public class UndertowServletWebServerCustomizer implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
@Override
public void customize(UndertowServletWebServerFactory factory) {
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 128 * 1024 * 1024));
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
});
}
}

View File

@@ -0,0 +1,45 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.adapter.config;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Resource
private TracerInterceptor tracerInterceptor;
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
}
if (converter instanceof MappingJackson2HttpMessageConverter) {
((MappingJackson2HttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
}
}
// protobuf 序列化
converters.add( new ProtobufHttpMessageConverter());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tracerInterceptor).addPathPatterns("/**");
}
}

View File

@@ -0,0 +1,24 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.cfg;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import sanbing.jcpp.protocol.cfg.enums.ForwarderType;
@Setter
@Getter
public class ForwarderCfg {
@NotNull
private ForwarderType type;
private MemoryCfg memory;
@Valid
private KafkaCfg kafka;
}

View File

@@ -0,0 +1,49 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.cfg;
import lombok.Getter;
import lombok.Setter;
import sanbing.jcpp.infrastructure.util.property.PropertyUtils;
import java.util.Map;
@Getter
@Setter
public class KafkaCfg {
private String topic;
private boolean jcppPartition;
private String bootstrapServers;
private String acks;
private EncoderType encoder;
private int retries;
private String compressionType; // none, gzip, snappy, lz4, zstd
private int batchSize;
private int lingerMs;
private long bufferMemory;
private Map<String, String> otherProperties; // Other inline properties if necessary
private String topicProperties;
public void setOtherProperties(String otherProperties) {
this.otherProperties = PropertyUtils.getProps(otherProperties);
}
public enum EncoderType {
protobuf,
json
}
}

View File

@@ -0,0 +1,17 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.cfg;
import jakarta.validation.Valid;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ListenerCfg {
@Valid
private TcpCfg tcp;
}

View File

@@ -0,0 +1,18 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.cfg;
import lombok.Getter;
import lombok.Setter;
/**
* @author baigod
*/
@Getter
@Setter
public class MemoryCfg {
private String topic;
}

View File

@@ -0,0 +1,25 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.cfg;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ProtocolCfg {
private boolean enabled;
@NotNull
@Valid
private ListenerCfg listener;
@NotNull
@Valid
private ForwarderCfg forwarder;
}

View File

@@ -0,0 +1,45 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.cfg;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class TcpCfg {
private String bindAddress;
@Max(65000)
private int bindPort;
@Min(1)
private int bossGroupThreadCount;
@Min(1)
private int workerGroupThreadCount;
private boolean soKeepAlive;
@Min(1)
@Max(65500)
private int soBacklog;
@Min(1)
private int soRcvbuf;
@Min(1)
private int soSndbuf;
private boolean nodelay;
@Valid
private TcpHandlerCfg handler;
}

View File

@@ -0,0 +1,55 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.cfg;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.validation.constraints.Min;
import lombok.Getter;
import lombok.Setter;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import sanbing.jcpp.infrastructure.util.property.PropertyUtils;
import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType;
import sanbing.jcpp.protocol.listener.tcp.configs.BinaryHandlerConfiguration;
import sanbing.jcpp.protocol.listener.tcp.configs.HandlerConfiguration;
import sanbing.jcpp.protocol.listener.tcp.configs.JsonHandlerConfiguration;
import sanbing.jcpp.protocol.listener.tcp.configs.TextHandlerConfiguration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class TcpHandlerCfg {
@Getter
private TcpHandlerType type;
@Min(1)
@Setter
@Getter
private int idleTimeoutSeconds;
@Min(1)
@Setter
@Getter
private int maxConnections;
private final Map<TcpHandlerType, HandlerConfiguration> HANDLER_MAP = new ConcurrentHashMap<>();
public HandlerConfiguration getConfiguration(TcpHandlerType type) {
return HANDLER_MAP.get(type);
}
public void setConfiguration(String configuration) {
final JsonNode cfgJson = JacksonUtil.valueToTree(PropertyUtils.getProps(configuration));
type = TcpHandlerType.valueOf(cfgJson.get("type").asText());
switch (type) {
case TEXT -> HANDLER_MAP.put(type, JacksonUtil.treeToValue(cfgJson, TextHandlerConfiguration.class));
case JSON -> HANDLER_MAP.put(type, JacksonUtil.treeToValue(cfgJson, JsonHandlerConfiguration.class));
case BINARY -> HANDLER_MAP.put(type, JacksonUtil.treeToValue(cfgJson, BinaryHandlerConfiguration.class));
default -> throw new IllegalArgumentException("Unknown TCP handler type: " + type);
}
}
}

View File

@@ -0,0 +1,12 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.cfg.enums;
public enum ForwarderType {
memory, // 本地队列模式
kafka // Kafka模式 - 发送到外部
}

View File

@@ -0,0 +1,14 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.cfg.enums;
/**
* @author baigod
*/
public enum TcpHandlerType {
TEXT,
BINARY,
JSON
}

View File

@@ -0,0 +1,23 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.domain;
/**
* @author baigod
*/
public enum DownlinkCmdEnum {
LOGIN_ACK,
VERIFY_PRICING_ACK,
QUERY_PRICING_ACK,
SET_PRICING,
REMOTE_START_CHARGING,
TRANSACTION_RECORD,
}

View File

@@ -0,0 +1,11 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.domain;
import java.util.UUID;
public record ListenerToHandlerMsg(UUID id, byte[] msg, ProtocolSession session) {
}

View File

@@ -0,0 +1,87 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
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.ProtocolProto.DownlinkRestMessage;
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.function.Function;
/**
* @author baigod
*/
@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;
public ProtocolSession(String protocolName) {
this.protocolName = protocolName;
this.pileCodeSet = new LinkedHashSet<>();
this.id = UUID.randomUUID();
this.lastActivityTime = LocalDateTime.now();
}
public abstract void onDownlink(DownlinkRestMessage downlinkMsg);
public void close() {
close(SessionCloseReason.DESTRUCTION);
}
public void close(SessionCloseReason reason) {
log.info("[{}] Protocol会话关闭原因: {}", this, reason);
scheduledFutures.values().forEach(scheduledFuture -> scheduledFuture.cancel(true));
scheduledFutures.clear();
}
@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);
}
}

View File

@@ -0,0 +1,22 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.domain;
import io.netty.buffer.ByteBufUtil;
import java.net.SocketAddress;
import java.util.UUID;
public record ProtocolUplinkMsg<T>(SocketAddress address, UUID id, T data, int size) {
@Override
public String toString() {
if (data instanceof byte[]) {
return ByteBufUtil.hexDump((byte[]) data);
} else {
return data.toString();
}
}
}

View File

@@ -0,0 +1,14 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.domain;
/**
* @author baigod
*/
public enum SessionCloseReason {
DESTRUCTION,
INACTIVE,
MANUALLY
}

View File

@@ -0,0 +1,13 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.domain;
import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage;
/**
* @author baigod
*/
public record SessionToHandlerMsg(DownlinkRestMessage downlinkMsg, ProtocolSession session) {
}

View File

@@ -0,0 +1,110 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.forwarder;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import sanbing.jcpp.infrastructure.queue.*;
import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo;
import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider;
import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider;
import sanbing.jcpp.infrastructure.queue.discovery.ServiceType;
import sanbing.jcpp.infrastructure.stats.MessagesStats;
import sanbing.jcpp.infrastructure.stats.StatsFactory;
import sanbing.jcpp.infrastructure.util.codec.ByteUtil;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import sanbing.jcpp.infrastructure.util.mdc.MDCUtils;
import sanbing.jcpp.infrastructure.util.trace.Tracer;
import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil;
import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_PREFIX;
import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_TS;
import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ID;
import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ORIGIN;
/**
* @author baigod
*/
@Slf4j
public abstract class Forwarder {
protected static final String ERROR = "error";
AtomicBoolean healthy = new AtomicBoolean(true);
@Getter
private final String protocolName;
protected MessagesStats forwarderMessagesStats;
protected final PartitionProvider partitionProvider;
protected final ServiceInfoProvider serviceInfoProvider;
protected final boolean isMonolith;
protected QueueProducer<ProtoQueueMsg<UplinkQueueMessage>> producer;
public Forwarder(String protocolName, StatsFactory statsFactory, PartitionProvider partitionProvider, ServiceInfoProvider serviceInfoProvider) {
this.protocolName = protocolName;
this.partitionProvider = partitionProvider;
this.serviceInfoProvider = serviceInfoProvider;
this.forwarderMessagesStats = statsFactory.createMessagesStats("forwarderMessages", "protocol", protocolName);
this.isMonolith = serviceInfoProvider.isMonolith();
}
public abstract Health health();
public abstract void destroy();
protected void jcppForward(String topic, String key, UplinkQueueMessage msg, BiConsumer<Boolean, ObjectNode> consumer) {
QueueMsgHeaders headers = new DefaultQueueMsgHeaders();
Tracer currentTracer = TracerContextUtil.getCurrentTracer();
headers.put(MSG_MD_PREFIX + JCPP_TRACER_ID, ByteUtil.stringToBytes(currentTracer.getTraceId()));
headers.put(MSG_MD_PREFIX + JCPP_TRACER_ORIGIN, ByteUtil.stringToBytes(currentTracer.getOrigin()));
headers.put(MSG_MD_PREFIX + MSG_MD_TS, ByteUtil.longToBytes(currentTracer.getTracerTs()));
TopicPartitionInfo tpi = partitionProvider.resolve(ServiceType.APP, topic, key);
producer.send(tpi, new ProtoQueueMsg<>(key, msg, headers), new QueueCallback() {
@Override
public void onSuccess(QueueMsgMetadata metadata) {
TracerContextUtil.newTracer(currentTracer.getTraceId(), currentTracer.getOrigin(), currentTracer.getTracerTs());
MDCUtils.recordTracer();
log.trace("单体消息转发成功 key:{}", key);
if (consumer != null) {
consumer.accept(true, JacksonUtil.newObjectNode());
}
}
@Override
public void onFailure(Throwable t) {
TracerContextUtil.newTracer(currentTracer.getTraceId(), currentTracer.getOrigin(), currentTracer.getTracerTs());
MDCUtils.recordTracer();
log.warn("单体消息转发异常", t);
if (consumer != null) {
ObjectNode objectNode = JacksonUtil.newObjectNode();
objectNode.put(ERROR, t.getClass() + ": " + t.getMessage());
consumer.accept(true, objectNode);
}
}
});
}
public abstract void sendMessage(UplinkQueueMessage msg, BiConsumer<Boolean, ObjectNode> consumer);
public abstract void sendMessage(UplinkQueueMessage msg);
}

View File

@@ -0,0 +1,204 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.forwarder;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.config.SslConfigs;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.header.internals.RecordHeader;
import org.apache.kafka.common.header.internals.RecordHeaders;
import org.apache.kafka.common.serialization.ByteArraySerializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.boot.actuate.health.Health;
import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider;
import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider;
import sanbing.jcpp.infrastructure.queue.provider.AppQueueFactory;
import sanbing.jcpp.infrastructure.stats.StatsFactory;
import sanbing.jcpp.infrastructure.util.codec.ByteUtil;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import sanbing.jcpp.infrastructure.util.mdc.MDCUtils;
import sanbing.jcpp.infrastructure.util.trace.Tracer;
import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil;
import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage;
import sanbing.jcpp.protocol.cfg.ForwarderCfg;
import sanbing.jcpp.protocol.cfg.KafkaCfg;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_PREFIX;
import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_TS;
import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ID;
import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ORIGIN;
/**
* @author baigod
*/
@Slf4j
public class KafkaForwarder extends Forwarder {
AtomicBoolean healthy = new AtomicBoolean(true);
private static final String OFFSET = "offset";
private static final String PARTITION = "partition";
private static final String TOPIC = "topic";
private final KafkaCfg kafkaCfg;
protected final boolean jcppPartition;
private KafkaProducer<String, byte[]> kafkaProducer;
public KafkaForwarder(String protocolName,
ForwarderCfg forwarderCfg,
StatsFactory statsFactory,
AppQueueFactory appQueueFactory,
PartitionProvider partitionProvider,
ServiceInfoProvider serviceInfoProvider) {
super(protocolName, statsFactory, partitionProvider, serviceInfoProvider);
this.kafkaCfg = forwarderCfg.getKafka();
this.jcppPartition = kafkaCfg.isJcppPartition();
if (this.isMonolith || jcppPartition) {
this.producer = appQueueFactory.createProtocolUplinkMsgProducer(kafkaCfg.getTopic());
} else {
Properties properties = new Properties();
properties.put(ProducerConfig.CLIENT_ID_CONFIG, "kafka-forwarder-" + getProtocolName() + "-" + serviceInfoProvider.getServiceId());
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaCfg.getBootstrapServers());
properties.put(ProducerConfig.ACKS_CONFIG, kafkaCfg.getAcks());
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);
properties.put(ProducerConfig.RETRIES_CONFIG, kafkaCfg.getRetries());
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, kafkaCfg.getCompressionType());
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, kafkaCfg.getBatchSize());
properties.put(ProducerConfig.LINGER_MS_CONFIG, kafkaCfg.getLingerMs());
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, kafkaCfg.getBufferMemory());
if (this.kafkaCfg.getOtherProperties() != null) {
this.kafkaCfg.getOtherProperties().forEach((k, v) -> {
if (SslConfigs.SSL_KEYSTORE_CERTIFICATE_CHAIN_CONFIG.equals(k)
|| SslConfigs.SSL_KEYSTORE_KEY_CONFIG.equals(k)
|| SslConfigs.SSL_TRUSTSTORE_CERTIFICATES_CONFIG.equals(k)) {
v = v.replace("\\n", "\n");
}
properties.put(k, v);
});
}
this.kafkaProducer = new KafkaProducer<>(properties);
}
}
@Override
public Health health() {
if (healthy.get()) {
return Health.up().withDetail("producer", "Kafka producer is healthy").build();
} else {
return Health.down().withDetail("producer", "Kafka producer is unhealthy").build();
}
}
@Override
public void destroy() {
healthy.set(false);
if (this.kafkaProducer != null) {
try {
this.kafkaProducer.close();
} catch (Exception e) {
log.error("Failed to close producer during destroy()", e);
}
}
}
@Override
public void sendMessage(UplinkQueueMessage msg, BiConsumer<Boolean, ObjectNode> consumer) {
String topic = kafkaCfg.getTopic();
try {
String messageKey = msg.getMessageKey();
if (isMonolith || jcppPartition) {
jcppForward(topic, messageKey, msg, consumer);
} else {
kafkaForward(topic, messageKey, msg, consumer);
}
} catch (Exception e) {
log.debug("[{}] Failed to forward Kafka message: {}", getProtocolName(), msg, e);
}
}
@Override
public void sendMessage(UplinkQueueMessage msg) {
sendMessage(msg, null);
}
private void kafkaForward(String topic, String key, UplinkQueueMessage msg, BiConsumer<Boolean, ObjectNode> consumer) throws InvalidProtocolBufferException {
Headers headers = new RecordHeaders();
Tracer currentTracer = TracerContextUtil.getCurrentTracer();
headers.add(new RecordHeader(MSG_MD_PREFIX + JCPP_TRACER_ID, ByteUtil.stringToBytes(currentTracer.getTraceId())));
headers.add(new RecordHeader(MSG_MD_PREFIX + JCPP_TRACER_ORIGIN, ByteUtil.stringToBytes(currentTracer.getOrigin())));
headers.add(new RecordHeader(MSG_MD_PREFIX + MSG_MD_TS, ByteUtil.longToBytes(currentTracer.getTracerTs())));
if (kafkaCfg.getEncoder() == KafkaCfg.EncoderType.json) {
String protoJson = JsonFormat.printer().print(msg);
log.info("[{}] Kafka forwarder send json headers:{}, message:{}", getProtocolName(), headers, protoJson);
kafkaProducer.send(new ProducerRecord<>(topic, null, null, key, ByteUtil.stringToBytes(protoJson), headers),
(metadata, e) -> logAndDoConsumer(consumer, metadata, e, currentTracer));
} else {
log.info("[{}] Kafka forwarder send protobuf headers:{}, message:{}", getProtocolName(), headers, msg);
kafkaProducer.send(new ProducerRecord<>(topic, null, null, key, msg.toByteArray(), headers),
(metadata, e) -> logAndDoConsumer(consumer, metadata, e, currentTracer));
}
}
private void logAndDoConsumer(BiConsumer<Boolean, ObjectNode> consumer, RecordMetadata metadata, Exception e, Tracer currentTracer) {
TracerContextUtil.newTracer(currentTracer.getTraceId(), currentTracer.getOrigin(), currentTracer.getTracerTs());
MDCUtils.recordTracer();
log.debug("Kafka 消息转发完成, success:{}", e == null);
if (consumer != null) {
onComplete(metadata, e, consumer);
}
}
private void onComplete(RecordMetadata metadata, Exception e, BiConsumer<Boolean, ObjectNode> consumer) {
if (consumer == null) {
return;
}
ObjectNode objectNode = JacksonUtil.newObjectNode();
objectNode.put(OFFSET, String.valueOf(metadata.offset()));
objectNode.put(PARTITION, String.valueOf(metadata.partition()));
objectNode.put(TOPIC, metadata.topic());
if (e != null) {
objectNode.put(ERROR, e.getClass() + ": " + e.getMessage());
}
consumer.accept(e == null, objectNode);
}
}

View File

@@ -0,0 +1,84 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.forwarder;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider;
import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider;
import sanbing.jcpp.infrastructure.queue.provider.AppQueueFactory;
import sanbing.jcpp.infrastructure.stats.StatsFactory;
import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage;
import sanbing.jcpp.protocol.cfg.ForwarderCfg;
import sanbing.jcpp.protocol.cfg.MemoryCfg;
import java.util.function.BiConsumer;
/**
* @author baigod
*/
@Slf4j
public class MemoryForwarder extends Forwarder {
private final MemoryCfg memoryCfg;
public MemoryForwarder(String protocolName,
ForwarderCfg forwarderCfg,
StatsFactory statsFactory,
AppQueueFactory appQueueFactory,
PartitionProvider partitionProvider,
ServiceInfoProvider serviceInfoProvider) {
super(protocolName, statsFactory, partitionProvider, serviceInfoProvider);
this.memoryCfg = forwarderCfg.getMemory();
super.producer = appQueueFactory.createProtocolUplinkMsgProducer(memoryCfg.getTopic());
}
@Override
public Health health() {
if (healthy.get()) {
return Health.up().withDetail("producer", "Memory producer is healthy").build();
} else {
return Health.down().withDetail("producer", "Memory producer is unhealthy").build();
}
}
@Override
public void destroy() {
healthy.set(false);
if (this.producer != null) {
try {
this.producer.stop();
} catch (Exception e) {
log.error("Failed to close producer during destroy()", e);
}
}
}
@Override
public void sendMessage(UplinkQueueMessage msg, BiConsumer<Boolean, ObjectNode> consumer) {
String topic = memoryCfg.getTopic();
String key = msg.getMessageKey();
try {
jcppForward(topic, key, msg, consumer);
} catch (Exception e) {
log.warn("[{}] Failed to forward Memory message: {}", getProtocolName(), msg, e);
}
}
@Override
public void sendMessage(UplinkQueueMessage msg) {
sendMessage(msg, null);
}
}

View File

@@ -0,0 +1,142 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.json.JsonObjectDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType;
import sanbing.jcpp.protocol.listener.tcp.TcpChannelHandler;
import sanbing.jcpp.protocol.listener.tcp.configs.BinaryHandlerConfiguration;
import sanbing.jcpp.protocol.listener.tcp.configs.TextHandlerConfiguration;
import sanbing.jcpp.protocol.listener.tcp.decoder.JCPPHeadTailFrameDecoder;
import sanbing.jcpp.protocol.listener.tcp.decoder.JCPPLengthFieldBasedFrameDecoder;
import sanbing.jcpp.protocol.listener.tcp.decoder.TcpMsgDecoder;
import sanbing.jcpp.protocol.listener.tcp.handler.ConnectionLimitHandler;
import sanbing.jcpp.protocol.listener.tcp.handler.IdleEventHandler;
import sanbing.jcpp.protocol.listener.tcp.handler.TracerHandler;
import java.nio.ByteOrder;
import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.BINARY;
import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.TEXT;
import static sanbing.jcpp.protocol.listener.tcp.configs.BinaryHandlerConfiguration.LITTLE_ENDIAN_BYTE_ORDER;
import static sanbing.jcpp.protocol.listener.tcp.configs.TextHandlerConfiguration.SYSTEM_LINE_SEPARATOR;
/**
* @author baigod
*/
@Slf4j
@RequiredArgsConstructor
public abstract class ChannelHandlerInitializer<C extends Channel> extends ChannelInitializer<C> {
protected final ChannelGroup CHANNEL_GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected abstract void initChannel(C ch);
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
}
public static ChannelHandlerInitializer<SocketChannel> createTcpChannelHandler(ChannelHandlerParameter parameter) {
TcpHandlerType type = parameter.handlerCfg().getType();
return switch (type) {
case TEXT -> new ChannelHandlerInitializer<>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
TextHandlerConfiguration textHandlerConfig = (TextHandlerConfiguration) parameter.handlerCfg().getConfiguration(TEXT);
ByteBuf[] delimiters = SYSTEM_LINE_SEPARATOR.equals(textHandlerConfig.getMessageSeparator())
? Delimiters.lineDelimiter() : Delimiters.nulDelimiter();
DelimiterBasedFrameDecoder framer = new DelimiterBasedFrameDecoder(textHandlerConfig.getMaxFrameLength(),
textHandlerConfig.isStripDelimiter(), delimiters);
socketChannel.pipeline()
.addLast("tracerHandler", new TracerHandler())
.addLast("connectionLimitHandler", new ConnectionLimitHandler(parameter.protocolName(), parameter.handlerCfg().getMaxConnections(), CHANNEL_GROUP, parameter.connectionsGauge()))
.addLast("idleStateHandler", new IdleStateHandler(parameter.handlerCfg().getIdleTimeoutSeconds(), 0, 0))
.addLast("idleEventHandler", new IdleEventHandler(parameter.protocolName()))
.addLast("framer", framer)
.addLast("tcpTextDecoder", new TcpMsgDecoder<>(parameter.protocolName(), msg -> TcpMsgDecoder.toString(msg, textHandlerConfig.getCharsetName())))
.addLast("tcpStringInHandler", new TcpChannelHandler<>(parameter));
}
};
case JSON -> new ChannelHandlerInitializer<>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
socketChannel.pipeline()
.addLast("tracerHandler", new TracerHandler())
.addLast("connectionLimitHandler", new ConnectionLimitHandler(parameter.protocolName(), parameter.handlerCfg().getMaxConnections(), CHANNEL_GROUP, parameter.connectionsGauge()))
.addLast("idleStateHandler",
new IdleStateHandler(parameter.handlerCfg().getIdleTimeoutSeconds(), 0, 0))
.addLast("idleEventHandler", new IdleEventHandler(parameter.protocolName()))
.addLast("datagramToJsonDecoder", new JsonObjectDecoder())
.addLast("tcpJsonDecoder", new TcpMsgDecoder<>(parameter.protocolName(), TcpMsgDecoder::toJson))
.addLast("tcpJsonInHandler", new TcpChannelHandler<>(parameter));
}
};
case BINARY -> new ChannelHandlerInitializer<>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
BinaryHandlerConfiguration binaryHandlerConfig = (BinaryHandlerConfiguration) parameter.handlerCfg().getConfiguration(BINARY);
ByteOrder byteOrder = LITTLE_ENDIAN_BYTE_ORDER.equalsIgnoreCase(binaryHandlerConfig.getByteOrder())
? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;
socketChannel.pipeline()
.addLast("tracerHandler", new TracerHandler())
.addLast("connectionLimitHandler", new ConnectionLimitHandler(parameter.protocolName(), parameter.handlerCfg().getMaxConnections(), CHANNEL_GROUP, parameter.connectionsGauge()))
.addLast("idleStateHandler", new IdleStateHandler(parameter.handlerCfg().getIdleTimeoutSeconds(), 0, 0))
.addLast("idleEventHandler", new IdleEventHandler(parameter.protocolName()));
if (LengthFieldBasedFrameDecoder.class.isAssignableFrom(binaryHandlerConfig.getDecoder())) {
LengthFieldBasedFrameDecoder framer = new LengthFieldBasedFrameDecoder(byteOrder,
binaryHandlerConfig.getMaxFrameLength(), binaryHandlerConfig.getLengthFieldOffset(),
binaryHandlerConfig.getLengthFieldLength(), binaryHandlerConfig.getLengthAdjustment(),
binaryHandlerConfig.getInitialBytesToStrip(), binaryHandlerConfig.isFailFast());
socketChannel.pipeline().addLast("LengthFieldBasedFrameDecoder", framer);
} else if (JCPPLengthFieldBasedFrameDecoder.class.isAssignableFrom(binaryHandlerConfig.getDecoder())) {
JCPPLengthFieldBasedFrameDecoder framer = new JCPPLengthFieldBasedFrameDecoder(binaryHandlerConfig.getHead(), byteOrder,
binaryHandlerConfig.getLengthFieldOffset(), binaryHandlerConfig.getLengthFieldLength(),
binaryHandlerConfig.getLengthAdjustment(), binaryHandlerConfig.getInitialBytesToStrip());
socketChannel.pipeline().addLast("JCPPLengthFieldBasedFrameDecoder", framer);
} else if (JCPPHeadTailFrameDecoder.class.isAssignableFrom(binaryHandlerConfig.getDecoder())) {
JCPPHeadTailFrameDecoder framer = new JCPPHeadTailFrameDecoder(binaryHandlerConfig.getHead(),
binaryHandlerConfig.getTail());
socketChannel.pipeline().addLast("JCPPHeadTailFrameDecoder", framer);
} else {
throw new IllegalArgumentException("Unknown binary decoder");
}
socketChannel.pipeline()
.addLast("tcpByteDecoderOverride", new TcpMsgDecoder<>(parameter.protocolName(), TcpMsgDecoder::toByteArray))
.addLast("tcpByteHandler", new TcpChannelHandler<>(parameter));
}
};
case null -> throw new IllegalArgumentException("Unknown: " + parameter.handlerCfg());
};
}
}

View File

@@ -0,0 +1,24 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener;
import io.micrometer.core.instrument.Timer;
import sanbing.jcpp.infrastructure.stats.DefaultCounter;
import sanbing.jcpp.infrastructure.stats.MessagesStats;
import sanbing.jcpp.protocol.ProtocolMessageProcessor;
import sanbing.jcpp.protocol.cfg.TcpHandlerCfg;
import java.util.concurrent.atomic.AtomicInteger;
public record ChannelHandlerParameter(String protocolName,
TcpHandlerCfg handlerCfg,
ProtocolMessageProcessor protocolMessageProcessor,
AtomicInteger connectionsGauge,
MessagesStats uplinkMsgStats,
MessagesStats downlinkMsgStats,
DefaultCounter uplinkTrafficCounter,
DefaultCounter downlinkTrafficCounter,
Timer downlinkTimer) {
}

View File

@@ -0,0 +1,45 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener;
import io.micrometer.core.instrument.Timer;
import lombok.Getter;
import org.springframework.boot.actuate.health.Health;
import sanbing.jcpp.infrastructure.stats.DefaultCounter;
import sanbing.jcpp.infrastructure.stats.MessagesStats;
import sanbing.jcpp.infrastructure.stats.StatsFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author baigod
*/
public abstract class Listener {
@Getter
private final String protocolName;
protected AtomicInteger connectionsGauge = new AtomicInteger();
protected MessagesStats uplinkMsgStats;
protected MessagesStats downlinkMsgStats;
protected DefaultCounter uplinkTrafficCounter;
protected DefaultCounter downlinkTrafficCounter;
protected Timer downlinkTimer;
public Listener(String protocolName, StatsFactory statsFactory) {
this.protocolName = protocolName;
statsFactory.createGauge("openConnections", connectionsGauge, "protocol", protocolName);
this.uplinkMsgStats = statsFactory.createMessagesStats("listenerUplinkMessage", "protocol", protocolName);
this.downlinkMsgStats = statsFactory.createMessagesStats("listenerDownlinkMessage", "protocol", protocolName);
this.uplinkTrafficCounter = statsFactory.createDefaultCounter("listenerUplinkTraffic", "protocol", protocolName);
this.downlinkTrafficCounter = statsFactory.createDefaultCounter("listenerDownlinkTraffic", "protocol", protocolName);
this.downlinkTimer = statsFactory.createTimer("listenerDownlink", "protocol", protocolName);
}
public abstract Health health();
public abstract void destroy() throws InterruptedException;
}

View File

@@ -0,0 +1,238 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp;
import com.fasterxml.jackson.databind.JsonNode;
import io.micrometer.core.instrument.Timer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.concurrent.Future;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import sanbing.jcpp.infrastructure.stats.DefaultCounter;
import sanbing.jcpp.infrastructure.stats.MessagesStats;
import sanbing.jcpp.infrastructure.util.exception.DownlinkException;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil;
import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage;
import sanbing.jcpp.protocol.ProtocolMessageProcessor;
import sanbing.jcpp.protocol.domain.ListenerToHandlerMsg;
import sanbing.jcpp.protocol.domain.ProtocolUplinkMsg;
import sanbing.jcpp.protocol.domain.SessionCloseReason;
import sanbing.jcpp.protocol.domain.SessionToHandlerMsg;
import sanbing.jcpp.protocol.listener.ChannelHandlerParameter;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.function.Supplier;
@Slf4j
public class TcpChannelHandler<T> extends SimpleChannelInboundHandler<ProtocolUplinkMsg<T>> {
private final String protocolName;
private final ProtocolMessageProcessor protocolMessageProcessor;
private final MessagesStats uplinkMsgStats;
private final DefaultCounter uplinkTrafficCounter;
private final MessagesStats downlinkMsgStats;
private final DefaultCounter downlinkTrafficCounter;
private final Timer downlinkTimer;
private final TcpSession tcpSession;
@SneakyThrows
public TcpChannelHandler(ChannelHandlerParameter parameter) {
this.protocolName = parameter.protocolName();
this.protocolMessageProcessor = parameter.protocolMessageProcessor();
this.uplinkMsgStats = parameter.uplinkMsgStats();
this.uplinkTrafficCounter = parameter.uplinkTrafficCounter();
this.downlinkMsgStats = parameter.downlinkMsgStats();
this.downlinkTrafficCounter = parameter.downlinkTrafficCounter();
this.downlinkTimer = parameter.downlinkTimer();
tcpSession = new TcpSession(protocolName, this::onDownlink, this::writeAndFlush);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ProtocolUplinkMsg<T> msg) {
if (log.isDebugEnabled()) {
log.debug("[{}]{}{} Netty拆出到上行报文:{}", protocolName, ctx.channel(), tcpSession, msg);
}
uplinkMsgStats.incrementTotal();
uplinkTrafficCounter.add(msg.size());
tcpSession.setLastActivityTime(LocalDateTime.now());
if (tcpSession.getAddress() == null) {
tcpSession.setAddress(msg.address());
}
if (tcpSession.getCtx() == null) {
tcpSession.setCtx(ctx);
}
T data = msg.data();
if (Objects.isNull(data)) {
log.debug("[{}]{}{} 上行报文为空被过滤 [{}]", protocolName, ctx.channel(), tcpSession, msg);
return;
}
try {
process(msg, ctx);
uplinkMsgStats.incrementSuccessful();
} catch (Exception e) {
uplinkMsgStats.incrementFailed();
log.error("[{}]{}{} TCP管道处理报文异常", protocolName, ctx.channel(), tcpSession, e);
}
}
private void process(ProtocolUplinkMsg<T> msg, ChannelHandlerContext ctx) {
switch (msg.data()) {
case byte[] bytes ->
protocolMessageProcessor.uplinkHandleAsync(new ListenerToHandlerMsg(msg.id(), bytes, tcpSession), uplinkMsgStats);
case JsonNode json ->
protocolMessageProcessor.uplinkHandleAsync(new ListenerToHandlerMsg(msg.id(), JacksonUtil.writeValueAsBytes(json), tcpSession), uplinkMsgStats);
case String text ->
protocolMessageProcessor.uplinkHandleAsync(new ListenerToHandlerMsg(msg.id(), JacksonUtil.writeValueAsBytes(text.getBytes()), tcpSession), uplinkMsgStats);
case null, default -> {
assert msg.data() != null;
log.warn("[{}]{}{} 不支持的TCP上行报文类型:{}", protocolName, ctx.channel(), tcpSession, msg.data().getClass());
}
}
}
protected void onDownlink(ChannelHandlerContext ctx, DownlinkRestMessage downlinkMsg) throws DownlinkException {
protocolMessageProcessor.downlinkHandle(new SessionToHandlerMsg(downlinkMsg, tcpSession), downlinkMsgStats);
}
protected void writeAndFlush(ChannelHandlerContext ctx, ByteBuf... byteBufList) {
if (byteBufList == null || byteBufList.length == 0) {
return;
}
if (ctx.isRemoved()) {
tcpSession.close(SessionCloseReason.INACTIVE);
log.warn("[{}]{}{} TCP会话已失效因此删除会话", protocolName, ctx.channel(), tcpSession);
return;
}
downlinkMsgStats.incrementTotal(byteBufList.length);
for (ByteBuf byteBuf : byteBufList) {
try {
if (Objects.isNull(byteBuf)) {
log.warn("[{}]{}{} 下发空报文被拦截", protocolName, ctx.channel(), tcpSession);
continue;
}
logDownlinkStart(ctx, byteBuf.readableBytes(), () -> ByteBufUtil.hexDump(byteBuf));
ctx.writeAndFlush(Unpooled.wrappedBuffer(byteBuf))
.addListener(channelFuture -> logDownlinkUnsuccessful(ctx, channelFuture));
downlinkMsgStats.incrementSuccessful();
} catch (Exception e) {
downlinkMsgStats.incrementFailed();
throw e;
}
}
}
private void logDownlinkStart(ChannelHandlerContext ctx, int payloadSize, Supplier<String> logTransform) {
downlinkTrafficCounter.add(payloadSize);
if (log.isDebugEnabled()) {
log.debug("[{}]{}{} 开始发送下行报文:{}", protocolName, ctx.channel(), tcpSession, logTransform.get());
}
}
private void logDownlinkUnsuccessful(ChannelHandlerContext ctx, Future<? super Void> channelFuture) {
downlinkTimer.record(Duration.ofMillis(System.currentTimeMillis() - TracerContextUtil.getCurrentTracer().getTracerTs()));
if (channelFuture.isDone() && !channelFuture.isSuccess()) {
log.info("[{}]{}{} 下行报文发送未成功", protocolName, ctx.channel(), tcpSession);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
if (log.isTraceEnabled()) {
log.trace("[{}]{}{} Channel Read Complete [{}]", protocolName, ctx.channel(), tcpSession, ctx.name());
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("[{}]{}{} Invalid message received, Exception caught", protocolName, ctx.channel(), tcpSession, cause);
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
log.info("[{}]{} 打开通道", protocolName, ctx.channel());
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
log.info("[{}]{}{} 关闭通道", protocolName, ctx.channel(), tcpSession);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
log.info("[{}]{} 通道活跃", protocolName, ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
log.info("[{}]{}{} 通道不活跃", protocolName, ctx.channel(), tcpSession);
}
}

View File

@@ -0,0 +1,114 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.concurrent.Future;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import sanbing.jcpp.infrastructure.stats.StatsFactory;
import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory;
import sanbing.jcpp.protocol.ProtocolMessageProcessor;
import sanbing.jcpp.protocol.cfg.TcpCfg;
import sanbing.jcpp.protocol.listener.ChannelHandlerInitializer;
import sanbing.jcpp.protocol.listener.ChannelHandlerParameter;
import sanbing.jcpp.protocol.listener.Listener;
/**
* @author baigod
*/
@Slf4j
public class TcpListener<T> extends Listener {
private Channel serverChannel;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private final ChannelHandlerParameter parameter;
public TcpListener(String protocolName, TcpCfg tcpCfg, ProtocolMessageProcessor protocolMessageProcessor, StatsFactory statsFactory) throws InterruptedException {
super(protocolName, statsFactory);
parameter = new ChannelHandlerParameter(protocolName, tcpCfg.getHandler(), protocolMessageProcessor, connectionsGauge, uplinkMsgStats, downlinkMsgStats, uplinkTrafficCounter, downlinkTrafficCounter, downlinkTimer);
tcpServerBootstrap(tcpCfg, getProtocolName());
}
private void tcpServerBootstrap(TcpCfg tcpCfg, String protocolName) throws InterruptedException {
bossGroup = new NioEventLoopGroup(tcpCfg.getBossGroupThreadCount(), JCPPThreadFactory.forName("tcp-boss"));
workerGroup = new NioEventLoopGroup(tcpCfg.getWorkerGroupThreadCount(), JCPPThreadFactory.forName("tcp-worker"));
ChannelHandlerInitializer<SocketChannel> channelHandler = ChannelHandlerInitializer.createTcpChannelHandler(parameter);
ServerBootstrap server = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, tcpCfg.getSoBacklog())
.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.SO_RCVBUF, tcpCfg.getSoRcvbuf())
.childOption(ChannelOption.SO_KEEPALIVE, tcpCfg.isSoKeepAlive())
.childOption(ChannelOption.TCP_NODELAY, tcpCfg.isNodelay())
.childOption(ChannelOption.SO_SNDBUF, tcpCfg.getSoSndbuf())
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(channelHandler);
serverChannel = server.bind(tcpCfg.getBindAddress(), tcpCfg.getBindPort()).sync().channel();
log.info("Tcp server [{}] started, BindAddress:[{}], BindPort: [{}]", protocolName, tcpCfg.getBindAddress(), tcpCfg.getBindPort());
}
private void tcpServerShutdown() throws InterruptedException {
if (this.serverChannel != null) {
ChannelFuture cf = this.serverChannel.close().sync();
cf.awaitUninterruptibly();
}
Future<?> bossFuture = null;
Future<?> workerFuture = null;
if (bossGroup != null) {
bossFuture = bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerFuture = workerGroup.shutdownGracefully();
}
log.info("[{}] Awaiting shutdown gracefully boss and worker groups...", getProtocolName());
if (bossFuture != null) {
bossFuture.sync();
}
if (workerFuture != null) {
workerFuture.sync();
}
log.info("[{}] Protocol server stopped!", getProtocolName());
}
@Override
public Health health() {
if (serverChannel != null) {
if (serverChannel.isActive()) {
return Health.up().withDetail("TcpServer", "Active").build();
} else {
return Health.down().withDetail("TcpServer", "Inactive").build();
}
}
return Health.down().build();
}
@Override
public void destroy() throws InterruptedException {
tcpServerShutdown();
}
}

View File

@@ -0,0 +1,90 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage;
import sanbing.jcpp.protocol.domain.ProtocolSession;
import sanbing.jcpp.protocol.domain.SessionCloseReason;
import sanbing.jcpp.protocol.listener.tcp.enums.SequenceNumberLength;
import java.net.SocketAddress;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
/**
* 设备会话
*
* @author baigod
*/
@EqualsAndHashCode(callSuper = true)
@Getter
@Setter
public class TcpSession extends ProtocolSession {
private SocketAddress address;
private ChannelHandlerContext ctx;
private final BiConsumer<ChannelHandlerContext, DownlinkRestMessage> sendDownlinkConsumer;
private final BiConsumer<ChannelHandlerContext, ByteBuf> writeAndFlushConsumer;
private final AtomicInteger sequenceNumber = new AtomicInteger(0);
public int nextSeqNo(SequenceNumberLength sequenceNumberLength) {
synchronized (sequenceNumber) {
int result = sequenceNumber.incrementAndGet();
switch (sequenceNumberLength) {
case BYTE -> {
if (result == 0xFF) {
sequenceNumber.set(0);
}
}
case SHORT -> {
if (result == Short.MAX_VALUE) {
sequenceNumber.set(0);
}
}
default -> {
if (result == Integer.MAX_VALUE) {
sequenceNumber.set(0);
}
}
}
return result;
}
}
public TcpSession(String protocolName,
BiConsumer<ChannelHandlerContext, DownlinkRestMessage> sendDownlinkConsumer,
BiConsumer<ChannelHandlerContext, ByteBuf> writeAndFlushConsumer) {
super(protocolName);
this.sendDownlinkConsumer = sendDownlinkConsumer;
this.writeAndFlushConsumer = writeAndFlushConsumer;
}
@Override
public void onDownlink(DownlinkRestMessage downlinkMsg) {
sendDownlinkConsumer.accept(ctx, downlinkMsg);
}
@Override
public void close(SessionCloseReason reason) {
super.close(reason);
ctx.flush();
ctx.close();
}
public void writeAndFlush(ByteBuf byteBuf) {
writeAndFlushConsumer.accept(ctx, byteBuf);
}
}

View File

@@ -0,0 +1,75 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.configs;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType;
import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.BINARY;
@Data
@ToString
@EqualsAndHashCode
public class BinaryHandlerConfiguration implements HandlerConfiguration {
public static final String LITTLE_ENDIAN_BYTE_ORDER = "LITTLE_ENDIAN";
/**
* 拆包器
*/
private Class<? extends ByteToMessageDecoder> decoder;
/**
* 大小端(共用)
*/
private String byteOrder;
/**
* 起始域HEX字符串
*/
private String head;
/**
* 结束域HeadTailFrameDecoder)
*/
private String tail;
/**
* 最大帧长LengthFieldBasedFrameDecoder
*/
private int maxFrameLength;
/**
* 长度域位置(共用)
*/
private int lengthFieldOffset;
/**
* 长度域长度(共用)
*/
private int lengthFieldLength;
/**
* 长度调整(共用)
*/
private int lengthAdjustment;
/**
* 初始跳过字节数(共用)
*/
private int initialBytesToStrip;
/**
* 快速失败LengthFieldBasedFrameDecoder
*/
private boolean failFast;
public TcpHandlerType getType() {
return BINARY;
}
}

View File

@@ -0,0 +1,37 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.configs;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType;
@JsonTypeInfo(
use = Id.NAME,
property = "type"
)
@JsonSubTypes({
@Type(
value = TextHandlerConfiguration.class,
name = "TEXT"
),
@Type(
value = BinaryHandlerConfiguration.class,
name = "BINARY"
),
@Type(
value = JsonHandlerConfiguration.class,
name = "JSON"
)
})
@JsonIgnoreProperties(
ignoreUnknown = true
)
public interface HandlerConfiguration {
TcpHandlerType getType();
}

View File

@@ -0,0 +1,22 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.configs;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType;
import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.JSON;
@Data
@ToString
@EqualsAndHashCode
public class JsonHandlerConfiguration implements HandlerConfiguration {
public TcpHandlerType getType() {
return JSON;
}
}

View File

@@ -0,0 +1,32 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.configs;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType;
import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.TEXT;
@Data
@ToString
@EqualsAndHashCode
public class TextHandlerConfiguration implements HandlerConfiguration {
public static final String SYSTEM_LINE_SEPARATOR = "SYSTEM_LINE_SEPARATOR";
private int maxFrameLength;
private boolean stripDelimiter;
private String messageSeparator;
private String charsetName;
public TcpHandlerType getType() {
return TEXT;
}
}

View File

@@ -0,0 +1,145 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.decoder;
import cn.hutool.core.util.HexUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static sanbing.jcpp.protocol.listener.tcp.enums.ReadAct.BREAK;
import static sanbing.jcpp.protocol.listener.tcp.enums.ReadAct.CONTINUE;
/**
* 起始域结束域拆包
*
* @author baigod
*/
@Slf4j
public class JCPPHeadTailFrameDecoder extends ByteToMessageDecoder {
/**
* 起始域
*/
private final byte[] headBytes;
/**
* 结束域
*/
private final byte[] tailBytes;
public JCPPHeadTailFrameDecoder(String head, String tail) {
checkNotNull(head, "head");
checkNotNull(head, "tail");
this.headBytes = HexUtil.decodeHex(head);
this.tailBytes = HexUtil.decodeHex(tail);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
while (in.isReadable()) {
Object decoded = decode(ctx, in);
if (decoded == null || decoded == BREAK) {
break;
}
if (decoded == CONTINUE) {
continue;
}
out.add(decoded);
}
}
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
if (log.isTraceEnabled()) {
String hexDump = ByteBufUtil.hexDump(in);
log.trace("{} 开始解析16进制报文{}", ctx.channel(), hexDump);
}
// 剩余可读长度
int readableBytes = in.readableBytes();
// 读取到的字节长度小于起始域+结束语长度,则跳过先不处理
if (readableBytes < headBytes.length + tailBytes.length) {
log.debug("{} 可读长度过短,因此跳过,可读长度:{}", ctx.channel(), readableBytes);
return BREAK;
}
// byteBuf当前的读索引
int buffIndex = in.readerIndex();
// 查看前n个字节判断消息头
byte[] firstBytes = new byte[headBytes.length];
for (int i = 0; i < headBytes.length; i++, buffIndex++) {
firstBytes[i] = in.getByte(buffIndex);
}
// 校验起始域如果不符则丢弃1字节直到读取到正确的起始域为止
if (!Arrays.equals(firstBytes, headBytes)) {
byte aByte = in.readByte();
if (log.isDebugEnabled()) {
log.debug("{} 丢弃1字节 {}", ctx.channel(), String.format("%02X", aByte & 0xFF));
}
return CONTINUE;
}
// 记住起始字节的位置
int startIndex = in.readerIndex();
// 找到结束字节序列
int endIndex = indexOf(in, tailBytes, headBytes.length);
if (endIndex < 0) {
log.debug("{} 未找到结束域索引,因此先跳过", ctx.channel());
return BREAK;
}
// 提取报文
int length = endIndex + tailBytes.length;
ByteBuf frame = in.retainedSlice(startIndex, length);
in.readerIndex(startIndex + length);
return frame;
}
public static int indexOf(ByteBuf in, byte[] bytes, int fromIndex) {
if (bytes.length == 0) {
return 0;
}
if (in.readableBytes() == 0) {
return -1;
}
int targetCount = bytes.length;
int readerIndex = in.readerIndex() + fromIndex;
int writerIndex = in.writerIndex();
int lastIndex = in.indexOf(readerIndex, writerIndex, bytes[0]);
while (lastIndex != -1) {
if (lastIndex + targetCount <= writerIndex) {
boolean matched = true;
for (int i = 1; i < targetCount; i++) {
if (in.getByte(lastIndex + i) != bytes[i]) {
matched = false;
break;
}
}
if (matched) {
return lastIndex - in.readerIndex();
}
}
lastIndex = in.indexOf(lastIndex + 1, writerIndex, bytes[0]);
}
return -1;
}
}

View File

@@ -0,0 +1,217 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.decoder;
import cn.hutool.core.util.HexUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.extern.slf4j.Slf4j;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.List;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
import static sanbing.jcpp.protocol.listener.tcp.enums.ReadAct.BREAK;
import static sanbing.jcpp.protocol.listener.tcp.enums.ReadAct.CONTINUE;
/**
* JCPP长度域拆包
*
* @author baigod
*/
@Slf4j
public class JCPPLengthFieldBasedFrameDecoder extends ByteToMessageDecoder {
/**
* 起始域
*/
private final byte[] headBytes;
private final ByteOrder byteOrder;
private final int lengthFieldOffset;
private final int lengthFieldLength;
private final int lengthFieldEndOffset;
private final int lengthAdjustment;
private final int initialBytesToStrip;
private int frameLengthInt = -1;
public JCPPLengthFieldBasedFrameDecoder(String head, ByteOrder byteOrder, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment,
int initialBytesToStrip) {
checkNotNull(head, "head");
this.headBytes = HexUtil.decodeHex(head);
this.byteOrder = checkNotNull(byteOrder, "byteOrder");
checkPositiveOrZero(lengthFieldOffset, "lengthFieldOffset");
checkPositiveOrZero(initialBytesToStrip, "initialBytesToStrip");
this.lengthFieldOffset = lengthFieldOffset;
this.lengthFieldLength = lengthFieldLength;
this.lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
this.lengthAdjustment = lengthAdjustment;
this.initialBytesToStrip = initialBytesToStrip;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
while (in.isReadable()) {
Object decoded = decode(ctx, in);
if (decoded == null || decoded == BREAK) {
break;
}
if (decoded == CONTINUE) {
continue;
}
out.add(decoded);
}
}
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
if (log.isDebugEnabled()) {
String hexDump = ByteBufUtil.hexDump(in);
log.debug("{} 开始解析16进制报文{}", ctx.channel(), hexDump);
}
// 帧长
long frameLength = 0;
// new frame
if (frameLengthInt == -1) {
// 剩余可读长度
int readableBytes = in.readableBytes();
// 读取到的字节长度小于长度域结束位置,则跳过先不处理
if (readableBytes < lengthFieldEndOffset) {
log.debug("{} 读取到的字节长度小于长度域结束位置,则跳过先不处理 readableBytes:{}", ctx.channel(), readableBytes);
return BREAK;
}
// byteBuf当前的读索引
int buffIndex = in.readerIndex();
// 查看前n个字节判断消息头
byte[] firstBytes = new byte[headBytes.length];
for (int i = 0; i < headBytes.length; i++, buffIndex++) {
firstBytes[i] = in.getByte(buffIndex);
}
// 校验起始域如果不符则丢弃1字节直到读取到正确的起始域为止
if (!Arrays.equals(firstBytes, headBytes)) {
in.skipBytes(1);
return CONTINUE;
}
// 实际长度域位置
int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset);
// 如果帧长<0则跳过buf并抛出异常
if (frameLength < 0) {
failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset);
}
// 帧长 = 调整长度 + 长度域结束位置
frameLength += lengthAdjustment + lengthFieldEndOffset;
// 如果帧长<长度与结束位置,则跳过并抛出异常
if (frameLength < lengthFieldEndOffset) {
failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);
}
frameLengthInt = (int) frameLength;
}
// frameLengthInt exist , just check buf
if (in.readableBytes() < frameLengthInt) {
log.debug("{} 可读长度小于帧长,因此跳过 {}", ctx.channel(), frameLengthInt);
return BREAK;
}
// 初始跳过长度如果大于帧长,则跳过并抛出异常
if (initialBytesToStrip > frameLengthInt) {
failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip);
}
in.skipBytes(initialBytesToStrip);
// extract frame
int readerIndex = in.readerIndex();
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = extractFrame(in, readerIndex, actualFrameLength);
in.readerIndex(readerIndex + actualFrameLength);
frameLengthInt = -1; // start processing the next frame
return frame;
}
/**
* 获取未调整的帧长度
*/
protected long getUnadjustedFrameLength(ByteBuf buf, int offset) {
long frameLength;
switch (lengthFieldLength) {
case 1 -> frameLength = buf.getUnsignedByte(offset);
case 2 -> {
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
frameLength = buf.getUnsignedShortLE(offset);
} else {
frameLength = buf.getUnsignedShort(offset);
}
}
case 3 -> {
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
frameLength = buf.getUnsignedMediumLE(offset);
} else {
frameLength = buf.getUnsignedMedium(offset);
}
}
case 4 -> {
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
frameLength = buf.getUnsignedIntLE(offset);
} else {
frameLength = buf.getUnsignedInt(offset);
}
}
case 8 -> {
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
frameLength = buf.getLongLE(offset);
} else {
frameLength = buf.getLong(offset);
}
}
default ->
throw new DecoderException("unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)");
}
return frameLength;
}
private static void failOnNegativeLengthField(ByteBuf in, long frameLength, int lengthFieldEndOffset) {
in.skipBytes(lengthFieldEndOffset);
throw new CorruptedFrameException("negative pre-adjustment length field: " + frameLength);
}
private static void failOnFrameLengthLessThanLengthFieldEndOffset(ByteBuf in, long frameLength, int lengthFieldEndOffset) {
in.skipBytes(lengthFieldEndOffset);
throw new CorruptedFrameException(
"Adjusted frame length (" + frameLength + ") is less " + "than lengthFieldEndOffset: " + lengthFieldEndOffset);
}
private static void failOnFrameLengthLessThanInitialBytesToStrip(ByteBuf in, long frameLength, int initialBytesToStrip) {
in.skipBytes((int) frameLength);
throw new CorruptedFrameException(
"Adjusted frame length (" + frameLength + ") is less " + "than initialBytesToStrip: " + initialBytesToStrip);
}
protected ByteBuf extractFrame(ByteBuf buffer, int index, int length) {
return buffer.retainedSlice(index, length);
}
}

View File

@@ -0,0 +1,51 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.decoder;
import com.fasterxml.jackson.databind.JsonNode;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import sanbing.jcpp.protocol.domain.ProtocolUplinkMsg;
import java.nio.charset.Charset;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
@RequiredArgsConstructor
@Slf4j
public class TcpMsgDecoder<T> extends MessageToMessageDecoder<ByteBuf> {
private final String protocolName;
private final Function<ByteBuf, T> transformer;
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {
try {
out.add(new ProtocolUplinkMsg<>(ctx.pipeline().channel().remoteAddress(), UUID.randomUUID(), this.transformer.apply(msg), msg.readableBytes()));
} catch (Exception e) {
log.error("[{}][{}] Exception during of decoding message", protocolName, ctx.channel(), e);
throw new RuntimeException(e);
}
}
public static byte[] toByteArray(ByteBuf buffer) {
byte[] bytes = new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
return bytes;
}
public static String toString(ByteBuf buffer, String charsetName) {
return buffer.toString(Charset.forName(charsetName));
}
public static JsonNode toJson(ByteBuf buffer) {
return JacksonUtil.fromBytes(toByteArray(buffer));
}
}

View File

@@ -0,0 +1,15 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.enums;
/**
* 读取动作,辅助枚举
*
* @author baigod
*/
public enum ReadAct {
BREAK,
CONTINUE
}

View File

@@ -0,0 +1,21 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.enums;
/**
* @author baigod
*/
public enum SequenceNumberLength {
// 1字节
BYTE,
// 2字节
SHORT,
// 4字节
INT,
}

View File

@@ -0,0 +1,60 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.group.ChannelGroup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@RequiredArgsConstructor
public class ConnectionLimitHandler extends ChannelInboundHandlerAdapter {
private final String protocolName;
private final int maxConnections;
private final ChannelGroup channelGroup;
private final AtomicInteger connectionsGauge;
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
if (connectionsGauge.incrementAndGet() > maxConnections) {
ctx.close();
log.info("[{}]{} channelRegistered超过最大连接数 {},因此关闭连接 {}",protocolName, ctx.channel(), maxConnections, ctx.channel());
} else {
super.channelRegistered(ctx);
log.info("[{}]{} channelRegistered 当前连接数 {} / {}",protocolName, ctx.channel(), connectionsGauge.get(), maxConnections);
}
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
connectionsGauge.decrementAndGet();
super.channelUnregistered(ctx);
log.info("[{}]{} channelUnregistered 当前连接数 {} / {}",protocolName, ctx.channel(), connectionsGauge.get(), maxConnections);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
channelGroup.add(ctx.channel());
log.info("[{}]{} channelActive 当前连接数管道数 {} / {}",protocolName, ctx.channel(), channelGroup.size(), maxConnections);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
channelGroup.remove(ctx.channel());
log.info("[{}]{} channelInactive 当前连接数管道数 {} / {}",protocolName, ctx.channel(), channelGroup.size(), maxConnections);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("[{}]{} ConnectionLimitHandler exceptionCaught",protocolName, ctx.channel(), cause);
ctx.close();
}
}

View File

@@ -0,0 +1,35 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 心跳检测
*
* @author baigod
*/
@Slf4j
@RequiredArgsConstructor
public class IdleEventHandler extends ChannelInboundHandlerAdapter {
private final String protocolName;
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent event) {
if (event.state() == IdleState.READER_IDLE) {
ctx.close();
log.info("[{}]{} 检测到空闲连接,连接关闭", protocolName, ctx.channel());
}
}
}
}

View File

@@ -0,0 +1,25 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.listener.tcp.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import sanbing.jcpp.infrastructure.util.mdc.MDCUtils;
import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil;
/**
* @author baigod
*/
public class TracerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
TracerContextUtil.newTracer("jcpp-protocol");
MDCUtils.recordTracer();
super.channelRead(ctx, msg);
}
}

View File

@@ -0,0 +1,30 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.provider;
import sanbing.jcpp.protocol.domain.ProtocolSession;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* @author baigod
*/
public interface ProtocolSessionRegistryProvider {
/**
* 注册会话
*/
void register(ProtocolSession protocolSession);
void unregister(UUID sessionId);
CompletableFuture<ProtocolSession> get(UUID sessionId);
/**
* 活跃会话
*/
void activate(ProtocolSession protocolSession);
}

View File

@@ -0,0 +1,15 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.provider;
import sanbing.jcpp.protocol.cfg.ProtocolCfg;
/**
* @author baigod
*/
public interface ProtocolsConfigProvider {
ProtocolCfg loadConfig(String protocol);
}

View File

@@ -0,0 +1,115 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.provider.impl;
import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory;
import sanbing.jcpp.infrastructure.util.config.ThreadPoolConfiguration;
import sanbing.jcpp.protocol.domain.ProtocolSession;
import sanbing.jcpp.protocol.domain.SessionCloseReason;
import sanbing.jcpp.protocol.provider.ProtocolSessionRegistryProvider;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @author baigod
*/
@Service
@Slf4j
public class DefaultProtocolSessionRegistryProvider implements ProtocolSessionRegistryProvider {
private static final int INIT_CACHE_LIMIT = 100_000;
@Value("${service.protocols.sessions.default-inactivity-timeout-in-sec}")
private int defaultInactivityTimeoutInSec;
@Value("${service.protocols.sessions.default-state-check-interval-in-sec}")
private int defaultStateCheckIntervalInSec;
@Getter
private final AsyncCache<UUID, ProtocolSession> SESSION_CACHE = buildCache();
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(JCPPThreadFactory.forName("session-state-checker"));
@PostConstruct
public void init() {
scheduledExecutorService.scheduleAtFixedRate(() ->
SESSION_CACHE.asMap().forEach((id, sessionCompletableFuture) ->
sessionCompletableFuture.whenComplete((protocolSession, throwable) -> {
if (throwable == null && protocolSession != null) {
if (protocolSession.getLastActivityTime().isBefore(LocalDateTime.now().minusSeconds(defaultInactivityTimeoutInSec))) {
protocolSession.close(SessionCloseReason.INACTIVE);
unregister(protocolSession.getId());
}
}
})
), defaultStateCheckIntervalInSec, defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
}
@PreDestroy
public void destroy() {
scheduledExecutorService.shutdownNow();
}
@Override
public void register(ProtocolSession protocolSession) {
if (log.isDebugEnabled()) {
log.debug("Registering session {}", protocolSession);
}
SESSION_CACHE.put(protocolSession.getId(), CompletableFuture.supplyAsync(() -> protocolSession, ThreadPoolConfiguration.JCPP_COMMON_THREAD_POOL));
}
@Override
public void unregister(UUID sessionId) {
log.info("Unregistering session {}", sessionId);
SESSION_CACHE.synchronous().invalidate(sessionId);
}
@Override
public CompletableFuture<ProtocolSession> get(UUID sessionId) {
log.debug("Get session {}", sessionId);
return SESSION_CACHE.get(sessionId, uuid -> null);
}
@Override
public void activate(ProtocolSession protocolSession) {
if (log.isDebugEnabled()) {
log.debug("Activating session {}", protocolSession);
}
protocolSession.setLastActivityTime(LocalDateTime.now());
SESSION_CACHE.put(protocolSession.getId(), CompletableFuture.supplyAsync(() -> protocolSession, ThreadPoolConfiguration.JCPP_COMMON_THREAD_POOL));
}
private AsyncCache<UUID, ProtocolSession> buildCache() {
return Caffeine.newBuilder()
.initialCapacity(INIT_CACHE_LIMIT)
.maximumSize(INIT_CACHE_LIMIT * 10)
.executor(ThreadPoolConfiguration.JCPP_COMMON_THREAD_POOL)
.buildAsync();
}
}

View File

@@ -0,0 +1,37 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.protocol.provider.impl;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Service;
import sanbing.jcpp.infrastructure.util.config.ConstraintValidator;
import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil;
import sanbing.jcpp.protocol.cfg.ProtocolCfg;
import sanbing.jcpp.protocol.provider.ProtocolsConfigProvider;
import java.util.Map;
@Setter
@Service
@Slf4j
@ConfigurationProperties("service")
public class DefaultProtocolsConfigProvider implements ProtocolsConfigProvider {
private Map<String, ProtocolCfg> protocols;
@Override
public ProtocolCfg loadConfig(String protocol) {
ProtocolCfg protocolCfg = protocols.get(protocol);
log.info("load {}'s configuration: \n{}", protocol, JacksonUtil.toPrettyString(protocolCfg));
ConstraintValidator.validateFields(protocolCfg, "'" + protocol + "' configuration is invalid:");
return protocolCfg;
}
}