代码初始化

This commit is contained in:
2026-05-07 15:54:43 +08:00
parent a078ca46e5
commit 3522f1011f
274 changed files with 15704 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
# BaseModule
**公共运行时库**:被 `ServerModule``CrossModule` 依赖。包含 **Netty TCP 帧编解码**、**CmdRpc 分发管线**、**Proto 元数据注册表**、**Spring 命令扫描** 以及与跨服调用相关的 **`CrossRpcInvoker` 接口**。
本模块 **不是** 可执行 Spring Boot 应用;随区服或跨服进程一起加载。
## 网络分层(逻辑)
```mermaid
flowchart TB
subgraph TCP["TCP 字节流"]
F["一帧: 4B length + body"]
end
subgraph Net["BaseModule / net & frame"]
D["LengthPrefixedProtobuf 解码"]
H["CmdRpcTcpFrameHandler"]
end
subgraph Cmd["BaseModule / cmd"]
CD["CmdRpcChannelDispatch"]
PR["ProtoRegistry → ProtoInfo"]
PC["ProtoCommandReflect 解析 CTS / newBuilder STC"]
DP["CmdRpcDispatch → GameCommand"]
end
F --> D --> H --> CD
CD --> PR
CD --> PC
CD --> DP
```
## 1. 帧格式:`LengthPrefixedProtobuf`
- **编码**`LengthPrefixedProtobuf.encodeMessage(alloc, CmdRpcEnvelope)` — 先写 **大端 int32 长度**,再写 `CmdRpcEnvelope.toByteArray()`
- **解码**`newTcpFrameDecoder()` 返回 Netty 解码器,攒满一帧后向 pipeline 下游抛出 **一条 ByteBuf(仅 body**
这样 **区服客户端 TCP****跨服 TCP****区服连跨服的 Outbound** 三者 **帧格式一致**,仅 IP/端口与 `CommandSource` 不同。
## 2. 入站处理:`CmdRpcTcpFrameHandler`
- 读取一帧 `byte[]`,调用 **`CmdRpcChannelDispatch.dispatch(...)`**。
- 将返回的 **`CmdRpcEnvelope`** 再次 **长度前缀编码** 写回对端。
- `linkSource` 由构造时注入:`Client_TCP`(区服接客户端)或 `CROSS_TCP`(跨服接入站)。
**注意**:默认在 **Netty IO 线程** 上执行命令;若 `roundTrip` 同步调跨服会阻塞该连接上的 IO,Demo 场景可接受,生产需评估线程模型。
## 3. 分发核心:`CmdRpcChannelDispatch`
步骤简述:
1. `CmdRpcEnvelope.parseFrom(raw)`
2. `protoRegistry.resolve(linkSource, serviceId, methodId)` → 可选的 **`ProtoInfo`**。
3. 若存在 `ProtoInfo`**解析 payload 为 CTS**、**创建 STC 的 `Builder`**(网络层职责,命令内只填字段)。
4. 构造 **`CommandContext`**(含 `ChannelHandlerContext``CrossRpcInvoker`、解码结果等)。
5. **`CmdRpcDispatch.dispatchEnvelope(in, lookup, cctx)`** → 执行具体 **`GameCommand`**。
```mermaid
flowchart LR
IN[CmdRpcEnvelope in]
PI{ProtoInfo 存在?}
DEC[解码 CTS + STC Builder]
RAW[decoded=null 由命令自处理]
EXE[GameCommand.execute]
OUT[CmdRpcEnvelope out]
IN --> PI
PI -->|是| DEC --> EXE
PI -->|否| RAW --> EXE
EXE --> OUT
```
## 4. 命令模型
| 类型 | 说明 |
|------|------|
| `GameCommand` | `execute(CommandContext, CmdRpcEnvelope)`,完全自控 payload。 |
| `AbstractTypedPbCommand<C, B>` | 依赖 `ProtoInfo``run(ctx, cts, stcBuilder)`;回包信封 **复制** `service/method/cross_ret/link_seq`。 |
### `CommandSource` 与注册
- `@Cmd(source, serviceId, methodId)` 标注在命令类上。
- **`SpringGameCommandRegistry`**:按 Spring 容器里 `GameCommand` Bean 扫描,**只收录** `source` 属于当前进程配置集合的命令(区服只收 `Client_TCP`,跨服只收 `CROSS_TCP`)。
## 5. `ProtoRegistry` 与自动注册
- **`ProtoRegistry`**`register(ProtoInfo)` / `resolve(source, serviceId, methodId)`
- **`ProtoRegistryAutoConfigurer`**`SmartInitializingSingleton`):扫描带 `@Cmd``GameCommand`,若继承 **`AbstractTypedPbCommand`**,则从泛型推断 **CTS / STC 类型** 并注册 **`ProtoInfo`**。
- **未** 继承强类型基类的命令(如路径 3 中继)**不注册** `ProtoInfo`,由命令自行处理 payload。
## 6. `CrossRpcInvoker`
- 接口方法:`CmdRpcEnvelope roundTrip(CmdRpcEnvelope request)`
- **ServerModule** 中由 **`CrossTcpClient`** 实现:维护至跨服的 **长连接**`link_seq` 多路复用、注册与心跳。
区服业务命令(路径 2/3)通过 **`CommandContext.crossRpc()`** 拿到该实例,向跨服发 **`CrossForward*`** 对应的信封。
## 7. 依赖说明
- **Spring Context**`@Component` 等)、**Netty**、**protobuf-java**、**slf4j-api**、**Lombok**(可选)。
- 生成的 proto Java 类来自 **ProtoBufModule**
## 包结构(主要)
```text
com.huangzj.base
├── cmd # 分发、上下文、注册表、Proto 反射、CrossRpcInvoker
├── frame # 长度前缀、Buf 工具
├── net # CmdRpcTcpFrameHandler
└── spring # SpringGameCommandRegistry、其它 Spring 辅助
```
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>huangzj</groupId>
<artifactId>comm-framework</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>BaseModule</artifactId>
<packaging>jar</packaging>
<name>BaseModule</name>
<dependencies>
<dependency>
<groupId>huangzj</groupId>
<artifactId>ProtoBufModule</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-buffer</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec</artifactId>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,37 @@
package com.huangzj.base.cmd;
import com.huangzj.cmd.gen.CmdRpcEnvelope;
import com.google.protobuf.Message;
import org.springframework.util.ClassUtils;
/**
* 强类型 PB 命令:CTS/STC 由 {@link ProtoRegistryAutoConfigurer} 根据本类泛型与 {@link Cmd} 自动注册为
* {@link ProtoInfo};网络层解析请求并创建 {@link CommandContext#responseBuilder()},子类只实现 {@link #run}。
*/
public abstract class AbstractTypedPbCommand<C extends Message, B extends Message.Builder> implements GameCommand {
@Override
public CmdRpcEnvelope execute(CommandContext ctx, CmdRpcEnvelope request) throws Exception {
if (ctx.protoInfo() == null || ctx.decodedRequest() == null || ctx.responseBuilder() == null) {
throw new IllegalStateException(
"缺少 ProtoInfo 或网络层未解码: "
+ ClassUtils.getUserClass(getClass()).getName()
+ ",请确认本类带 @Cmd 且继承 AbstractTypedPbCommand<CTS, STC.Builder> 以自动注册 ProtoInfo");
}
@SuppressWarnings("unchecked")
C cts = (C) ctx.decodedRequest();
@SuppressWarnings("unchecked")
B stcBuilder = (B) ctx.responseBuilder();
run(ctx, cts, stcBuilder);
// CmdRpcEnvelope头原样带回:保证请求方(含区服↔跨服 link_seq)能把响应与请求对齐
return CmdRpcEnvelope.newBuilder()
.setServiceId(request.getServiceId())
.setMethodId(request.getMethodId())
.setCrossRet(request.getCrossRet())
.setLinkSeq(request.getLinkSeq())
.setPayload(stcBuilder.build().toByteString())
.build();
}
protected abstract void run(CommandContext ctx, C cts, B stcBuilder) throws Exception;
}
@@ -0,0 +1,25 @@
package com.huangzj.base.cmd;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标在具体的 {@link GameCommand} 实现上,Spring 扫 Bean 时按来源 + service/method 归组。
* {@code serviceId} / {@code methodId} 请使用 {@link com.huangzj.cmd.gen.PbService} 与各 {@code ServiceN_Method}
* 生成的 {@code *_VALUE} 常量,勿写裸数字。
* CTS/STC 在 {@link ProtoRegistry} 中通过 {@link ProtoInfo} 注册,须与本注解一致。
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Cmd {
CommandSource source();
/** 对应 {@link com.huangzj.cmd.gen.PbService} 的 wire 值,如 {@code PbService.Service_1_VALUE}。 */
int serviceId();
/** 对应具体 {@code ServiceN_Method} 的 wire 值,如 {@code Service1_Method.Method1_VALUE}。 */
int methodId();
}
@@ -0,0 +1,54 @@
package com.huangzj.base.cmd;
import com.huangzj.cmd.gen.CmdRpcEnvelope;
import com.google.protobuf.Message;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
/**
* Netty 入站:先解析CmdRpcEnvelope,再按 {@link ProtoRegistry} 取 {@link ProtoInfo} 做 CTS 解码与 STC Builder 创建(网络层职责),
* 最后交给 {@link CmdRpcDispatch}。
*/
@Slf4j
public final class CmdRpcChannelDispatch {
private CmdRpcChannelDispatch() {
}
/**
* @param linkSource 当前链路(客户端 TCP / 跨服 TCP),与注册表 {@link ProtoInfo#source()} 一致
* @param protoRegistry PB 元数据表,在应用启动时完成注册
*/
public static CmdRpcEnvelope dispatch(
byte[] raw,
GameCommandLookup lookup,
ChannelHandlerContext ctx,
CrossRpcInvoker crossRpc,
CommandSource linkSource,
ProtoRegistry protoRegistry)
throws Exception {
CmdRpcEnvelope in = CmdRpcEnvelope.parseFrom(raw);
// 同一套 service/method,在 Client_TCP 与 CROSS_TCP 可对应不同 ProtoInfo(来源隔离)
ProtoInfo protoInfo = protoRegistry.resolve(linkSource, in.getServiceId(), in.getMethodId());
if (log.isDebugEnabled()) {
log.debug(
"CmdRpc inbound source={} remote={} service={} method={} protoInfo={}",
linkSource,
ctx.channel().remoteAddress(),
in.getServiceId(),
in.getMethodId(),
protoInfo != null);
}
Message decoded = null;
Message.Builder responseBuilder = null;
if (protoInfo != null) {
// 强类型命令:网络层负责 bytes→CTS、预创建 STC Builder,命令里只填字段
decoded = ProtoCommandReflect.parser(protoInfo.ctsClass()).parseFrom(in.getPayload());
responseBuilder = ProtoCommandReflect.builderSupplier(protoInfo.stcClass()).get();
}
// protoInfo==null 时(如纯中继)由具体 GameCommand 自行解析 payload / 组包
CommandContext cctx =
new CommandContext(ctx, crossRpc, in.getCrossRet(), protoInfo, decoded, responseBuilder);
return CmdRpcDispatch.dispatchEnvelope(in, lookup, cctx);
}
}
@@ -0,0 +1,30 @@
package com.huangzj.base.cmd;
import com.huangzj.cmd.gen.CmdRpcEnvelope;
/**
* 解析CmdRpcEnvelope、查表、执行,三步并一行,Netty handler 里少写点重复代码。
*/
public final class CmdRpcDispatch {
private CmdRpcDispatch() {
}
/**
* 从原始字节起跳,适合 TCP 一帧读完再分发。
*/
public static CmdRpcEnvelope dispatch(byte[] raw, GameCommandLookup lookup, CommandContext ctx) throws Exception {
CmdRpcEnvelope in = CmdRpcEnvelope.parseFrom(raw);
return dispatchEnvelope(in, lookup, ctx);
}
/**
* 已经 parse 过CmdRpcEnvelope时用,避免重复解析;上下文里的 cross_ret 必须在这之前就算好。
*/
public static CmdRpcEnvelope dispatchEnvelope(CmdRpcEnvelope in, GameCommandLookup lookup, CommandContext ctx) throws Exception {
// lookup 按「来源已过滤后的表」解析,故同一 service/method 在区服/跨服可绑定不同实现类
GameCommand cmd = lookup.resolve(in.getServiceId(), in.getMethodId());
// 异常若直接抛出,由上层 Netty handler 记录并关连接;此处不吞异常
return cmd.execute(ctx, in);
}
}
@@ -0,0 +1,52 @@
package com.huangzj.base.cmd;
import com.google.protobuf.Message;
import io.netty.channel.ChannelHandlerContext;
import lombok.Getter;
import lombok.experimental.Accessors;
/**
* 命令执行上下文:Netty、跨服调用器、CmdRpcEnvelope cross_ret,以及网络层根据 {@link ProtoInfo} 解析好的请求/响应 Builder。
*/
@Getter
@Accessors(fluent = true)
public class CommandContext {
private final ChannelHandlerContext channelHandlerContext;
private final CrossRpcInvoker crossRpc;
private final int crossReturnCode;
/**
* 本帧在 {@link ProtoRegistry} 中对应的元数据;未注册强类型协议时为 null。
*/
private final ProtoInfo protoInfo;
/**
* 网络层已按 {@link ProtoInfo#ctsClass()} 解析出的请求体;无 ProtoInfo 时为 null。
*/
private final Message decodedRequest;
/**
* 网络层按 {@link ProtoInfo#stcClass()} 创建的 STC Builder,业务填充后组包;无 ProtoInfo 时为 null。
*/
private final Message.Builder responseBuilder;
public CommandContext(ChannelHandlerContext channelHandlerContext, CrossRpcInvoker crossRpcInvoker) {
this(channelHandlerContext, crossRpcInvoker, 0, null, null, null);
}
public CommandContext(ChannelHandlerContext channelHandlerContext, CrossRpcInvoker crossRpcInvoker, int crossReturnCode) {
this(channelHandlerContext, crossRpcInvoker, crossReturnCode, null, null, null);
}
/**
* @param protoInfo 可为 null
* @param decodedRequest 与 protoInfo 成对;有 protoInfo 时通常非 null
* @param responseBuilder 与 protoInfo 成对;有 protoInfo 时通常非 null
*/
public CommandContext(ChannelHandlerContext channelHandlerContext, CrossRpcInvoker crossRpcInvoker, int crossReturnCode, ProtoInfo protoInfo, Message decodedRequest, Message.Builder responseBuilder) {
this.channelHandlerContext = channelHandlerContext;
this.crossRpc = crossRpcInvoker;
this.crossReturnCode = crossReturnCode;
this.protoInfo = protoInfo;
this.decodedRequest = decodedRequest;
this.responseBuilder = responseBuilder;
}
}
@@ -0,0 +1,28 @@
package com.huangzj.base.cmd;
import lombok.Getter;
/**
* 命令从哪儿来:区服面向客户端的 TCP、或跨服进程上的 TCP。
* {@code fromType} 若要与外部协议数字对齐,可直接使用。
*/
@Getter
public enum CommandSource {
/** 区服侧面向客户端的那条 Cmd/会话链路(名字历史遗留,别和 OSI 的 TCP 较真)。 */
Client_TCP(1, "CLIENT_TCP"),
/** 跨服机器上监听的 CmdRpc TCP。 */
CROSS_TCP(2, "CROSS_TCP");
/** 若要和外部协议数字对齐,用这个。 */
private final int fromType;
/** 给人看的短名字,日志里用着方便。 */
private final String fromTypeName;
CommandSource(int fromType, String fromTypeName) {
this.fromType = fromType;
this.fromTypeName = fromTypeName;
}
}
@@ -0,0 +1,14 @@
package com.huangzj.base.cmd;
import com.huangzj.cmd.gen.CmdRpcEnvelope;
/**
* 区服侧想跟跨服聊一句时用的抽象,默认实现是 TCP 同步 roundTrip。
*/
public interface CrossRpcInvoker {
/**
* 发一帧过去、等一帧回来;实现里一般是 TCP 同步写读,超时或断线要抛异常。
*/
CmdRpcEnvelope roundTrip(CmdRpcEnvelope request) throws Exception;
}
@@ -0,0 +1,15 @@
package com.huangzj.base.cmd;
import com.huangzj.cmd.gen.CmdRpcEnvelope;
/**
* 一条 CmdRpc 业务命令:入参出参都是CmdRpcEnvelope,具体 PB 在实现里自己拆。
*/
public interface GameCommand {
/**
* 处理一帧 RPCrequest 里带好 service/method/payload,返回完整CmdRpcEnvelope(含 payload)。
* 具体异常策略由上层 {@code CmdRpcDispatch} 决定是否包进CmdRpcEnvelope或断开连接。
*/
CmdRpcEnvelope execute(CommandContext ctx, CmdRpcEnvelope request) throws Exception;
}
@@ -0,0 +1,9 @@
package com.huangzj.base.cmd;
/**
* 根据 serviceId + methodId 捞出一条命令,找不到就抛 IllegalArgumentException。
*/
public interface GameCommandLookup {
GameCommand resolve(int serviceId, int methodId);
}
@@ -0,0 +1,60 @@
package com.huangzj.base.cmd;
import com.google.protobuf.Message;
import com.google.protobuf.Parser;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
/**
* 按 protobuf 生成类做 CTS 解析 / STC Builder 创建(类比原工程 {@code ProtoMgr.decode} / {@code getStcProtoMessageByCtsNo})。
*/
final class ProtoCommandReflect {
private static final ConcurrentHashMap<Class<?>, Parser<?>> PARSERS = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<Class<?>, Supplier<? extends Message.Builder>> BUILDER_FACTORIES =
new ConcurrentHashMap<>();
private ProtoCommandReflect() {
}
@SuppressWarnings("unchecked")
static <C extends Message> Parser<C> parser(Class<? extends Message> ctsClass) {
return (Parser<C>) PARSERS.computeIfAbsent(ctsClass, ProtoCommandReflect::loadParser);
}
@SuppressWarnings("unchecked")
static <B extends Message.Builder> Supplier<B> builderSupplier(Class<? extends Message> stcClass) {
return (Supplier<B>) BUILDER_FACTORIES.computeIfAbsent(stcClass, ProtoCommandReflect::loadBuilderFactory);
}
private static Parser<?> loadParser(Class<?> msgClass) {
try {
Method m = msgClass.getMethod("parser");
return (Parser<?>) m.invoke(null);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(
"CTS 须为 protobuf 生成类且含 public static Parser parser(): " + msgClass.getName(), e);
}
}
private static Supplier<? extends Message.Builder> loadBuilderFactory(Class<?> msgClass) {
try {
Method m = msgClass.getMethod("newBuilder");
Object probe = m.invoke(null);
if (!(probe instanceof Message.Builder)) {
throw new IllegalStateException("newBuilder 未返回 Message.Builder: " + msgClass.getName());
}
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(
"STC 须为 protobuf 生成类且含 public static Builder newBuilder(): " + msgClass.getName(), e);
}
return () -> {
try {
return (Message.Builder) msgClass.getMethod("newBuilder").invoke(null);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
};
}
}
@@ -0,0 +1,14 @@
package com.huangzj.base.cmd;
import com.google.protobuf.Message;
/**
* PB 接口注册时的元数据:与 {@link CommandSource} + service/method 绑定,供网络层取 CTS/STC 类型做解析。
* (对齐原工程 {@code ProtoInfo + ProtoMgr.getProtoInfoByCode} 思路。)
*/
public record ProtoInfo(
CommandSource source,
int serviceId,
int methodId,
Class<? extends Message> ctsClass,
Class<? extends Message> stcClass) {}
@@ -0,0 +1,33 @@
package com.huangzj.base.cmd;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
/**
* 全局 PB 命令表:按「链路 + service + method」注册 {@link ProtoInfo},网络层只查这里拿类型。
*/
@Component
public class ProtoRegistry {
private final ConcurrentHashMap<String, ProtoInfo> byKey = new ConcurrentHashMap<>();
public void register(ProtoInfo info) {
String k = key(info.source(), info.serviceId(), info.methodId());
ProtoInfo prev = byKey.putIfAbsent(k, info);
if (prev != null) {
throw new IllegalStateException("ProtoInfo 重复注册: " + k + " 已有 " + prev);
}
}
/**
* 未注册时返回 null(例如纯透传、自定义解析的命令)。
*/
public ProtoInfo resolve(CommandSource source, int serviceId, int methodId) {
return byKey.get(key(source, serviceId, methodId));
}
private static String key(CommandSource source, int serviceId, int methodId) {
return source.name() + ":" + serviceId + ":" + methodId;
}
}
@@ -0,0 +1,56 @@
package com.huangzj.base.cmd;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
/**
* 启动时扫描所有 {@link GameCommand} Bean:带 {@link Cmd} 且继承 {@link AbstractTypedPbCommand} 的,
* 根据注解中的 source/service/method 与泛型上的 CTS/STC 自动 {@link ProtoRegistry#register}。
* 未继承强类型基类(如纯透传)的命令不注册,由网络层按无 {@link ProtoInfo} 处理。
*/
@Slf4j
@Component
public class ProtoRegistryAutoConfigurer implements SmartInitializingSingleton {
private final ApplicationContext applicationContext;
private final ProtoRegistry protoRegistry;
public ProtoRegistryAutoConfigurer(ApplicationContext applicationContext, ProtoRegistry protoRegistry) {
this.applicationContext = applicationContext;
this.protoRegistry = protoRegistry;
}
@Override
public void afterSingletonsInstantiated() {
Map<String, GameCommand> beans = applicationContext.getBeansOfType(GameCommand.class);
AtomicInteger n = new AtomicInteger();
for (GameCommand bean : beans.values()) {
Class<?> userClass = ClassUtils.getUserClass(bean.getClass());
Cmd cmd = userClass.getAnnotation(Cmd.class);
if (cmd == null) {
continue;
}
// 仅 AbstractTypedPbCommand 子类可解析泛型;GameCommand 裸实现(如路径3中继)不注册 ProtoInfo
TypedPbCommandReflect.resolveCtsStc(userClass)
.ifPresent(pair -> {
ProtoInfo info =
new ProtoInfo(cmd.source(), cmd.serviceId(), cmd.methodId(), pair.cts(), pair.stc());
protoRegistry.register(info);
n.incrementAndGet();
log.info(
"ProtoInfo registered: source={} service={} method={} cts={} stc={}",
cmd.source(),
cmd.serviceId(),
cmd.methodId(),
pair.cts().getSimpleName(),
pair.stc().getSimpleName());
});
}
log.info("ProtoRegistry auto-registration done, {} ProtoInfo bean(s)", n.get());
}
}
@@ -0,0 +1,64 @@
package com.huangzj.base.cmd;
import com.google.protobuf.Message;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import org.springframework.util.ClassUtils;
/**
* 从继承 {@link AbstractTypedPbCommand} 的具体命令类上解析 CTS / STC 消息类型(第二泛参为 {@code Xxx.Builder} 时,STC 取 {@code Xxx})。
*/
final class TypedPbCommandReflect {
private TypedPbCommandReflect() {}
record CtsStc(Class<? extends Message> cts, Class<? extends Message> stc) {}
static java.util.Optional<CtsStc> resolveCtsStc(Class<?> commandClass) {
Class<?> user = ClassUtils.getUserClass(commandClass);
ParameterizedType pt = findAbstractTypedSuper(user);
if (pt == null) {
// 非 AbstractTypedPbCommand 子类,或泛型在运行期被擦除到无法解析
return java.util.Optional.empty();
}
Type[] args = pt.getActualTypeArguments();
if (args.length < 2) {
return java.util.Optional.empty();
}
Class<?> ctsClass = rawClass(args[0]);
Class<?> builderOrSecond = rawClass(args[1]);
if (ctsClass == null
|| builderOrSecond == null
|| !Message.class.isAssignableFrom(ctsClass)) {
return java.util.Optional.empty();
}
// Java PB 惯例:Message.Builder 为 Message 的静态内部类,外层即 STC 类型
Class<?> stcClass = builderOrSecond.getDeclaringClass();
if (stcClass == null || !Message.class.isAssignableFrom(stcClass)) {
return java.util.Optional.empty();
}
return java.util.Optional.of(
new CtsStc(ctsClass.asSubclass(Message.class), stcClass.asSubclass(Message.class)));
}
private static ParameterizedType findAbstractTypedSuper(Class<?> c) {
while (c != null && c != Object.class) {
Type t = c.getGenericSuperclass();
if (t instanceof ParameterizedType p && p.getRawType() == AbstractTypedPbCommand.class) {
return p;
}
c = c.getSuperclass();
}
return null;
}
private static Class<?> rawClass(Type t) {
if (t instanceof Class<?> cl) {
return cl;
}
if (t instanceof ParameterizedType p && p.getRawType() instanceof Class<?> cl) {
return cl;
}
return null;
}
}
@@ -0,0 +1,32 @@
package com.huangzj.base.frame;
import com.google.protobuf.MessageLite;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
/**
* TCP 上 4 字节长度 + protobuf body 的通用写法,跨服和区服客户端共用。
*/
public final class LengthPrefixedProtobuf {
/** 单帧上限,防止恶意大包把内存打爆。 */
public static final int MAX_FRAME_LENGTH = 65536;
private LengthPrefixedProtobuf() {
}
/** 长度字段在开头 4 字节,剥离长度头后剩下纯 PB。 */
public static LengthFieldBasedFrameDecoder newTcpFrameDecoder() {
return new LengthFieldBasedFrameDecoder(MAX_FRAME_LENGTH, 0, 4, 0, 4);
}
/** 先发 4 字节小端长度,再发 body,和上面的 decoder 成对使用。 */
public static ByteBuf encodeMessage(ByteBufAllocator alloc, MessageLite message) {
byte[] body = message.toByteArray();
ByteBuf buf = alloc.buffer(4 + body.length);
buf.writeInt(body.length);
buf.writeBytes(body);
return buf;
}
}
@@ -0,0 +1,20 @@
package com.huangzj.base.frame;
import io.netty.buffer.ByteBuf;
/**
* 把 ByteBuf 里当前可读区间一次性拷成 byte[],handler 里少写两行样板代码。
*/
public final class NettyBufUtil {
private NettyBufUtil() {
}
/** 读指针会跟着动,注意别重复读同一段。 */
public static byte[] readReadableBytes(ByteBuf buf) {
int n = buf.readableBytes();
byte[] raw = new byte[n];
buf.readBytes(raw);
return raw;
}
}
@@ -0,0 +1,16 @@
package com.huangzj.base.module;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 挂在某个宿主(一般是玩家或会话)上的模块基类,手里攥着宿主引用。
*
* @param <P> 宿主类型,demo 里是 {@link com.huangzj.server.module.DemoPlayerHost}
*/
@Getter
@RequiredArgsConstructor
public abstract class AbsHostModule<P> {
protected final P player;
}
@@ -0,0 +1,121 @@
package com.huangzj.base.module;
import lombok.Getter;
import lombok.Setter;
/**
* 玩家侧逻辑模块的骨架:tick、存档、登录回调都先空着,子类按需填。
* 和修仙那套 GeneralModule 思路类似,这里做了精简,只保留 demo 要用的钩子。
*/
@Getter
@Setter
public abstract class GeneralModule<P> extends AbsHostModule<P> {
/** tick / 存盘时共用的锁,避免和业务线程打架(demo 里只用在 runTick)。 */
protected final Object saveLockObj = new Object();
/** 是否有脏数据要同步,具体怎么用看业务。 */
protected boolean needSync;
/** 是否已经进入过登录后流程,占位字段。 */
protected boolean enterAfterLogin;
/** 为 true 时才会被 {@link com.huangzj.base.spring.module.PlayerModuleRegistry#runTickAll(long)} 扫到。 */
private boolean enableTick;
protected GeneralModule(P player) {
super(player);
}
/** 标记需要同步,一般配合 syncChange 使用。 */
public void setNeedSync() {
this.needSync = true;
}
/** 同步做完了就清掉标记。 */
public void finishSync() {
this.needSync = false;
}
/** 当前模块归属的玩家 id,子类必须给出。 */
public abstract long getPlayerId();
/** 拉档,默认 true 表示不用拦。 */
public boolean loadData() {
return true;
}
/** 拉档后的二次处理,默认放行。 */
public boolean afterLoadData() {
return true;
}
/** 存盘,子类可写 Mongo/Redis;默认啥也不干。 */
public boolean saveData() {
return true;
}
/** 把内存改动推到客户端或其它服,force 是否强推由子类解释。 */
public void syncChange(boolean force) {
}
/** 模块类型,默认就是具体类。 */
public Class<?> getModuleClass() {
return getClass();
}
/** 登录完成要下发的消息,占位。 */
public void loginSendMsg(boolean isReconnect) {
}
/** 登录后逻辑,占位。 */
public void afterLogin(boolean isReconnect) {
}
/** 下线清理,占位。 */
public void offline() {
}
/** 处理游戏内推送/内部消息,占位。 */
public void handleGameMsg(Object msg) {
}
/** 红点刷新,占位。 */
public void refreshRedPoint() {
}
/** 跨天/重置类逻辑,占位。 */
public void dailyResetData() {
}
/** 打开定时 tick,注册表只会调度 isEnableTick 为 true 的模块。 */
public void enableTick() {
this.enableTick = true;
}
/** 若要做精准调度可覆写;默认 0 表示「不承诺下次时间点」。 */
public long calcNextTickAt() {
return 0L;
}
/**
* 外层统一包一层锁和异常,真正的周期逻辑写在 {@link #tick(long)}。
*/
public void runTick(long runAt) {
try {
synchronized (saveLockObj) {
tick(runAt);
}
} catch (Exception e) {
onTickError(e);
}
}
/** 每个调度周期进来一次,子类写业务。 */
protected void tick(long runAt) {
}
/** tick 抛错时的钩子,默认吞掉,需要打日志或告警就覆写。 */
protected void onTickError(Exception e) {
}
}
@@ -0,0 +1,15 @@
package com.huangzj.base.module;
import java.util.Collections;
import java.util.Map;
/**
* 模块要是愿意暴露点计数器之类的,就实现这个;注册表拼 /module/info 时会 merge 进来。
*/
public interface ModuleMetricsProvider {
/** 默认啥指标也没有,子类按需 override。 */
default Map<String, Object> metrics() {
return Collections.emptyMap();
}
}
@@ -0,0 +1,17 @@
package com.huangzj.base.module;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标在 {@link GeneralModule} 上,数字越小越先跑 tick(只是 demo 约定,你可以按习惯改)。
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PlayerModule {
/** 排序用,默认 0;相同顺序时不保证谁先谁后。 */
int order() default 0;
}
@@ -0,0 +1,48 @@
package com.huangzj.base.net;
import com.huangzj.base.cmd.CmdRpcChannelDispatch;
import com.huangzj.base.cmd.CommandSource;
import com.huangzj.base.cmd.CrossRpcInvoker;
import com.huangzj.base.cmd.GameCommandLookup;
import com.huangzj.base.cmd.ProtoRegistry;
import com.huangzj.base.frame.LengthPrefixedProtobuf;
import com.huangzj.base.frame.NettyBufUtil;
import com.huangzj.cmd.gen.CmdRpcEnvelope;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 长度前缀剥掉后的一帧 = 整包 {@link CmdRpcEnvelope}:查 {@link ProtoRegistry} 解码并分发。
*/
@Slf4j
@RequiredArgsConstructor
public class CmdRpcTcpFrameHandler extends SimpleChannelInboundHandler<ByteBuf> {
private final GameCommandLookup lookup;
private final CrossRpcInvoker crossRpc;
private final CommandSource linkSource;
private final ProtoRegistry protoRegistry;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] raw = NettyBufUtil.readReadableBytes(msg);
// 解码 + ProtoRegistry + GameCommand 执行在同一线程(默认 IO 线程),业务应避免长时间阻塞
CmdRpcEnvelope out =
CmdRpcChannelDispatch.dispatch(raw, lookup, ctx, crossRpc, linkSource, protoRegistry);
ctx.writeAndFlush(LengthPrefixedProtobuf.encodeMessage(ctx.alloc(), out));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error(
"CmdRpc TCP handler error source={} remote={}",
linkSource,
ctx.channel().remoteAddress(),
cause);
// 半包解码器之上若已错乱,继续读无意义,直接断链让客户端重连
ctx.close();
}
}
@@ -0,0 +1,55 @@
package com.huangzj.base.spring;
import com.huangzj.base.cmd.Cmd;
import com.huangzj.base.cmd.CommandSource;
import com.huangzj.base.cmd.GameCommand;
import com.huangzj.base.cmd.GameCommandLookup;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ClassUtils;
/**
* 启动时把 Spring 容器里的 {@link GameCommand} 扫一遍,按 {@link Cmd} 过滤来源。
* 当前 key 为 {@link Cmd#serviceId()} + {@link Cmd#methodId()}(与 {@link com.huangzj.cmd.gen.CmdRpcEnvelope} 一致)。
*/
@Slf4j
public class SpringGameCommandRegistry implements GameCommandLookup {
private final Map<String, GameCommand> commands = new HashMap<>();
/** 只收某一种来源的命令。 */
public SpringGameCommandRegistry(ApplicationContext applicationContext, CommandSource source) {
this(applicationContext, EnumSet.of(source));
}
/** 区服若收多种来源,用 Set 传多个 {@link CommandSource}。 */
public SpringGameCommandRegistry(ApplicationContext applicationContext, Set<CommandSource> sources) {
Map<String, GameCommand> beans = applicationContext.getBeansOfType(GameCommand.class);
for (GameCommand cmd : beans.values()) {
Class<?> userClass = ClassUtils.getUserClass(cmd);
Cmd meta = userClass.getAnnotation(Cmd.class);
// 同一 Bean 可能带 @Cmd(source=Client_TCP),在跨服进程扫描时会被 sources 过滤掉
if (meta != null && sources.contains(meta.source())) {
commands.put(key(meta.serviceId(), meta.methodId()), cmd);
}
}
log.info("GameCommand registry sources={} size={} keys={}", sources, commands.size(), commands.keySet());
}
private static String key(int serviceId, int methodId) {
return ":" + serviceId + ":" + methodId;
}
@Override
public GameCommand resolve(int serviceId, int methodId) {
GameCommand cmd = commands.get(key(serviceId, methodId));
if (cmd == null) {
throw new IllegalArgumentException("unknown " + serviceId + "/" + methodId);
}
return cmd;
}
}
@@ -0,0 +1,85 @@
package com.huangzj.base.spring.module;
import com.huangzj.base.module.GeneralModule;
import com.huangzj.base.module.ModuleMetricsProvider;
import com.huangzj.base.module.PlayerModule;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* 把容器里所有 {@link GeneralModule} 捞出来排个序,统一 tick、save,顺便给 HTTP 一个快照。
*/
@Component
public class PlayerModuleRegistry implements ApplicationContextAware {
/** 排好序的模块列表,只在容器刷新时重建。 */
private List<GeneralModule<?>> modules = List.of();
/**
* Spring 回调:收集所有 GeneralModule Bean,按 {@link PlayerModule#order()} 从小到大排。
*/
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, GeneralModule> raw = applicationContext.getBeansOfType(GeneralModule.class);
List<GeneralModule<?>> list = new ArrayList<>();
for (GeneralModule m : raw.values()) {
list.add(m);
}
list.sort(Comparator.comparingInt(this::orderOf));
modules = List.copyOf(list);
}
/** 没标注解就当 0,和别的模块挤在一起。 */
private int orderOf(GeneralModule<?> module) {
PlayerModule ann = module.getClass().getAnnotation(PlayerModule.class);
return ann == null ? 0 : ann.order();
}
/** 当前快照,一般给调试或管理接口。 */
public List<GeneralModule<?>> all() {
return modules;
}
/** 对所有「开了 tick」的模块跑一遍。 */
public void runTickAll(long runAt) {
for (GeneralModule<?> module : modules) {
if (module.isEnableTick()) {
module.runTick(runAt);
}
}
}
/** 依次存盘,谁失败谁自己处理,这里不中断链路。 */
public void saveAll() {
for (GeneralModule<?> module : modules) {
module.saveData();
}
}
/**
* 拼一份给人看的列表:类名、顺序、是否 tick,外加实现了 {@link ModuleMetricsProvider} 的指标。
*/
public List<Map<String, Object>> describeModules() {
List<Map<String, Object>> rows = new ArrayList<>();
for (GeneralModule<?> module : modules) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("class", module.getClass().getSimpleName());
PlayerModule ann = module.getClass().getAnnotation(PlayerModule.class);
row.put("order", ann == null ? 0 : ann.order());
row.put("enableTick", module.isEnableTick());
if (module instanceof ModuleMetricsProvider provider) {
row.putAll(provider.metrics());
}
rows.add(row);
}
return rows;
}
}
@@ -0,0 +1,17 @@
com\huangzj\base\cmd\CmdRpcDispatch.class
com\huangzj\base\cmd\GameCommand.class
com\huangzj\base\module\PlayerModule.class
com\huangzj\base\module\ModuleMetricsProvider.class
com\huangzj\base\cmd\GameCommandLookup.class
com\huangzj\base\cmd\CrossRpcInvoker.class
com\huangzj\base\spring\SpringGameCommandRegistry.class
com\huangzj\base\frame\NettyBufUtil.class
com\huangzj\base\module\AbsHostModule.class
com\huangzj\base\cmd\CommandContext.class
com\huangzj\base\module\GeneralModule.class
com\huangzj\base\spring\module\PlayerModuleRegistry.class
com\huangzj\base\frame\LengthPrefixedProtobuf.class
com\huangzj\base\cmd\Cmd.class
com\huangzj\base\cmd\AbstractTypedPbCommand.class
com\huangzj\base\cmd\CommandSource.class
com\huangzj\base\cmd\AbstractCrossToGamePbCommand.class
@@ -0,0 +1,17 @@
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\cmd\AbstractTypedPbCommand.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\frame\LengthPrefixedProtobuf.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\cmd\CommandSource.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\module\GeneralModule.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\module\AbsHostModule.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\cmd\CommandContext.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\cmd\CmdRpcDispatch.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\cmd\Cmd.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\spring\module\PlayerModuleRegistry.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\cmd\AbstractCrossToGamePbCommand.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\module\ModuleMetricsProvider.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\cmd\CrossRpcInvoker.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\cmd\GameCommandLookup.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\spring\SpringGameCommandRegistry.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\module\PlayerModule.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\frame\NettyBufUtil.java
F:\coding\comm-framework\BaseModule\src\main\java\com\huangzj\base\cmd\GameCommand.java