代码初始化
This commit is contained in:
@@ -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 辅助
|
||||
```
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* 处理一帧 RPC:request 里带好 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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17
@@ -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
|
||||
+17
@@ -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
|
||||
Reference in New Issue
Block a user