代码初始化
This commit is contained in:
@@ -0,0 +1 @@
|
||||
logs/
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+26
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="CrossModule" />
|
||||
<module name="ProtoBufModule" />
|
||||
<module name="ServerModule" />
|
||||
<module name="BaseModule" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="BaseModule" options="-parameters" />
|
||||
<module name="CrossModule" options="-parameters" />
|
||||
<module name="ProtoBufModule" options="-parameters" />
|
||||
<module name="ServerModule" options="-parameters" />
|
||||
<module name="comm-framework" options="" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+18
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="test@43.155.238.153" uuid="fb532063-34ed-447d-94fa-f31be2dd310a">
|
||||
<driver-ref>mongo</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<imported>true</imported>
|
||||
<jdbc-driver>com.dbschema.MongoJdbcDriver</jdbc-driver>
|
||||
<jdbc-url>mongodb://43.155.238.153:27017/test</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/BaseModule/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/CrossModule/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ProtoBufModule/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ServerModule/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+35
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central" />
|
||||
<option name="url" value="https://repo.huaweicloud.com/repository/maven/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="spring-milestones" />
|
||||
<option name="name" value="Spring Milestones" />
|
||||
<option name="url" value="https://repo.spring.io/milestone" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="aliyun-public" />
|
||||
<option name="name" value="aliyun-public" />
|
||||
<option name="url" value="https://maven.aliyun.com/repository/public" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="ms-17" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
Generated
+124
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Palette2">
|
||||
<group name="Swing">
|
||||
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||
</item>
|
||||
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||
<initial-values>
|
||||
<property name="text" value="Button" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="RadioButton" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="CheckBox" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="Label" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||
<preferred-size width="-1" height="20" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||
</item>
|
||||
</group>
|
||||
</component>
|
||||
</project>
|
||||
@@ -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
|
||||
@@ -0,0 +1,101 @@
|
||||
# CrossModule
|
||||
|
||||
**跨服独立进程**:仅启动 **一个 CmdRpc TCP 服务**(默认 **9200**),处理来源为 **`CommandSource.CROSS_TCP`** 的命令。
|
||||
区服通过 **`CrossTcpClient`**(在 ServerModule)作为 **TCP 客户端** 连到本进程。
|
||||
|
||||
## 进程职责
|
||||
|
||||
| 职责 | 说明 |
|
||||
|------|------|
|
||||
| 监听 TCP | `CrossTcpServer` 在 `ApplicationReadyEvent` 后 `bind(cross.tcp.port)` |
|
||||
| Pipeline | `CrossTcpChannelInitializer`:`LengthPrefixedProtobuf` 解码 + `CmdRpcTcpFrameHandler` |
|
||||
| 命令路由 | `CrossGameCommandRegistry` 只注册 `@Cmd(source=CROSS_TCP)` 的 Bean |
|
||||
| 链路安全(可选) | `CrossLinkAcceptProperties`:`cross.link.expected-token` 非空时校验注册 token |
|
||||
|
||||
## 网络拓扑
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Zone["ServerModule"]
|
||||
CC[CrossTcpClient]
|
||||
end
|
||||
subgraph Cross["CrossModule :9200"]
|
||||
XT[CrossTcpServer]
|
||||
PL[pipeline]
|
||||
REG[CrossGameCommandRegistry]
|
||||
end
|
||||
CC <-->|单 TCP 长连接 长度前缀+CmdRpcEnvelope| XT
|
||||
XT --> PL --> REG
|
||||
```
|
||||
|
||||
## 入站 Pipeline(与 BaseModule 一致)
|
||||
|
||||
```text
|
||||
Socket
|
||||
→ LengthFieldBasedFrameDecoder(由 LengthPrefixedProtobuf 配置)
|
||||
→ CmdRpcTcpFrameHandler
|
||||
→ CmdRpcChannelDispatch(linkSource = CROSS_TCP)
|
||||
→ GameCommand.execute
|
||||
→ 编码响应帧写回
|
||||
```
|
||||
|
||||
跨服侧 **`CrossRpcInvoker` 传 `null`**:跨服命令里 **不应** 再同步 `roundTrip` 回调区服(Demo 未实现跨服主动调区服)。
|
||||
|
||||
## 已实现的 `CROSS_TCP` 命令
|
||||
|
||||
### 业务:Service_2 / Method2
|
||||
|
||||
- **`CrossForwardCommand`**(`AbstractTypedPbCommand`)
|
||||
- 入参:`CrossForwardCts`
|
||||
- 出参:`CrossForwardStc`
|
||||
- Demo 逻辑:给 `trace` 加前缀并写入 `cross_time_ms`。
|
||||
|
||||
### 链路:Service_4(注册 / 心跳)
|
||||
|
||||
| method | 命令类 | 作用 |
|
||||
|--------|--------|------|
|
||||
| Register | `CrossLinkRegisterCommand` | 校验 `server_id`、可选 `expected-token` |
|
||||
| Heartbeat | `CrossLinkHeartbeatCommand` | 回显 `client_time_ms`,写入 `cross_time_ms` |
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Z as 区服 CrossTcpClient
|
||||
participant X as CrossModule
|
||||
Z->>X: Service_4 Register + link_seq + CrossLinkRegisterCts
|
||||
X-->>Z: 同 link_seq + CrossLinkRegisterStc(ok/message)
|
||||
loop 每 heartbeat-interval-ms
|
||||
Z->>X: Service_4 Heartbeat + link_seq
|
||||
X-->>Z: CrossLinkHeartbeatStc
|
||||
end
|
||||
Z->>X: Service_2 Method2 + link_seq + CrossForwardCts
|
||||
X-->>Z: CrossForwardStc
|
||||
```
|
||||
|
||||
## 配置(`src/main/resources/application.yml`)
|
||||
|
||||
```yaml
|
||||
cross:
|
||||
tcp:
|
||||
port: 9200 # Netty 监听端口
|
||||
link:
|
||||
expected-token: "" # 非空则须与区服 cross.token 一致
|
||||
app:
|
||||
logging:
|
||||
dir: logs/CrossModule
|
||||
```
|
||||
|
||||
## 启动
|
||||
|
||||
主类:`com.huangzj.cross.CrossModuleApplication`
|
||||
|
||||
建议 **先于区服** 启动,否则区服 `CrossTcpClient` 预热与心跳会失败(日志告警),待跨服起来后下次业务或心跳会重连。
|
||||
|
||||
## 日志
|
||||
|
||||
`logback-spring.xml`:控制台 + 按级别分文件(`info.log` / `error.log`),目录由 `app.logging.dir` 指定。
|
||||
|
||||
## 扩展跨服 RPC
|
||||
|
||||
1. 在 **ProtoBufModule** 增加 `service/method` 与消息体。
|
||||
2. 在本模块新增 `@Cmd(source=CROSS_TCP, ...)` 的 `GameCommand` 或 `AbstractTypedPbCommand`。
|
||||
3. 若需强类型自动解码,使用 **`AbstractTypedPbCommand`**;启动时 **`ProtoRegistryAutoConfigurer`** 会为跨服进程注册对应 `ProtoInfo`。
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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>CrossModule</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>CrossModule</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>huangzj</groupId>
|
||||
<artifactId>ProtoBufModule</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>huangzj</groupId>
|
||||
<artifactId>BaseModule</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>com.huangzj.cross.CrossModuleApplication</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.huangzj.cross;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
/**
|
||||
* 跨服独立进程:只起 TCP + 命令注册,记得先让它跑起来区服才能 proxy。
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = "com.huangzj")
|
||||
@ConfigurationPropertiesScan(basePackages = "com.huangzj")
|
||||
public class CrossModuleApplication {
|
||||
|
||||
/** 轻量进程,一般先于区服启动。 */
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CrossModuleApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.huangzj.cross.cmd;
|
||||
|
||||
import com.huangzj.base.cmd.AbstractTypedPbCommand;
|
||||
import com.huangzj.base.cmd.Cmd;
|
||||
import com.huangzj.base.cmd.CommandContext;
|
||||
import com.huangzj.base.cmd.CommandSource;
|
||||
import com.huangzj.cmd.gen.CrossForwardCts;
|
||||
import com.huangzj.cmd.gen.CrossForwardStc;
|
||||
import com.huangzj.cmd.gen.PbService;
|
||||
import com.huangzj.cmd.gen.Service2_Method;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** 跨服示例:{@link PbService#Service_2} + {@link Service2_Method#Method2}。 */
|
||||
@Component
|
||||
@Cmd(
|
||||
source = CommandSource.CROSS_TCP,
|
||||
serviceId = PbService.Service_2_VALUE,
|
||||
methodId = Service2_Method.Method2_VALUE)
|
||||
public class CrossForwardCommand extends AbstractTypedPbCommand<CrossForwardCts, CrossForwardStc.Builder> {
|
||||
|
||||
@Override
|
||||
protected void run(CommandContext ctx, CrossForwardCts cts, CrossForwardStc.Builder stcBuilder) {
|
||||
// Demo:给 trace 加前缀便于区分「跨服加工」;生产可在此落库、转发其它服等
|
||||
stcBuilder.setTrace("cross:" + cts.getTrace()).setCrossTimeMs(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.huangzj.cross.cmd;
|
||||
|
||||
import com.huangzj.base.cmd.CommandSource;
|
||||
import com.huangzj.base.spring.SpringGameCommandRegistry;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 跨服进程只认 {@link CommandSource#CROSS_TCP} 的命令,别的来源不会进这张表。
|
||||
*/
|
||||
@Component
|
||||
public class CrossGameCommandRegistry extends SpringGameCommandRegistry {
|
||||
|
||||
/** 启动时扫 Bean,只收集带 {@code @Cmd(source=CROSS_TCP)} 的实现。 */
|
||||
public CrossGameCommandRegistry(ApplicationContext applicationContext) {
|
||||
super(applicationContext, CommandSource.CROSS_TCP);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.huangzj.cross.cmd;
|
||||
|
||||
import com.huangzj.base.cmd.AbstractTypedPbCommand;
|
||||
import com.huangzj.base.cmd.Cmd;
|
||||
import com.huangzj.base.cmd.CommandContext;
|
||||
import com.huangzj.base.cmd.CommandSource;
|
||||
import com.huangzj.cmd.gen.CrossLinkHeartbeatCts;
|
||||
import com.huangzj.cmd.gen.CrossLinkHeartbeatStc;
|
||||
import com.huangzj.cmd.gen.PbService;
|
||||
import com.huangzj.cmd.gen.Service4_Method;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** 区服 ↔ 跨服长连接心跳(仅 {@link CommandSource#CROSS_TCP})。 */
|
||||
@Component
|
||||
@Cmd(
|
||||
source = CommandSource.CROSS_TCP,
|
||||
serviceId = PbService.Service_4_VALUE,
|
||||
methodId = Service4_Method.Heartbeat_VALUE)
|
||||
public class CrossLinkHeartbeatCommand extends AbstractTypedPbCommand<CrossLinkHeartbeatCts, CrossLinkHeartbeatStc.Builder> {
|
||||
|
||||
@Override
|
||||
protected void run(CommandContext ctx, CrossLinkHeartbeatCts cts, CrossLinkHeartbeatStc.Builder stcBuilder) {
|
||||
// 原样回显客户端时间,便于区服侧算 RTT;cross_time_ms 为跨服处理时刻
|
||||
stcBuilder.setClientTimeMs(cts.getClientTimeMs()).setCrossTimeMs(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.huangzj.cross.cmd;
|
||||
|
||||
import com.huangzj.base.cmd.AbstractTypedPbCommand;
|
||||
import com.huangzj.base.cmd.Cmd;
|
||||
import com.huangzj.base.cmd.CommandContext;
|
||||
import com.huangzj.base.cmd.CommandSource;
|
||||
import com.huangzj.cmd.gen.CrossLinkRegisterCts;
|
||||
import com.huangzj.cmd.gen.CrossLinkRegisterStc;
|
||||
import com.huangzj.cmd.gen.PbService;
|
||||
import com.huangzj.cmd.gen.Service4_Method;
|
||||
import com.huangzj.cross.config.CrossLinkAcceptProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 区服 TCP 连上跨服后的注册握手(仅 {@link CommandSource#CROSS_TCP})。
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Cmd(
|
||||
source = CommandSource.CROSS_TCP,
|
||||
serviceId = PbService.Service_4_VALUE,
|
||||
methodId = Service4_Method.Register_VALUE)
|
||||
public class CrossLinkRegisterCommand extends AbstractTypedPbCommand<CrossLinkRegisterCts, CrossLinkRegisterStc.Builder> {
|
||||
|
||||
private final CrossLinkAcceptProperties acceptProperties;
|
||||
|
||||
@Override
|
||||
protected void run(CommandContext ctx, CrossLinkRegisterCts cts, CrossLinkRegisterStc.Builder stcBuilder) {
|
||||
String expected = acceptProperties.getExpectedToken();
|
||||
// 跨服未配置 expected-token 时不校验,方便本地多区服联调;生产环境建议两边配置一致
|
||||
if (StringUtils.hasText(expected) && !expected.equals(cts.getToken())) {
|
||||
stcBuilder.setOk(false).setMessage("token mismatch");
|
||||
return;
|
||||
}
|
||||
// server_id 用于日志、路由扩展;空则拒绝,避免匿名连接占坑
|
||||
if (!StringUtils.hasText(cts.getServerId())) {
|
||||
stcBuilder.setOk(false).setMessage("server_id required");
|
||||
return;
|
||||
}
|
||||
stcBuilder.setOk(true).setMessage("ok " + cts.getServerId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.huangzj.cross.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 跨服侧:区服注册时可选的 token 校验(为空则不校验)。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "cross.link")
|
||||
public class CrossLinkAcceptProperties {
|
||||
|
||||
/** 非空时须与区服 {@code cross.token} 一致才允许注册。 */
|
||||
private String expectedToken = "";
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.huangzj.cross.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 跨服进程自己监听的 TCP 端口,长度前缀 + Protobuf 那一套。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "cross.tcp")
|
||||
public class CrossTcpProperties {
|
||||
|
||||
/** Netty 监听端口,长度前缀 + protobuf 帧。 */
|
||||
private int port;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.huangzj.cross.net;
|
||||
|
||||
import com.huangzj.base.cmd.CommandSource;
|
||||
import com.huangzj.base.cmd.ProtoRegistry;
|
||||
import com.huangzj.base.frame.LengthPrefixedProtobuf;
|
||||
import com.huangzj.base.net.CmdRpcTcpFrameHandler;
|
||||
import com.huangzj.cross.cmd.CrossGameCommandRegistry;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** 跨服 TCP:pipeline 上挂帧解码 + CmdRpc 业务 handler。 */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CrossTcpChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||
|
||||
private final CrossGameCommandRegistry crossGameCommandRegistry;
|
||||
private final ProtoRegistry protoRegistry;
|
||||
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
ch.pipeline().addLast(LengthPrefixedProtobuf.newTcpFrameDecoder());
|
||||
ch.pipeline()
|
||||
.addLast(
|
||||
new CmdRpcTcpFrameHandler(
|
||||
crossGameCommandRegistry, null, CommandSource.CROSS_TCP, protoRegistry));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.huangzj.cross.net;
|
||||
|
||||
import com.huangzj.cross.config.CrossTcpProperties;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import java.net.BindException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 跨服侧的 TCP 入口:Spring 起来之后绑端口,应用关掉时把线程组也收干净。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CrossTcpServer {
|
||||
|
||||
private final CrossTcpProperties properties;
|
||||
private final CrossTcpChannelInitializer crossTcpChannelInitializer;
|
||||
private EventLoopGroup bossGroup;
|
||||
private EventLoopGroup workerGroup;
|
||||
private Channel serverChannel;
|
||||
|
||||
/** 等 Spring 容器就绪再绑端口,避免 Bean 还没齐就开始接连接。 */
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void start() throws InterruptedException {
|
||||
bossGroup = new NioEventLoopGroup(1);
|
||||
workerGroup = new NioEventLoopGroup();
|
||||
try {
|
||||
ServerBootstrap b = new ServerBootstrap();
|
||||
b.group(bossGroup, workerGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.childHandler(crossTcpChannelInitializer);
|
||||
serverChannel = b.bind(properties.getPort()).sync().channel();
|
||||
log.info("Cross TCP {}", properties.getPort());
|
||||
} catch (Exception e) {
|
||||
if (findBindException(e) != null) {
|
||||
shutdownGroupsOnly();
|
||||
int port = properties.getPort();
|
||||
throw new IllegalStateException(
|
||||
"跨服 TCP 端口 "
|
||||
+ port
|
||||
+ " 已被占用(Address already in use)。请关闭占用该端口的进程,或在 application.yml 里修改 cross.tcp.port。"
|
||||
+ " Windows 可查:netstat -ano | findstr :" + port,
|
||||
e);
|
||||
}
|
||||
shutdownGroupsOnly();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static BindException findBindException(Throwable t) {
|
||||
while (t != null) {
|
||||
if (t instanceof BindException) {
|
||||
return (BindException) t;
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 绑定失败时尚未 assign serverChannel,只收掉线程组,避免泄漏。 */
|
||||
private void shutdownGroupsOnly() {
|
||||
if (bossGroup != null) {
|
||||
bossGroup.shutdownGracefully(0, 1, TimeUnit.SECONDS).syncUninterruptibly();
|
||||
bossGroup = null;
|
||||
}
|
||||
if (workerGroup != null) {
|
||||
workerGroup.shutdownGracefully(0, 1, TimeUnit.SECONDS).syncUninterruptibly();
|
||||
workerGroup = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 进程退出时把 channel 和线程组都关掉,别留一堆 Netty 线程 hanging。 */
|
||||
@PreDestroy
|
||||
public void stop() throws InterruptedException {
|
||||
if (serverChannel != null) {
|
||||
serverChannel.close().syncUninterruptibly();
|
||||
}
|
||||
if (bossGroup != null) {
|
||||
bossGroup.shutdownGracefully(0, 1, TimeUnit.SECONDS).syncUninterruptibly();
|
||||
}
|
||||
if (workerGroup != null) {
|
||||
workerGroup.shutdownGracefully(0, 1, TimeUnit.SECONDS).syncUninterruptibly();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# 开发环境
|
||||
cross:
|
||||
tcp:
|
||||
port: 9200
|
||||
link:
|
||||
expected-token: ""
|
||||
|
||||
app:
|
||||
logging:
|
||||
dir: logs/CrossModule
|
||||
@@ -0,0 +1,10 @@
|
||||
# 部署环境:监听端口、注册校验 token、日志目录建议走环境变量
|
||||
cross:
|
||||
tcp:
|
||||
port: ${CROSS_TCP_PORT:9200}
|
||||
link:
|
||||
expected-token: ${CROSS_LINK_EXPECTED_TOKEN:}
|
||||
|
||||
app:
|
||||
logging:
|
||||
dir: ${APP_LOG_DIR:/var/log/comm-framework/cross}
|
||||
@@ -0,0 +1,5 @@
|
||||
spring:
|
||||
application:
|
||||
name: cross-module
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 目录由本模块 application.yml 的 app.logging.dir 指定,与 ServerModule 互不干扰 -->
|
||||
<springProperty name="LOG_DIR" scope="context" source="app.logging.dir" defaultValue="logs/CrossModule"/>
|
||||
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
|
||||
|
||||
<!-- INFO、WARN:不含 ERROR,便于日常浏览 -->
|
||||
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_DIR}/info.log</file>
|
||||
<encoder>
|
||||
<pattern>${FILE_LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>INFO</level>
|
||||
</filter>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>ERROR</level>
|
||||
<onMatch>DENY</onMatch>
|
||||
<onMismatch>NEUTRAL</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_DIR}/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<maxHistory>14</maxHistory>
|
||||
<totalSizeCap>512MB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- ERROR 单独落盘,告警/排障只盯这个文件即可 -->
|
||||
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_DIR}/error.log</file>
|
||||
<encoder>
|
||||
<pattern>${FILE_LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>ERROR</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_DIR}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>512MB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- DEBUG 仅进控制台(若把 root 调到 DEBUG);需要落盘时在 application.yml 里单独配 logger + appender -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE_INFO"/>
|
||||
<appender-ref ref="FILE_ERROR"/>
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -0,0 +1,10 @@
|
||||
# 开发环境
|
||||
cross:
|
||||
tcp:
|
||||
port: 9200
|
||||
link:
|
||||
expected-token: ""
|
||||
|
||||
app:
|
||||
logging:
|
||||
dir: logs/CrossModule
|
||||
@@ -0,0 +1,10 @@
|
||||
# 部署环境:监听端口、注册校验 token、日志目录建议走环境变量
|
||||
cross:
|
||||
tcp:
|
||||
port: ${CROSS_TCP_PORT:9200}
|
||||
link:
|
||||
expected-token: ${CROSS_LINK_EXPECTED_TOKEN:}
|
||||
|
||||
app:
|
||||
logging:
|
||||
dir: ${APP_LOG_DIR:/var/log/comm-framework/cross}
|
||||
@@ -0,0 +1,5 @@
|
||||
spring:
|
||||
application:
|
||||
name: cross-module
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
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.
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 目录由本模块 application.yml 的 app.logging.dir 指定,与 ServerModule 互不干扰 -->
|
||||
<springProperty name="LOG_DIR" scope="context" source="app.logging.dir" defaultValue="logs/CrossModule"/>
|
||||
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
|
||||
|
||||
<!-- INFO、WARN:不含 ERROR,便于日常浏览 -->
|
||||
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_DIR}/info.log</file>
|
||||
<encoder>
|
||||
<pattern>${FILE_LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>INFO</level>
|
||||
</filter>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>ERROR</level>
|
||||
<onMatch>DENY</onMatch>
|
||||
<onMismatch>NEUTRAL</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_DIR}/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<maxHistory>14</maxHistory>
|
||||
<totalSizeCap>512MB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- ERROR 单独落盘,告警/排障只盯这个文件即可 -->
|
||||
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_DIR}/error.log</file>
|
||||
<encoder>
|
||||
<pattern>${FILE_LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>ERROR</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_DIR}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>512MB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- DEBUG 仅进控制台(若把 root 调到 DEBUG);需要落盘时在 application.yml 里单独配 logger + appender -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE_INFO"/>
|
||||
<appender-ref ref="FILE_ERROR"/>
|
||||
</root>
|
||||
</configuration>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
com\huangzj\cross\config\CrossTcpProperties.class
|
||||
com\huangzj\cross\net\CrossTcpServer$1.class
|
||||
com\huangzj\cross\cmd\CrossGameCommandRegistry.class
|
||||
com\huangzj\cross\CrossModuleApplication.class
|
||||
com\huangzj\cross\net\CrossTcpCommandHandler.class
|
||||
com\huangzj\cross\net\CrossTcpServer.class
|
||||
com\huangzj\cross\cmd\CrossForwardCommand.class
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
F:\coding\comm-framework\CrossModule\src\main\java\com\huangzj\cross\cmd\CrossGameCommandRegistry.java
|
||||
F:\coding\comm-framework\CrossModule\src\main\java\com\huangzj\cross\config\CrossTcpProperties.java
|
||||
F:\coding\comm-framework\CrossModule\src\main\java\com\huangzj\cross\CrossModuleApplication.java
|
||||
F:\coding\comm-framework\CrossModule\src\main\java\com\huangzj\cross\net\CrossTcpServer.java
|
||||
F:\coding\comm-framework\CrossModule\src\main\java\com\huangzj\cross\cmd\CrossForwardCommand.java
|
||||
F:\coding\comm-framework\CrossModule\src\main\java\com\huangzj\cross\net\CrossTcpCommandHandler.java
|
||||
@@ -0,0 +1,84 @@
|
||||
# ProtoBufModule
|
||||
|
||||
本模块承载 **RPC 与业务消息的二进制契约**:所有 TCP 链路上传输的业务体都通过此处定义的 `protobuf` 生成 Java 类(包名 `com.huangzj.cmd.gen`)。
|
||||
|
||||
## 职责
|
||||
|
||||
- 维护 `src/main/protobuf` 下的 `.proto` 源文件。
|
||||
- Maven `protobuf-maven-plugin` 在 `generate-sources` 阶段生成 Java 源码到 `target/generated-sources/protobuf/java`。
|
||||
- **其它模块**(BaseModule、ServerModule、CrossModule)仅依赖本模块打出的 jar,不重复拷贝 `.proto`。
|
||||
|
||||
## 目录结构(源文件)
|
||||
|
||||
```text
|
||||
src/main/protobuf/
|
||||
└── serverProto/
|
||||
├── cmd_rpc.proto # CmdRpcEnvelope、各 CTS/STC、Method 枚举
|
||||
└── pb_service.proto # PbService(Service_1 ~ Service_4)
|
||||
```
|
||||
|
||||
> 历史上独立的 `CrossProto` 已移除;跨服与区服共用 **`demo.cmd`** 包下的消息,通过 **service/method + CommandSource** 在 Java 侧区分语义。
|
||||
|
||||
## 网络层核心消息:`CmdRpcEnvelope`
|
||||
|
||||
```protobuf
|
||||
message CmdRpcEnvelope {
|
||||
uint32 service_id = 1;
|
||||
uint32 method_id = 2;
|
||||
bytes payload = 3; // 具体 CTS 的序列化字节
|
||||
int32 cross_ret = 4; // 预留/透传
|
||||
uint32 link_seq = 5; // 区服↔跨服长连接多路复用序号(客户端 TCP 可为 0)
|
||||
}
|
||||
```
|
||||
|
||||
### `link_seq` 的作用(与网络实现配合)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Z as 区服 CrossTcpClient
|
||||
participant X as 跨服 TCP
|
||||
Note over Z,X: 单条 TCP 上可能同时存在多帧在途
|
||||
Z->>X: 请求 A (link_seq=101)
|
||||
Z->>X: 请求 B (link_seq=102)
|
||||
X-->>Z: 响应 B (link_seq=102)
|
||||
X-->>Z: 响应 A (link_seq=101)
|
||||
```
|
||||
|
||||
- 区服在 **`CrossTcpClient`** 内为每次 `roundTrip` / 注册 / 心跳分配 **非 0** 的 `link_seq`。
|
||||
- 跨服命令通过 **`AbstractTypedPbCommand`** 回包时 **原样带回** 请求信封中的 `link_seq`,以便客户端把响应与 `CompletableFuture` 对应。
|
||||
|
||||
## 服务与方法枚举
|
||||
|
||||
| PbService | 用途 |
|
||||
|-----------|------|
|
||||
| Service_1 | 路径 1 示例(Echo) |
|
||||
| Service_2 | 跨服业务 Method2(`CrossForward*`) |
|
||||
| Service_3 | 路径 3 区服对客户端暴露的中转接口 |
|
||||
| Service_4 | **链路控制**:Register、Heartbeat(仅区服→跨服长连接上使用) |
|
||||
|
||||
`Service4_Method`:`Register`、`Heartbeat`,载荷分别为 `CrossLinkRegisterCts/Stc`、`CrossLinkHeartbeatCts/Stc`。
|
||||
|
||||
## 业务消息(与路径对应)
|
||||
|
||||
- **路径 1**:`CmdEchoCts` / `CmdEchoStc`。
|
||||
- **路径 2(客户端侧)**:`CrossProxyCts` / `CrossProxyStc`(区服内会转为 `CrossForward*` 再发往跨服)。
|
||||
- **路径 3**:客户端 payload 使用 **`CrossForwardCts`** 形态(与跨服 Service_2/Method2 一致),信封为 Service_3/Method3。
|
||||
- **跨服 Service_2/Method2**:`CrossForwardCts` / `CrossForwardStc`。
|
||||
|
||||
## 生成代码
|
||||
|
||||
```bash
|
||||
cd ProtoBufModule
|
||||
mvn -q generate-sources
|
||||
# 或使用 genpb.bat(Windows,会探测 Maven)
|
||||
```
|
||||
|
||||
生成物加入 `ProtoBufModule` jar,依赖方编译时自动可见。
|
||||
|
||||
## 扩展约定
|
||||
|
||||
新增 RPC 时建议:
|
||||
|
||||
1. 在 `pb_service.proto` 增加 `PbService` 枚举值(若新服务号)。
|
||||
2. 在 `cmd_rpc.proto` 增加 `ServiceX_Method` 与 CTS/STC `message`。
|
||||
3. 执行 `generate-sources` 后,在 **BaseModule** 侧通过 `@Cmd` + `AbstractTypedPbCommand` 或 `GameCommand` 实现逻辑,并由 **`ProtoRegistryAutoConfigurer`** 自动注册 `ProtoInfo`(强类型命令)。
|
||||
@@ -0,0 +1,21 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
set "POM=%~dp0pom.xml"
|
||||
set "MVN_CMD="
|
||||
if defined MAVEN_HOME if exist "%MAVEN_HOME%\bin\mvn.cmd" set "MVN_CMD=%MAVEN_HOME%\bin\mvn.cmd"
|
||||
if not defined MVN_CMD where mvn >nul 2>&1 && set "MVN_CMD=mvn"
|
||||
if not defined MVN_CMD for %%I in (
|
||||
"C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.7\plugins\maven\lib\maven3\bin\mvn.cmd"
|
||||
"C:\Program Files\JetBrains\IntelliJ IDEA 2023.3.8\plugins\maven\lib\maven3\bin\mvn.cmd"
|
||||
) do if exist %%~I set "MVN_CMD=%%~I"
|
||||
if not defined MVN_CMD (
|
||||
echo Set MAVEN_HOME or add mvn to PATH.
|
||||
exit /b 1
|
||||
)
|
||||
call "%MVN_CMD%" -q -f "%POM%" generate-sources
|
||||
set "ERR=%ERRORLEVEL%"
|
||||
if not "%ERR%"=="0" exit /b %ERR%
|
||||
echo Java sources: %~dp0target\generated-sources\protobuf\java
|
||||
endlocal
|
||||
exit /b 0
|
||||
@@ -0,0 +1,72 @@
|
||||
<?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>ProtoBufModule</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>ProtoBufModule</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<protoSourceRoot>${project.basedir}/src/main/protobuf</protoSourceRoot>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>add-sources</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>add-source</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>${project.build.directory}/generated-sources/protobuf/java</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,93 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package demo.cmd;
|
||||
|
||||
option java_package = "com.huangzj.cmd.gen";
|
||||
option java_multiple_files = true;
|
||||
|
||||
|
||||
enum Service1_Method{
|
||||
Default1 = 0;
|
||||
Method1 = 1;
|
||||
}
|
||||
|
||||
|
||||
enum Service2_Method{
|
||||
Default2 = 0;
|
||||
Method2 = 1;
|
||||
}
|
||||
|
||||
|
||||
enum Service3_Method{
|
||||
Default3 = 0;
|
||||
Method3 = 1;
|
||||
}
|
||||
|
||||
/** 区服 ↔ 跨服链路控制(注册、心跳),不经客户端 TCP。 */
|
||||
enum Service4_Method {
|
||||
Default4 = 0;
|
||||
Register = 1;
|
||||
Heartbeat = 2;
|
||||
}
|
||||
|
||||
service Service1Service {
|
||||
rpc GetUser (CmdEchoCts) returns (CmdEchoStc);
|
||||
}
|
||||
|
||||
|
||||
message CmdRpcEnvelope {
|
||||
uint32 service_id = 1;
|
||||
uint32 method_id = 2;
|
||||
bytes payload = 3;
|
||||
int32 cross_ret = 4;
|
||||
/** 区服↔跨服链路上对齐请求与响应;客户端 TCP 可保持默认 0。 */
|
||||
uint32 link_seq = 5;
|
||||
}
|
||||
|
||||
message CmdEchoCts {
|
||||
string text = 1;
|
||||
}
|
||||
|
||||
message CmdEchoStc {
|
||||
string text = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
message CrossForwardCts {
|
||||
string trace = 1;
|
||||
}
|
||||
|
||||
message CrossForwardStc {
|
||||
string trace = 1;
|
||||
int64 cross_time_ms = 2;
|
||||
}
|
||||
|
||||
message CrossProxyCts {
|
||||
string trace = 1;
|
||||
}
|
||||
|
||||
message CrossProxyStc {
|
||||
string trace = 1;
|
||||
int64 cross_time_ms = 2;
|
||||
/** 路径2:区服在跨服返回后再追加的说明,证明「经区服二次处理」。 */
|
||||
string server_note = 3;
|
||||
}
|
||||
|
||||
message CrossLinkRegisterCts {
|
||||
string server_id = 1;
|
||||
string token = 2;
|
||||
}
|
||||
|
||||
message CrossLinkRegisterStc {
|
||||
bool ok = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message CrossLinkHeartbeatCts {
|
||||
int64 client_time_ms = 1;
|
||||
}
|
||||
|
||||
message CrossLinkHeartbeatStc {
|
||||
int64 client_time_ms = 1;
|
||||
int64 cross_time_ms = 2;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package demo.cmd;
|
||||
|
||||
option java_package = "com.huangzj.cmd.gen";
|
||||
option java_multiple_files = true;
|
||||
|
||||
enum PbService {
|
||||
Default = 0;
|
||||
Service_1 = 1;
|
||||
Service_2 = 2;
|
||||
Service_3 = 3;
|
||||
Service_4 = 4;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package demo.crosslink;
|
||||
|
||||
option java_package = "com.huangzj.cross.gen";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message CrossPing {
|
||||
string trace = 1;
|
||||
int32 nonce = 2;
|
||||
}
|
||||
|
||||
message CrossPong {
|
||||
string trace = 1;
|
||||
int32 nonce = 2;
|
||||
int64 cross_time_ms = 3;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user