mirror of
https://gitee.com/san-bing/JChargePointProtocol
synced 2026-05-05 02:19:56 +08:00
云快充1.5.0 初始化
This commit is contained in:
50
jcpp-protocol-api/pom.xml
Normal file
50
jcpp-protocol-api/pom.xml
Normal 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>
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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("/**");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 抖音关注:程序员三丙
|
||||
* 知识星球:https://t.zsxq.com/j9b21
|
||||
*/
|
||||
package sanbing.jcpp.protocol.cfg.enums;
|
||||
|
||||
public enum ForwarderType {
|
||||
|
||||
memory, // 本地队列模式
|
||||
|
||||
kafka // Kafka模式 - 发送到外部
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 抖音关注:程序员三丙
|
||||
* 知识星球:https://t.zsxq.com/j9b21
|
||||
*/
|
||||
package sanbing.jcpp.protocol.cfg.enums;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
*/
|
||||
public enum TcpHandlerType {
|
||||
TEXT,
|
||||
BINARY,
|
||||
JSON
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 抖音关注:程序员三丙
|
||||
* 知识星球:https://t.zsxq.com/j9b21
|
||||
*/
|
||||
package sanbing.jcpp.protocol.domain;
|
||||
|
||||
/**
|
||||
* @author baigod
|
||||
*/
|
||||
public enum SessionCloseReason {
|
||||
DESTRUCTION,
|
||||
INACTIVE,
|
||||
MANUALLY
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 抖音关注:程序员三丙
|
||||
* 知识星球:https://t.zsxq.com/j9b21
|
||||
*/
|
||||
package sanbing.jcpp.protocol.listener.tcp.enums;
|
||||
|
||||
/**
|
||||
* 读取动作,辅助枚举
|
||||
*
|
||||
* @author baigod
|
||||
*/
|
||||
public enum ReadAct {
|
||||
BREAK,
|
||||
CONTINUE
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user