diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..b90040e
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,232 @@
+name: Java CI with Maven, Nacos, and Python
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ unit-tests:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Run Maven Tests
+ run: |
+ set -euo pipefail
+ mvn -B -ntp clean test
+
+ - name: Upload Surefire Reports
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: surefire-reports
+ path: |
+ **/target/surefire-reports/*.txt
+ **/target/surefire-reports/*.xml
+
+ go-client-check:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go_client/go.mod
+ cache-dependency-path: go_client/go.sum
+
+ - name: Verify Go Client Build
+ working-directory: go_client
+ run: |
+ set -euo pipefail
+ go mod download
+ go test ./...
+ go build -o /tmp/go-rpc-client main.go
+
+ integration-test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ needs: [unit-tests, go-client-check]
+
+ services:
+ nacos:
+ image: nacos/nacos-server:v2.2.3
+ env:
+ MODE: standalone
+ AUTH_ENABLE: false
+ ports:
+ - 8848:8848
+ - 9848:9848
+ # Wait for Nacos to be fully ready
+ options: >-
+ --name nacos-server
+ --health-cmd "curl -f http://localhost:8848/nacos/v1/console/health/readiness"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 10
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go_client/go.mod
+ cache-dependency-path: go_client/go.sum
+
+ - name: Install Python Dependencies
+ run: |
+ set -euo pipefail
+ python -m pip install --upgrade pip
+ pip install grpcio grpcio-tools protobuf
+
+ - name: Build Jars For Integration Test
+ run: |
+ set -euo pipefail
+ mvn -B -ntp -DskipTests clean package
+
+ - name: Wait For Nacos Ports
+ run: |
+ set -euo pipefail
+ echo "Waiting for Nacos HTTP API on 8848..."
+ timeout 60s bash -c 'until curl -sf http://localhost:8848/nacos/v1/console/health/readiness > /dev/null; do sleep 1; done'
+ echo "Waiting for Nacos gRPC port on 9848..."
+ timeout 60s bash -c 'until echo > /dev/tcp/localhost/9848; do sleep 1; done'
+ echo "Nacos 8848/9848 are both reachable."
+
+ - name: Start RPC Provider (Java Server)
+ run: |
+ set -euo pipefail
+ # Build classpath including project jars and dependencies
+ CLASSPATH="rpc-provider/target/rpc-provider-1.0-SNAPSHOT.jar:rpc-transport-netty/target/rpc-transport-netty-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-provider -am)"
+
+ echo "Starting Java Server..."
+ echo "Provider protocol override: rpc.protocol=grpc"
+ # Run in background with nohup
+ nohup java -Dlogback.configurationFile=rpc-core/src/main/resources/logback.xml -Drpc.protocol=grpc -cp "$CLASSPATH" com.xiaoyu.rpc.provider.ProviderApp > server.log 2>&1 &
+
+ # Wait for server port 8080 to be available
+ echo "Waiting for Java Server to start on port 8080..."
+ timeout 60s bash -c 'until echo > /dev/tcp/localhost/8080; do sleep 1; done'
+
+ echo "Java Server is running!"
+ # Optional: Print initial logs
+ head -n 20 server.log
+
+ - name: Wait For Service Registration In Nacos
+ run: |
+ set -euo pipefail
+ echo "Waiting for HelloService registration..."
+ if timeout 60s bash -c 'until grep -q "Service registered: com.xiaoyu.rpc.api.HelloService" server.log; do sleep 1; done'; then
+ echo "HelloService registration confirmed."
+ else
+ echo "Service registration wait timed out. Recent server logs:"
+ tail -n 120 server.log || true
+ exit 1
+ fi
+
+ - name: Run Multi-Client Chains In Parallel
+ run: |
+ set -euo pipefail
+ mkdir -p .ci-status
+
+ CLASSPATH="rpc-consumer/target/rpc-consumer-1.0-SNAPSHOT.jar:rpc-transport-netty/target/rpc-transport-netty-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-consumer -am)"
+
+ run_java() {
+ echo "Running Java Client (grpc)..."
+ if timeout 120s java -Dlogback.configurationFile=rpc-core/src/main/resources/logback.xml -Drpc.protocol=grpc -Drpc.serializer=protobuf -cp "$CLASSPATH" com.xiaoyu.rpc.consumer.ConsumerApp | tee java-client.log \
+ && grep -q "Result1:" java-client.log \
+ && grep -q "Result2:" java-client.log; then
+ echo "PASS" > .ci-status/java.status
+ else
+ echo "Java client chain failed or timed out."
+ echo "FAIL" > .ci-status/java.status
+ fi
+ }
+
+ run_python() {
+ echo "Running Python Client..."
+ if timeout 120s bash -c 'cd python_client && python client.py' | tee python-client.log \
+ && grep -q "Message: Success" python-client.log; then
+ echo "PASS" > .ci-status/python.status
+ else
+ echo "Python client chain failed or timed out."
+ echo "FAIL" > .ci-status/python.status
+ fi
+ }
+
+ run_go() {
+ echo "Running Go Client..."
+ if timeout 120s bash -c 'cd go_client && go run main.go' | tee go-client.log \
+ && grep -q "Message: Success" go-client.log; then
+ echo "PASS" > .ci-status/go.status
+ else
+ echo "Go client chain failed or timed out."
+ echo "FAIL" > .ci-status/go.status
+ fi
+ }
+
+ run_java &
+ PID_JAVA=$!
+ run_python &
+ PID_PY=$!
+ run_go &
+ PID_GO=$!
+
+ wait "${PID_JAVA}" || true
+ wait "${PID_PY}" || true
+ wait "${PID_GO}" || true
+
+ - name: Aggregate Multi-Client Results
+ run: |
+ set -euo pipefail
+ JAVA_RESULT=$(cat .ci-status/java.status 2>/dev/null || echo "FAIL")
+ PY_RESULT=$(cat .ci-status/python.status 2>/dev/null || echo "FAIL")
+ GO_RESULT=$(cat .ci-status/go.status 2>/dev/null || echo "FAIL")
+
+ echo "Multi-client chain summary:"
+ echo " Java : ${JAVA_RESULT}"
+ echo " Python : ${PY_RESULT}"
+ echo " Go : ${GO_RESULT}"
+
+ if [[ "${JAVA_RESULT}" != "PASS" || "${PY_RESULT}" != "PASS" || "${GO_RESULT}" != "PASS" ]]; then
+ echo "At least one client chain failed."
+ exit 1
+ fi
+ echo "All three client chains passed."
+
+ - name: Upload CI Logs
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: ci-logs
+ path: |
+ server.log
+ java-client.log
+ python-client.log
+ go-client.log
+ .ci-status/*.status
diff --git a/README.md b/README.md
index a80c050..27c797a 100644
--- a/README.md
+++ b/README.md
@@ -13,55 +13,107 @@
## 📖 Introduction
This project is a high-performance, pluggable RPC framework designed to demonstrate the convergence of standard protocols and dynamic invocation.
-Unlike traditional RPC frameworks that bind tightly to a single protocol or require strict code generation for every service, **XiaoYu RPC** features a unique **"Universal gRPC Adpater"**. It implements the standard gRPC protocol (HTTP/2 + Protobuf) but routes requests dynamically to Java service implementations. This allows you to:
+Unlike traditional RPC frameworks that bind tightly to a single protocol or require strict code generation for every service, **XiaoYu RPC** features a unique **"Universal gRPC Adapter"**. It implements the standard gRPC protocol (HTTP/2 + Protobuf) but routes requests dynamically to Java service implementations. This allows you to:
1. **Use standard gRPC clients** (like Python, Go, Node.js) to call your Java services directly.
2. **Retain Java's dynamic flexibility** (Reflection/ByteBuddy) without generating separate `.proto` service stubs for every business class.
+
+## 🏗️ Project Architecture
+
+The project is organized into the following modules to ensure separation of concerns and maintainability:
+
+| Module | Description |
+|--------|-------------|
+| **`rpc-api`** | Defines service interfaces. Shared between Provider and Consumer. |
+| **`rpc-common`** | Common utilities, Value Objects (`RpcRequest`, `RpcResponse`), and Protobuf definitions (`rpc_meta.proto`). |
+| **`rpc-core`** | The core framework implementation. Contains SPI interfaces, Dynamic Proxy, and Registry logic. **Netty-free**. |
+| **`rpc-transport-netty`** | The default transport implementation based on **Netty**. |
+| **`rpc-provider`** | Example provider application that implements and exports services. |
+| **`rpc-consumer`** | Example consumer application that imports and invokes services. |
+| **`rpc-spring-boot-starter`** | Spring Boot auto-configuration starter for provider/consumer integration. |
+| **`rpc-benchmark`** | Performance benchmarking module using JMH (Java Microbenchmark Harness). |
+| **`python_client`** | Python client implementation demonstrating cross-language gRPC interoperability. |
+| **`go_client`** | Go client implementation demonstrating cross-language gRPC interoperability. |
+
+### System Architecture Diagram
+
+```mermaid
+flowchart LR
+ subgraph External["External Systems"]
+ E1["python_client (grpc-python)"]
+ E2["go_client (grpc-go)"]
+ E3["Nacos Registry"]
+ end
+
+ subgraph Internal["Internal Components"]
+ subgraph Clients["Java Clients"]
+ C1["rpc-consumer"]
+ C2["Spring Boot App"]
+ end
+
+ subgraph Core["rpc-core (Microkernel)"]
+ P1["ProxyFactory (JDK/ByteBuddy)"]
+ P2["RpcClient / RpcServer"]
+ P3["ExtensionLoader (SPI)"]
+ end
+
+ subgraph Plugins["SPI Plugins"]
+ S1["Protocol: Netty / HTTP / HTTP2 / gRPC"]
+ S2["Serializer: Protobuf / Kryo / JSON / Java"]
+ S3["LoadBalancer: RoundRobin / Random"]
+ S4["Registry: Nacos / Local"]
+ S5["Transport: rpc-transport-netty"]
+ end
+
+ subgraph Provider["Service Provider"]
+ M1["rpc-provider"]
+ M2["HelloServiceImpl"]
+ end
+ end
+
+ A1["rpc-api (service interfaces)"]
+ A2["rpc-common (RpcRequest/RpcResponse + proto)"]
+
+ C1 --> P1
+ C2 --> P1
+ E1 -->|gRPC / HTTP2 + Protobuf| S1
+ E2 -->|gRPC / HTTP2 + Protobuf| S1
+ P1 --> P2
+ P2 --> S3
+ P2 --> S4
+ P2 --> S5
+ P2 --> S2
+ S5 --> S1
+ S1 --> M1
+ M1 --> M2
+ S4 <-->|service register/discover| E3
+ A1 -.shared API.-> C1
+ A1 -.shared API.-> M1
+ A2 -.shared model.-> P2
+ P3 -.loads.-> S1
+ P3 -.loads.-> S2
+ P3 -.loads.-> S3
+ P3 -.loads.-> S4
+ P3 -.loads.-> S5
+```
+
## ✨ Key Features
- **🔌 Plugin-based Architecture**: Leverages a custom SPI mechanism for maximum flexibility.
- **🤝 Universal gRPC Compatibility**: A custom-implemented `GrpcProtocol` layer that runs standard gRPC on HTTP/2, proven to interoperate with official `grpc-python` clients.
- **⚡ Dynamic-Static Hybrid**: Combines the performance of Protobuf serialization (with custom Type Wrappers) and the flexibility of Java dynamic proxies.
+- **🚀 Pluggable Transport**: Fully decoupled transport layer. Default implementation is `rpc-transport-netty`, but can be swapped for Tomcat/Socket.
- **📡 Multi-Protocol Support**: Choice of `Netty` (Custom), `HTTP/1.1`, or `gRPC` (HTTP/2) for communication.
- **⚡ High-Performance Proxy**: Uses **ByteBuddy** for dynamic proxy generation, optimized for Java 17+.
- **⚖️ Intelligent Load Balancing**: Includes `RoundRobin` and `Random` strategies.
-- **📦 Diverse Serialization**: Supports `Protobuf` (Enhanced with Scalar Wrappers), `Kryo`, `JSON`, and standard `Java` serialization.
+- **📦 Diverse Serialization**: Supports `Protobuf` (Enhanced with Scalar Wrappers), **Kryo** (Optimized), `JSON`, and standard `Java` serialization.
+- **🔄 Request Multiplexing**: True asynchronous request/response correlation using `request_id`, enabling a single connection to handle thousands of concurrent streams (especially for HTTP/2).
- **🔍 Service Discovery**: Integrated with **Nacos** for robust service registry and discovery.
---
-## 🔬 Technical Deep Dive: gRPC Protocol Implementation
-
-The core of XiaoYu RPC's interoperability lies in its custom implementation of the gRPC wire protocol over Netty's HTTP/2 stack.
-
-
-
-### 1. Wire Format (5-Byte Header)
-
-Every gRPC message is prefixed with a 5-byte header, handled directly in `GrpcServerHandler`:
-
-- **Compression Flag (1 Byte)**: `0` (Uncompressed) or `1` (Compressed).
-- **Message Length (4 Bytes)**: Big-endian integer specifying the length of the following Protobuf payload.
-- **Payload**: Standard Protobuf binary data, deserialized via `NativeProtobufSerializer`.
-
-### 2. Header Alignment
-
-Strict adherence to gRPC HTTP/2 headers ensures compatibility:
-
-- **:status**: `200` (HTTP level success)
-- **content-type**: `application/grpc` (Crucial for client recognition)
-- **te**: `trailers`
-
-### 3. Trailer & Status
-
-gRPC uses HTTP/2 Trailers to convey the final RPC status, distinct from the HTTP status code.
-
-- **HEADERS Frame (EndStream=true)**: Sent after the data payload.
-- **grpc-status**: `0` for OK, non-zero for errors.
-- **grpc-message**: Descriptive error message.
-
-## 🚀 Quick Start
+## Quick Start
### 1. Prerequisites (Nacos)
@@ -79,12 +131,15 @@ docker run --name nacos-standalone \
Execute the following commands to start the Java RPC Provider. This will build the project and register the `HelloService` to your local Nacos instance.
+The default `rpc.protocol` is `netty` for Java-to-Java Provider/Consumer calls.
+If you need Python/Go interoperability, switch to `grpc` in `rpc-core/src/main/resources/rpc-config.yaml`.
+
```bash
# 1. Build Project
mvn clean package -DskipTests
# 2. Start Provider
-java -cp rpc-provider/target/rpc-provider-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-provider -am) com.xiaoyu.rpc.provider.ProviderApp
+java -cp rpc-provider/target/rpc-provider-1.0-SNAPSHOT.jar:rpc-transport-netty/target/rpc-transport-netty-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-provider -am) com.xiaoyu.rpc.provider.ProviderApp
```
### 3. Run the Consumer
@@ -92,17 +147,56 @@ java -cp rpc-provider/target/rpc-provider-1.0-SNAPSHOT.jar:rpc-core/target/rpc-c
Execute the `ConsumerApp` in the `rpc-consumer` module to make calls to the provider.
```bash
-java -cp rpc-consumer/target/rpc-consumer-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-consumer -am) com.xiaoyu.rpc.consumer.ConsumerApp
+java -cp rpc-consumer/target/rpc-consumer-1.0-SNAPSHOT.jar:rpc-transport-netty/target/rpc-transport-netty-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-consumer -am) com.xiaoyu.rpc.consumer.ConsumerApp
```
-### 4. Running Integration Tests
+### 4. Running Tests
+
+**Unit Tests**:
+Run the comprehensive unit test suite covering SPI, serializers, load balancers, and protocols:
+
+```bash
+mvn test -pl rpc-core,rpc-transport-netty
+```
-To run the full integration test suite:
+**Integration Tests**:
+Run the full integration test suite:
```bash
-mvn test -pl rpc-consumer -Dtest=FullIntegrationTest
+mvn test -pl rpc-consumer -am -Dtest=FullIntegrationTest
```
+### 5. Performance & Benchmark Results
+
+XiaoYu RPC is designed for high performance. Below are the verified results from our JMH benchmark suite.
+
+#### 5.1 Protocol Performance (Throughput & Latency)
+Tested with **8 concurrent threads** on local loopback (127.0.0.1).
+
+| Protocol | Throughput (ops/ms) | Latency (ms/op) | Characteristics |
+| :--- | :--- | :--- | :--- |
+| **Netty (Custom)** | **84.245** | **0.093** | **Champion.** Pure binary, minimal overhead. |
+| **HTTP/1.1** | 76.853 | 0.104 | Robust, but limited by serial processing per connection. |
+| **HTTP/2** | 58.847 | 0.137 | **Multiplexing Power.** Higher overhead but stable under load. |
+
+> [!TIP]
+> **Why HTTP/2?**
+> While HTTP/1.1 is slightly faster in zero-latency local loopback tests due to its simplicity, HTTP/2's **Multiplexing** allows it to handle massive concurrent requests over a single connection without Head-of-Line (HoL) blocking, which is critical for real-world distributed systems.
+
+#### 5.2 Serialization Efficiency
+Comparison of processing a standard POJO (`RpcRequest`).
+
+| Serializer | Throughput (ops/us) | Latency (us/op) | Payload Size |
+| :--- | :---: | :---: | :---: |
+| **Protobuf** | **34.429** | **0.029** | **65 bytes** |
+| **Kryo (Optimized)** | 11.932 | 0.066 | 68 bytes |
+| **JSON** | 2.050 | 0.497 | 231 bytes |
+| **Java** | 1.102 | 0.895 | 652 bytes |
+
+**Key Insights:**
+- **Protobuf vs. Java**: Protobuf is **77x faster** and **10x smaller** than standard Java serialization.
+- **Binary vs. Text**: Kryo (Binary) provides **6x higher throughput** than JSON (Text) for complex objects due to Varint compression and omission of field names.
+
---
## 🛠️ Configuration
@@ -111,16 +205,59 @@ Configure the framework via `rpc-core/src/main/resources/rpc-config.yaml`.
```yaml
rpc:
- protocol: "http2" # Protocol: netty, http, http2
- server-host: 127.0.0.1
+ transport: "netty" # Transport: netty
+ protocol: "netty" # Protocol: netty, http, http2
+ server-host: "127.0.0.1"
server-port: 8080
registry: "nacos" # Registry: nacos, local
registry-address: "127.0.0.1:8848"
- serializer: KRYO # Serializer: PROTOBUF, KRYO, JAVA, JSON
- proxy: bytebuddy # Proxy: jdk, bytebuddy
+ serializer: "protobuf" # Serializer: protobuf, kryo, java, json
+ proxy: "bytebuddy" # Proxy: jdk, bytebuddy
load-balancer: roundrobin # Load Balancer: roundrobin, random
+ max-message-size: 8388608 # 8MB
```
+> [!IMPORTANT]
+> Default is `netty` for a faster local Java-to-Java path.
+> `grpc` now also works with `rpc-consumer` (`RpcClientProxy`) and can be used for both Java and Python/Go interoperability.
+
+## 🔌 SPI Design & Ecosystem
+
+XiaoYu RPC adheres to the **Microkernel Architecture**, where the core (`rpc-core`) only provides the lifecycle management and SPI (Service Provider Interface) definitions, while all specific functionalities are implemented as plugins. This design ensures the framework is highly extensible, lightweight, and follows the **Open-Closed Principle**.
+
+### 🧩 Core Extension Points
+
+We strictly define interfaces to decouple every major component:
+
+| Interface | Description | Default Impl | Purpose |
+|-----------|-------------|--------------|---------|
+| **`Transport`** | Abstraction of network communication. Decouples the underlying I/O framework. | `NettyTransport` | Allow switching between Netty, Tomcat, or Socket without changing core logic. |
+| **`Protocol`** | Message protocol definition. Controls how bytes are framed and processed. | `NettyProtocol` | Support multiple protocols (Custom RPC, gRPC, HTTP) on the same port. |
+| **`Serializer`** | Object serialization strategy. | `ProtoBuf` | Balance performance (Protobuf/Kryo) vs Compatibility (JSON/Java). |
+| **`LoadBalancer`** | Client-side load balancing strategy. | `RoundRobin` | Distribute traffic evenly or randomly to providers. |
+| **`ServiceRegistry`** | Service registration and discovery. | `Nacos`, `Local` | Decouple from specific registry backend (swap Nacos for Zookeeper/Consul easily). |
+| **`ProxyFactory`** | Dynamic proxy generation strategy. | `ByteBuddy` | Optimization for different JDK versions (ByteBuddy works best on Java 17+). |
+
+### 🛠️ ExtensionLoader
+
+We implemented a powerful loading mechanism similar to Dubbo's `ExtensionLoader`. It scans `META-INF/rpc/` for configuration files and loads implementation classes lazily by name.
+
+### How to Add a New Extension
+
+1. **Implement the Interface**: Create a class that implements the target SPI interface (e.g., `Serializer`).
+2. **Create SPI Configuration File**:
+ - Create a file in `src/main/resources/META-INF/rpc/`
+ - Filename must match the fully qualified interface name (e.g., `com.xiaoyu.rpc.common.serialization.Serializer`).
+3. **Register the Implementation**: Add a key-value pair to the file:
+ ```properties
+ my-serializer=com.example.MyCustomSerializer
+ ```
+4. **Use It**: update `rpc-config.yaml`:
+ ```yaml
+ rpc:
+ serializer: my-serializer
+ ```
+
## ❓ FAQ
**Q: Why ByteBuddy?**
@@ -129,29 +266,98 @@ A: CGLIB is problematic on Java 17+ due to deep reflection restrictions. ByteBud
**Q: Connection Timeout/Refusal?**
A: Ensure Nacos is running and the ports `8848` and `9848` are accessible. Check your `rpc-config.yaml` for correct host/port settings.
+**Q: How to switch to Local Registry for testing?**
+A: Set `registry: "local"` in `rpc-config.yaml`. This bypasses Nacos and uses an in-memory map, useful for unit tests or offline development.
+
+
+**Q: Encountering "No Transport Found" error?**
+A: Make sure you have included `rpc-transport-netty` (or custom transport module) in your runtime dependencies. `rpc-core` does not include a transport implementation by default to ensure modularity.
+
---
-## 🐍 Cross-Language gRPC Support (Python)
+## 🌱 Spring Boot Integration
-This framework supports interoperability with standard gRPC clients (e.g., Python), allowing non-Java clients to invoke services hosted by the RPC framework.
+A dedicated Spring Boot Starter is available: `rpc-spring-boot-starter`.
+
+### Dependency
+
+```xml
+
+ com.xiaoyu.rpc
+ rpc-spring-boot-starter
+ 1.0-SNAPSHOT
+
+```
+
+### Provider Example
+
+```java
+@RpcService
+public class HelloServiceImpl implements HelloService {
+ @Override
+ public String sayHello(String name) {
+ return "Hello, " + name;
+ }
+}
+```
+
+### Consumer Example
+
+```java
+@RestController
+public class HelloController {
+ @RpcReference
+ private HelloService helloService;
+
+ @GetMapping("/hello")
+ public String hello(@RequestParam String name) {
+ return helloService.sayHello(name);
+ }
+}
+```
+
+### Configuration (`application.yml`)
+
+```yaml
+rpc:
+ server-port: 8080
+ registry: nacos
+ registry-address: 127.0.0.1:8848
+ serializer: kryo
+ server-enabled: true # Set to false for consumer-only apps
+```
+
+---
+
+## 🌐 Multi-Language gRPC Support (Python & Go)
+
+This framework supports interoperability with standard gRPC clients (e.g., Python, Go), allowing non-Java clients to invoke services hosted by the RPC framework.
### Features
- **Standard gRPC Protocol**: Implements standard HTTP/2 transport compatible with widespread gRPC libraries (via `grpc-io`).
- **Protobuf Serialization**: Supports standard Protobuf `Empty`, `StringValue`, `Int32Value`, etc., via wrapper types for seamless data exchange.
-- **Nacos Integation**: Services registered in Nacos can be discovered and invoked.
+- **Nacos Integration**: Services registered in Nacos can be discovered and invoked.
### Usage Guide
-1. **Configure Java Server**:
- Update `rpc-config.yaml` to enable `grpc` protocol and `protobuf` serialization:
+1. **Configure Java Server (gRPC Mode)**:
+ Update `rpc-core/src/main/resources/rpc-config.yaml` as follows (copy-paste ready):
```yaml
rpc:
+ transport: "netty"
protocol: "grpc"
- serializer: "protobuf"
+ server-host: "127.0.0.1"
+ server-port: 8080
registry: "nacos"
+ registry-address: "127.0.0.1:8848"
+ serializer: "protobuf"
+ proxy: "bytebuddy"
+ load-balancer: "roundrobin"
+ max-message-size: 8388608
```
+ After multi-language tests, switch `protocol` back to `netty` (or `http`/`http2`) for Java-to-Java calls.
2. **Start the Java Provider (gRPC Mode)**:
Run the following commands to build the project and start the server:
@@ -160,32 +366,76 @@ This framework supports interoperability with standard gRPC clients (e.g., Pytho
mvn clean package -DskipTests
# Run the Provider
- java -cp rpc-provider/target/rpc-provider-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-provider -am) com.xiaoyu.rpc.provider.ProviderApp
+ java -cp rpc-provider/target/rpc-provider-1.0-SNAPSHOT.jar:rpc-transport-netty/target/rpc-transport-netty-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-provider -am) com.xiaoyu.rpc.provider.ProviderApp
```
3. **Run the Python Client**:
- Navigate to the `python_client` directory and set up the environment:
+ Refer to [`python_client/client.py`](python_client/client.py) for details.
```bash
cd python_client
+ # ... (existing steps)
+ python3 client.py
+ ```
- # Create and valid virtual environment
- python3 -m venv venv
- source venv/bin/activate
-
- # Install dependencies
- pip install grpcio grpcio-tools protobuf
+4. **Run the Go Client**:
+ Navigate to the `go_client` directory and run:
- # Run the client
- python3 client.py
+ ```bash
+ cd go_client
+ go run main.go
```
**Expected Output**:
```text
+ Sending RpcRequest: interface=com.xiaoyu.rpc.api.HelloService, method=sayHello, param=World
RpcResponse received:
- Data: Hello, World! (from Multi-Module Netty Server)
+ Data: Hello, World!
Message: Success
```
+## 🔄 Continuous Integration & Delivery (CI/CD)
+
+To ensure system reliability and code quality, this project integrates a robust CI/CD pipeline using **GitHub Actions**. This pipeline automatically validates the build process and runs integration tests upon every push and pull request.
+
+**Key Workflows:**
+- **Automated Testing**: Runs unit and integration tests to verify RPC functionality.
+- **Service Verification**: Launches Nacos, the Java Provider, and Python Client in a containerized environment to test cross-language interoperability.
+- **Build Status**: Provides immediate feedback on code health via GitHub Actions.
+
+
+
+
+---
+
+## 🔬 Technical Deep Dive: gRPC Protocol Implementation
+
+The core of XiaoYu RPC's interoperability lies in its custom implementation of the gRPC wire protocol over Netty's HTTP/2 stack.
+
+
+
+### 1. Wire Format (5-Byte Header)
+
+Every gRPC message is prefixed with a 5-byte header, handled directly in `GrpcServerHandler`:
+
+- **Compression Flag (1 Byte)**: `0` (Uncompressed) or `1` (Compressed).
+- **Message Length (4 Bytes)**: Big-endian integer specifying the length of the following Protobuf payload.
+- **Payload**: Standard Protobuf binary data, deserialized via `NativeProtobufSerializer`.
+
+### 2. Header Alignment
+
+Strict adherence to gRPC HTTP/2 headers ensures compatibility:
+
+- **:status**: `200` (HTTP level success)
+- **content-type**: `application/grpc` (Crucial for client recognition)
+- **te**: `trailers`
+
+### 3. Trailer & Status
+
+gRPC uses HTTP/2 Trailers to convey the final RPC status, distinct from the HTTP status code.
+
+- **HEADERS Frame (EndStream=true)**: Sent after the data payload.
+- **grpc-status**: `0` for OK, non-zero for errors.
+- **grpc-message**: Descriptive error message.
---
diff --git a/README_ZH.md b/README_ZH.md
new file mode 100644
index 0000000..6f7beaa
--- /dev/null
+++ b/README_ZH.md
@@ -0,0 +1,464 @@
+# 🚀 XiaoYu RPC Framework
+
+> 基于 **Netty**, **Nacos**, 和 **ByteBuddy** 构建的轻量级、高性能、可扩展 RPC 框架。
+> 支持多种协议,包括 **HTTP/2**, **HTTP/1.1**, 以及自定义 **Netty** 协议。
+
+
+
+
+
+
+---
+
+## 📖 简介
+
+本项目是一个旨在演示标准协议与动态调用融合的高性能插件化 RPC 框架。
+与传统强绑定单一协议或需要为每个服务生成代码的 RPC 框架不同,**XiaoYu RPC** 独创了 **"通用 gRPC 适配器 (Universal gRPC Adapter)"**。它实现了标准的 gRPC 协议 (HTTP/2 + Protobuf),但能够将请求动态路由到 Java 服务实现。这使得您可以:
+
+1. **直接使用标准 gRPC 客户端** (如 Python, Go, Node.js) 调用您的 Java 服务。
+2. **保留 Java 的动态灵活性** (反射/ByteBuddy),无需为每个业务类生成单独的 `.proto` Stub 代码。
+
+## 🏗️ 项目架构
+
+项目采用模块化设计,确保关注点分离和可维护性:
+
+| 模块 | 描述 |
+|--------|-------------|
+| **`rpc-api`** | 定义服务接口。由服务提供者(Provider)和消费者(Consumer)共享。 |
+| **`rpc-common`** | 通用工具类、值对象 (`RpcRequest`, `RpcResponse`) 及 Protobuf 定义 (`rpc_meta.proto`)。 |
+| **`rpc-core`** | 框架核心实现。包含 SPI 接口定义、动态代理和注册中心逻辑。**完全不含 Netty 依赖**。 |
+| **`rpc-transport-netty`** | 基于 **Netty** 实现的默认传输层模块。 |
+| **`rpc-provider`** | 示例服务提供者应用,用于实现并暴露服务。 |
+| **`rpc-consumer`** | 示例服务消费者应用,用于引入并调用服务。 |
+| **`rpc-spring-boot-starter`** | Spring Boot 自动配置 Starter,便于集成 Provider/Consumer。 |
+| **`rpc-benchmark`** | 基于 JMH (Java Microbenchmark Harness) 的性能基准测试模块。 |
+| **`python_client`** | Python 客户端实现,用于演示跨语言 gRPC 互操作性。 |
+| **`go_client`** | Go 客户端实现,用于演示跨语言 gRPC 互操作性。 |
+
+### 系统架构图
+
+```mermaid
+flowchart LR
+ subgraph External["外部系统"]
+ E1["python_client (grpc-python)"]
+ E2["go_client (grpc-go)"]
+ E3["Nacos 注册中心"]
+ end
+
+ subgraph Internal["内部组件"]
+ subgraph Clients["Java 客户端"]
+ C1["rpc-consumer"]
+ C2["Spring Boot 应用"]
+ end
+
+ subgraph Core["rpc-core (微内核)"]
+ P1["ProxyFactory (JDK/ByteBuddy)"]
+ P2["RpcClient / RpcServer"]
+ P3["ExtensionLoader (SPI)"]
+ end
+
+ subgraph Plugins["SPI 插件层"]
+ S1["Protocol: Netty / HTTP / HTTP2 / gRPC"]
+ S2["Serializer: Protobuf / Kryo / JSON / Java"]
+ S3["LoadBalancer: RoundRobin / Random"]
+ S4["Registry: Nacos / Local"]
+ S5["Transport: rpc-transport-netty"]
+ end
+
+ subgraph Provider["服务提供端"]
+ M1["rpc-provider"]
+ M2["HelloServiceImpl"]
+ end
+ end
+
+ A1["rpc-api (服务接口)"]
+ A2["rpc-common (请求响应模型 + proto)"]
+
+ C1 --> P1
+ C2 --> P1
+ E1 -->|gRPC / HTTP2 + Protobuf| S1
+ E2 -->|gRPC / HTTP2 + Protobuf| S1
+ P1 --> P2
+ P2 --> S3
+ P2 --> S4
+ P2 --> S5
+ P2 --> S2
+ S5 --> S1
+ S1 --> M1
+ M1 --> M2
+ S4 <-->|服务注册/发现| E3
+ A1 -.共享接口.-> C1
+ A1 -.共享接口.-> M1
+ A2 -.共享模型.-> P2
+ P3 -.加载.-> S1
+ P3 -.加载.-> S2
+ P3 -.加载.-> S3
+ P3 -.加载.-> S4
+ P3 -.加载.-> S5
+```
+
+## ✨ 核心特性
+
+- **🔌 插件化架构**: 采用自定义 SPI 机制,实现最大程度的灵活性。
+- **🤝 通用 gRPC 兼容性**: 自研 `GrpcProtocol` 层,在 HTTP/2 上运行标准 gRPC 协议,实测可与官方 `grpc-python` 客户端完美互通。
+- **⚡ 动静混合模式**: 结合了 Protobuf 序列化(配合自定义类型包装器)的高性能和 Java 动态代理的灵活性。
+- **🚀 可插拔传输层**: 传输层完全解耦。默认实现为 `rpc-transport-netty`,但可无缝替换为 Tomcat 或 Socket 实现。
+- **📡 多协议支持**: 支持 `Netty` (自定义), `HTTP/1.1`, 或 `gRPC` (HTTP/2) 通信协议。
+- **⚡ 高性能代理**: 使用 **ByteBuddy** 生成动态代理,针对 Java 17+ 进行了优化。
+- **⚖️ 智能负载均衡**: 内置 `RoundRobin` (轮询) 和 `Random` (随机) 策略。
+- **📦 多样化序列化**: 支持 `Protobuf` (增强版), **Kryo** (针对 POJO 优化), `JSON`, 以及标准 `Java` 序列化。
+- **🔄 请求多路复用**: 真正的异步请求/响应关联,通过 `request_id` 实现单条连接处理数千个并发流(尤其在 HTTP/2 下性能卓越)。
+- **🔍 服务发现**: 集成 **Nacos** 实现健壮的服务注册与发现。
+
+---
+
+## 🔌 SPI 设计与生态
+
+XiaoYu RPC 遵循 **微内核架构 (Microkernel Architecture)**,核心模块 (`rpc-core`) 仅提供生命周期管理和 SPI (Service Provider Interface) 定义,所有具体功能均作为插件实现。这种设计确保了框架的高度可扩展性和轻量化,并遵循 **开闭原则 (Open-Closed Principle)**。
+
+### 🧩 核心扩展点
+
+我们要严格定义接口以解耦每个主要组件:
+
+| 接口 | 描述 | 默认实现 | 设计目的 |
+|-----------|-------------|--------------|---------|
+| **`Transport`** | 网络通信抽象。解耦底层 I/O 框架。 | `NettyTransport` | 允许在不更改核心逻辑的情况下切换 Netty, Tomcat, 或 Socket。 |
+| **`Protocol`** | 消息协议定义。控制字节流的帧处理方式。 | `NettyProtocol` | 支持在同一端口上运行多种协议 (自定义 RPC, gRPC, HTTP)。 |
+| **`Serializer`** | 对象序列化策略。 | `ProtoBuf` | 平衡性能 (Protobuf/Kryo) 与兼容性 (JSON/Java)。 |
+| **`LoadBalancer`** | 客户端负载均衡策略。 | `RoundRobin` | 均匀或随机地将流量分发给服务提供者。 |
+| **`ServiceRegistry`** | 服务注册与发现。 | `Nacos`, `Local` | 解耦具体的注册中心后端 (可轻松替换为 Zookeeper/Consul)。 |
+| **`ProxyFactory`** | 动态代理生成策略。 | `ByteBuddy` | 针对不同 JDK 版本的优化 (ByteBuddy 在 Java 17+ 表现更佳)。 |
+
+### 🛠️ ExtensionLoader
+
+我们实现了一套类似于 Dubbo 的强大加载机制 `ExtensionLoader`。它会扫描 `META-INF/rpc/` 目录下的配置文件,并按需懒加载实现类。
+
+### 如何添加新扩展
+
+1. **实现接口**: 创建一个类实现目标 SPI 接口 (例如 `Serializer`)。
+2. **创建 SPI 配置文件**:
+ * 在 `src/main/resources/META-INF/rpc/` 目录下创建文件。
+ * 文件名必须与接口的全限定名一致 (例如 `com.xiaoyu.rpc.common.serialization.Serializer`)。
+3. **注册实现类**: 在文件中添加键值对:
+ ```properties
+ my-serializer=com.example.MyCustomSerializer
+ ```
+4. **使用扩展**: 更新 `rpc-config.yaml`:
+ ```yaml
+ rpc:
+ serializer: my-serializer
+ ```
+
+---
+
+## 🚀 快速开始
+
+### 1. 前置条件 (Nacos)
+
+使用 Docker 启动 Nacos:
+
+```bash
+docker run --name nacos-standalone \
+ -e MODE=standalone \
+ -p 8848:8848 \
+ -p 9848:9848 \
+ -d nacos/nacos-server:v2.3.1-slim
+```
+
+### 2. 运行 Provider
+
+执行以下命令启动 Java RPC Provider。这将编译项目并将 `HelloService` 注册到本地 Nacos 实例。
+
+默认 `rpc.protocol` 已设置为 `netty`,用于 Java-to-Java 的 Provider/Consumer 调用。
+若需要 Python/Go 跨语言互通,请在 `rpc-core/src/main/resources/rpc-config.yaml` 中手动切换到 `grpc`。
+
+```bash
+# 1. 构建项目
+mvn clean package -DskipTests
+
+# 2. 启动 Provider
+java -cp rpc-provider/target/rpc-provider-1.0-SNAPSHOT.jar:rpc-transport-netty/target/rpc-transport-netty-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-provider -am) com.xiaoyu.rpc.provider.ProviderApp
+```
+
+### 3. 运行 Consumer
+
+在 `rpc-consumer` 模块中执行 `ConsumerApp` 发起调用。
+
+```bash
+java -cp rpc-consumer/target/rpc-consumer-1.0-SNAPSHOT.jar:rpc-transport-netty/target/rpc-transport-netty-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-consumer -am) com.xiaoyu.rpc.consumer.ConsumerApp
+```
+
+### 4. 运行测试
+
+**单元测试**:
+运行包含 SPI、序列化器、负载均衡器和协议的全套单元测试:
+
+```bash
+mvn test -pl rpc-core,rpc-transport-netty
+```
+
+**集成测试**:
+运行完整的集成测试套件:
+
+```bash
+mvn test -pl rpc-consumer -am -Dtest=FullIntegrationTest
+```
+
+### 5. 基准测试与性能结果
+
+XiaoYu RPC 专注于极致性能。以下是使用 **JMH** 测得的真实数据(基于 8 线程并发,本地回路 127.0.0.1 压测)。
+
+#### 5.1 协议性能对比(吞吐量与延迟)
+
+| 通信协议 | 吞吐量 (ops/ms) | 平均延迟 (ms/op) | 性能简评 |
+| :--- | :--- | :--- | :--- |
+| **Netty (自定义)** | **84.245** | **0.093** | **性能冠军。** 纯二进制协议,开销极小。 |
+| **HTTP/1.1** | 76.853 | 0.104 | 表现稳健,但在单连接高并发下受限于串行处理。 |
+| **HTTP/2** | 58.847 | 0.137 | **多路复用利器。** 虽然协议头较重,但支持高并发流。 |
+
+> [!TIP]
+> **为什么多路复用很重要?**
+> 在零延迟的本地测试中,简单的 HTTP/1.1 略快;但在真实生产环境下,由于网络波动和高延迟,HTTP/2 通过消除**队头阻塞 (HoL)** 能显著提升系统吞吐量和稳定性。
+
+#### 5.2 序列化效率对比
+针对标准 POJO 对象 (`RpcRequest`) 的处理能力。
+
+| 序列化器 | 吞吐量 (ops/us) | 延迟 (us/op) | 报文大小 |
+| :--- | :---: | :---: | :---: |
+| **Protobuf** | **34.429** | **0.029** | **65 bytes** |
+| **Kryo (已优化)** | 11.932 | 0.066 | 68 bytes |
+| **JSON** | 2.050 | 0.497 | 231 bytes |
+| **Java** | 1.102 | 0.895 | 652 bytes |
+
+**核心洞察:**
+- **Protobuf vs. Java**: Protobuf 的处理速度比 Java 原生序列化快 **77 倍**,且体积缩小了 **10 倍**。
+- **二进制 vs. 文本**: 在处理复杂 POJO 时,二进制协议 (Kryo/Protobuf) 的吞吐量比文本协议 (JSON) 高出约 **6 倍**,这归功于 Varint 压缩和去除字段名存储。
+
+---
+
+## 🛠️ 配置手册
+
+通过 `rpc-core/src/main/resources/rpc-config.yaml` 配置框架。
+
+```yaml
+rpc:
+ transport: "netty" # 传输层: netty
+ protocol: "netty" # 协议: netty, http, http2
+ server-host: "127.0.0.1"
+ server-port: 8080
+ registry: "nacos" # 注册中心: nacos, local
+ registry-address: "127.0.0.1:8848"
+ serializer: "protobuf" # 序列化器: protobuf, kryo, java, json
+ proxy: "bytebuddy" # 代理方式: jdk, bytebuddy
+ load-balancer: roundrobin # 负载均衡: roundrobin, random
+ max-message-size: 8388608 # 8MB
+```
+
+> [!IMPORTANT]
+> 默认使用 `netty`,适合本地 Java-to-Java 的高性能链路。
+> 现在 `grpc` 也支持 `rpc-consumer`(`RpcClientProxy`)调用,可同时用于 Java 侧和 Python/Go 互操作。
+
+---
+
+## ❓ 常见问题 (FAQ)
+
+**Q: 为什么选择 ByteBuddy?**
+A: CGLIB 在 Java 17+ 上由于深层反射限制存在问题。ByteBuddy 是目前字节码操作的行业标准。
+
+**Q: 连接超时或被拒绝 (Connection Refused)?**
+A: 请确保 Nacos 已运行且端口 `8848` 和 `9848` 可访问。检查 `rpc-config.yaml` 主机/端口配置是否正确。
+
+**Q: 如何切换到本地注册中心进行测试?**
+A: 在 `rpc-config.yaml` 中设置 `registry: "local"`。这将绕过 Nacos,使用内存 Map 进行服务注册,非常适合单元测试或无网开发。
+
+
+
+**Q: 遇到 "No Transport Found" 错误?**
+A: 请确保你在运行时依赖中引入了 `rpc-transport-netty`(或其他传输模块)。为了保证轻量和解耦,`rpc-core` 默认不包含传输层实现。
+
+---
+
+## 🌱 Spring Boot 集成
+
+我们提供了一个专用的 Spring Boot Starter: `rpc-spring-boot-starter`。
+
+### 依赖引入
+
+```xml
+
+ com.xiaoyu.rpc
+ rpc-spring-boot-starter
+ 1.0-SNAPSHOT
+
+```
+
+### 服务提供者示例
+
+```java
+@RpcService
+public class HelloServiceImpl implements HelloService {
+ @Override
+ public String sayHello(String name) {
+ return "Hello, " + name;
+ }
+}
+```
+
+### 服务消费者示例
+
+```java
+@RestController
+public class HelloController {
+ @RpcReference
+ private HelloService helloService;
+
+ @GetMapping("/hello")
+ public String hello(@RequestParam String name) {
+ return helloService.sayHello(name);
+ }
+}
+```
+
+### 配置 (`application.yml`)
+
+```yaml
+rpc:
+ server-port: 8080
+ registry: nacos
+ registry-address: 127.0.0.1:8848
+ serializer: kryo
+ server-enabled: true # 对于纯消费者应用,设置为 false
+```
+
+---
+
+## 🌐 多语言 gRPC 支持 (Python & Go)
+
+本框架支持与标准 gRPC 客户端(如 Python, Go)互操作,允许非 Java 客户端调用本框架托管的服务。
+
+### 特性
+
+- **标准 gRPC 协议**: 实现了兼容主流 gRPC 库(如 `grpc-io`)的标准 HTTP/2 传输层。
+- **Protobuf 序列化**: 通过包装类型支持标准 Protobuf `Empty`, `StringValue`, `Int32Value` 等的数据交换。
+- **Nacos 集成**: 注册在 Nacos 中的服务可以被发现和调用。
+
+### 使用指南
+
+1. **配置 Java 服务端 (gRPC 模式)**:
+ 将 `rpc-core/src/main/resources/rpc-config.yaml` 调整为以下配置(可直接覆盖):
+
+ ```yaml
+ rpc:
+ transport: "netty"
+ protocol: "grpc"
+ server-host: "127.0.0.1"
+ server-port: 8080
+ registry: "nacos"
+ registry-address: "127.0.0.1:8848"
+ serializer: "protobuf"
+ proxy: "bytebuddy"
+ load-balancer: "roundrobin"
+ max-message-size: 8388608
+ ```
+ 完成多语言联调后,如需恢复 Java-to-Java 调用,请将 `protocol` 改回 `netty`(或 `http`/`http2`)。
+
+2. **启动 Java Provider (gRPC 模式)**:
+
+ ```bash
+ # 构建项目 (跳过测试以加速)
+ mvn clean package -DskipTests
+
+ # 启动 Provider
+ java -cp rpc-provider/target/rpc-provider-1.0-SNAPSHOT.jar:rpc-transport-netty/target/rpc-transport-netty-1.0-SNAPSHOT.jar:rpc-core/target/rpc-core-1.0-SNAPSHOT.jar:rpc-common/target/rpc-common-1.0-SNAPSHOT.jar:rpc-api/target/rpc-api-1.0-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout -pl rpc-provider -am) com.xiaoyu.rpc.provider.ProviderApp
+ ```
+
+3. **运行 Python 客户端**:
+ 详情请参考 [`python_client/client.py`](python_client/client.py)。
+
+ ```bash
+ cd python_client
+
+ # 创建并激活虚拟环境
+ python3 -m venv venv
+ source venv/bin/activate
+
+ # 安装依赖
+ pip install grpcio grpcio-tools protobuf
+
+ # 运行客户端
+ python3 client.py
+ ```
+
+ **预期输出**:
+
+ ```text
+ RpcResponse received:
+ Data: Hello, World! (from Multi-Module Netty Server)
+ Message: Success
+ ```
+
+4. **运行 Go 客户端**:
+ 进入 `go_client` 目录并运行:
+
+ ```bash
+ cd go_client
+ go run main.go
+ ```
+
+ **预期输出**:
+
+ ```text
+ Sending RpcRequest: interface=com.xiaoyu.rpc.api.HelloService, method=sayHello, param=World
+ RpcResponse received:
+ Data: Hello, World!
+ Message: Success
+ ```
+
+## 🔄 持续集成与交付 (CI/CD)
+
+为了确保系统可靠性和代码质量,本项目集成了一套基于 **GitHub Actions** 的健壮 CI/CD 流水线。
+
+**关键工作流:**
+- **自动化测试**: 运行单元测试和集成测试以验证 RPC 功能。
+- **服务验证**: 在容器化环境中启动 Nacos, Java Provider, 和 Python Client 以测试跨语言互操作性。
+- **构建状态**: 通过 GitHub Actions 提供代码健康度的即时反馈。
+
+
+
+---
+
+## 🔬 技术深度解析: gRPC 协议实现
+
+XiaoYu RPC 互操作性的核心在于基于 Netty HTTP/2 栈自定义实现的 gRPC 传输协议。
+
+
+
+### 1. 报文格式 (5字节头部)
+
+每个 gRPC 消息都以 5 字节的头部开始,由 `GrpcServerHandler` 直接处理:
+
+- **压缩标志 (1 字节)**: `0` (未压缩) 或 `1` (压缩)。
+- **消息长度 (4 字节)**: 大端序整数,指定后续 Protobuf 载荷的长度。
+- **载荷**: 标准 Protobuf 二进制数据,通过 `NativeProtobufSerializer` 反序列化。
+
+### 2. 头部对齐 (Header Alignment)
+
+严格遵守 gRPC HTTP/2 头部规范以确保兼容性:
+
+- **:status**: `200` (HTTP 层面的成功)
+- **content-type**: `application/grpc` (客户端识别的关键)
+- **te**: `trailers`
+
+### 3. Trailer 与状态码
+
+gRPC 使用 HTTP/2 Trailers 来传递最终的 RPC 状态,这与 HTTP 状态码是区分开的。
+
+- **HEADERS 帧 (EndStream=true)**: 在数据载荷之后发送。
+- **grpc-status**: `0` 表示 OK,非零表示错误。
+- **grpc-message**: 描述性错误信息。
+
+---
+
+## 🤝 贡献
+
+欢迎贡献代码!请随时提交 Issue 或 Pull Request 以改进框架。
diff --git a/docs/images/image.png b/docs/images/image.png
new file mode 100644
index 0000000..5d9c706
Binary files /dev/null and b/docs/images/image.png differ
diff --git a/go_client/go.mod b/go_client/go.mod
new file mode 100644
index 0000000..697a3e7
--- /dev/null
+++ b/go_client/go.mod
@@ -0,0 +1,17 @@
+module go_client
+
+go 1.25.6
+
+require (
+ google.golang.org/grpc v1.70.0
+ google.golang.org/protobuf v1.36.11
+)
+
+require (
+ go.opentelemetry.io/otel v1.34.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
+ golang.org/x/net v0.38.0 // indirect
+ golang.org/x/sys v0.31.0 // indirect
+ golang.org/x/text v0.23.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
+)
diff --git a/go_client/go.sum b/go_client/go.sum
new file mode 100644
index 0000000..29fdc61
--- /dev/null
+++ b/go_client/go.sum
@@ -0,0 +1,34 @@
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
+google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
diff --git a/go_client/main.go b/go_client/main.go
new file mode 100644
index 0000000..2f4b1b3
--- /dev/null
+++ b/go_client/main.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "go_client/pb"
+
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+func main() {
+ // Connect to the Java gRPC server
+ addr := "localhost:8080"
+ conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf("did not connect: %v", err)
+ }
+ defer conn.Close()
+
+ client := pb.NewGrpcServiceClient(conn)
+
+ // Wrap the parameter in a Protobuf StringValue
+ param := wrapperspb.String( "World")
+ paramBytes, err := proto.Marshal(param)
+ if err != nil {
+ log.Fatalf("failed to marshal param: %v", err)
+ }
+
+ // Prepare the RPC request
+ req := &pb.RpcRequest{
+ InterfaceName: "com.xiaoyu.rpc.api.HelloService",
+ MethodName: "sayHello",
+ ParamTypes: []string{"java.lang.String"},
+ Parameters: [][]byte{paramBytes},
+ RequestId: fmt.Sprintf("go-req-%d", time.Now().UnixNano()),
+ }
+
+ // Set a timeout for the call
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ defer cancel()
+
+ // Call the handle method
+ fmt.Printf("Sending RpcRequest: interface=%s, method=%s, param=%s\n", req.InterfaceName, req.MethodName, "World")
+ resp, err := client.Handle(ctx, req)
+ if err != nil {
+ log.Fatalf("could not call handle: %v", err)
+ }
+
+ fmt.Println("RpcResponse received:")
+ if resp.Message == "Success" {
+ resultVal := &wrapperspb.StringValue{}
+ if err := proto.Unmarshal(resp.Data, resultVal); err != nil {
+ log.Printf("failed to unmarshal data: %v", err)
+ fmt.Printf("Data (Raw): %v\n", resp.Data)
+ } else {
+ fmt.Printf("Data: %s\n", resultVal.Value)
+ }
+ } else {
+ fmt.Printf("Data (Raw): %v\n", resp.Data)
+ }
+ fmt.Printf("Message: %s\n", resp.Message)
+ fmt.Printf("RequestID: %s\n", resp.RequestId)
+}
diff --git a/go_client/pb/rpc_meta.pb.go b/go_client/pb/rpc_meta.pb.go
new file mode 100644
index 0000000..e5c6cae
--- /dev/null
+++ b/go_client/pb/rpc_meta.pb.go
@@ -0,0 +1,249 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.11
+// protoc v6.33.0
+// source: rpc_meta.proto
+
+package pb
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// 3. 对应你的 RpcRequest
+type RpcRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // 对应 String interfaceName
+ InterfaceName string `protobuf:"bytes,1,opt,name=interface_name,json=interfaceName,proto3" json:"interface_name,omitempty"`
+ // 对应 String methodName
+ MethodName string `protobuf:"bytes,2,opt,name=method_name,json=methodName,proto3" json:"method_name,omitempty"`
+ // 对应 Class>[] paramTypes
+ // Proto存不了Class对象,只能存全类名(String),比如 "java.lang.String"
+ ParamTypes []string `protobuf:"bytes,3,rep,name=param_types,json=paramTypes,proto3" json:"param_types,omitempty"`
+ // 对应 Object[] parameters
+ // Proto存不了Object,必须存成二进制(bytes)。
+ // 这里用 repeated 表示数组
+ Parameters [][]byte `protobuf:"bytes,4,rep,name=parameters,proto3" json:"parameters,omitempty"`
+ // 请求ID,用于多路复用
+ RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RpcRequest) Reset() {
+ *x = RpcRequest{}
+ mi := &file_rpc_meta_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RpcRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RpcRequest) ProtoMessage() {}
+
+func (x *RpcRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_rpc_meta_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RpcRequest.ProtoReflect.Descriptor instead.
+func (*RpcRequest) Descriptor() ([]byte, []int) {
+ return file_rpc_meta_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *RpcRequest) GetInterfaceName() string {
+ if x != nil {
+ return x.InterfaceName
+ }
+ return ""
+}
+
+func (x *RpcRequest) GetMethodName() string {
+ if x != nil {
+ return x.MethodName
+ }
+ return ""
+}
+
+func (x *RpcRequest) GetParamTypes() []string {
+ if x != nil {
+ return x.ParamTypes
+ }
+ return nil
+}
+
+func (x *RpcRequest) GetParameters() [][]byte {
+ if x != nil {
+ return x.Parameters
+ }
+ return nil
+}
+
+func (x *RpcRequest) GetRequestId() string {
+ if x != nil {
+ return x.RequestId
+ }
+ return ""
+}
+
+// 4. 对应你的 RpcResponse
+type RpcResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // 对应 Object data
+ // 同样无法存Object,只能存序列化后的二进制
+ Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
+ // 对应 String message
+ Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
+ // 请求ID,用于多路复用
+ RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RpcResponse) Reset() {
+ *x = RpcResponse{}
+ mi := &file_rpc_meta_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RpcResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RpcResponse) ProtoMessage() {}
+
+func (x *RpcResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_rpc_meta_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RpcResponse.ProtoReflect.Descriptor instead.
+func (*RpcResponse) Descriptor() ([]byte, []int) {
+ return file_rpc_meta_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *RpcResponse) GetData() []byte {
+ if x != nil {
+ return x.Data
+ }
+ return nil
+}
+
+func (x *RpcResponse) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+func (x *RpcResponse) GetRequestId() string {
+ if x != nil {
+ return x.RequestId
+ }
+ return ""
+}
+
+var File_rpc_meta_proto protoreflect.FileDescriptor
+
+const file_rpc_meta_proto_rawDesc = "" +
+ "\n" +
+ "\x0erpc_meta.proto\"\xb4\x01\n" +
+ "\n" +
+ "RpcRequest\x12%\n" +
+ "\x0einterface_name\x18\x01 \x01(\tR\rinterfaceName\x12\x1f\n" +
+ "\vmethod_name\x18\x02 \x01(\tR\n" +
+ "methodName\x12\x1f\n" +
+ "\vparam_types\x18\x03 \x03(\tR\n" +
+ "paramTypes\x12\x1e\n" +
+ "\n" +
+ "parameters\x18\x04 \x03(\fR\n" +
+ "parameters\x12\x1d\n" +
+ "\n" +
+ "request_id\x18\x05 \x01(\tR\trequestId\"Z\n" +
+ "\vRpcResponse\x12\x12\n" +
+ "\x04data\x18\x01 \x01(\fR\x04data\x12\x18\n" +
+ "\amessage\x18\x02 \x01(\tR\amessage\x12\x1d\n" +
+ "\n" +
+ "request_id\x18\x03 \x01(\tR\trequestId22\n" +
+ "\vGrpcService\x12#\n" +
+ "\x06handle\x12\v.RpcRequest\x1a\f.RpcResponseB*\n" +
+ "\x18com.xiaoyu.rpc.common.voP\x01Z\fgo_client/pbb\x06proto3"
+
+var (
+ file_rpc_meta_proto_rawDescOnce sync.Once
+ file_rpc_meta_proto_rawDescData []byte
+)
+
+func file_rpc_meta_proto_rawDescGZIP() []byte {
+ file_rpc_meta_proto_rawDescOnce.Do(func() {
+ file_rpc_meta_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_rpc_meta_proto_rawDesc), len(file_rpc_meta_proto_rawDesc)))
+ })
+ return file_rpc_meta_proto_rawDescData
+}
+
+var file_rpc_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_rpc_meta_proto_goTypes = []any{
+ (*RpcRequest)(nil), // 0: RpcRequest
+ (*RpcResponse)(nil), // 1: RpcResponse
+}
+var file_rpc_meta_proto_depIdxs = []int32{
+ 0, // 0: GrpcService.handle:input_type -> RpcRequest
+ 1, // 1: GrpcService.handle:output_type -> RpcResponse
+ 1, // [1:2] is the sub-list for method output_type
+ 0, // [0:1] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_rpc_meta_proto_init() }
+func file_rpc_meta_proto_init() {
+ if File_rpc_meta_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_meta_proto_rawDesc), len(file_rpc_meta_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_rpc_meta_proto_goTypes,
+ DependencyIndexes: file_rpc_meta_proto_depIdxs,
+ MessageInfos: file_rpc_meta_proto_msgTypes,
+ }.Build()
+ File_rpc_meta_proto = out.File
+ file_rpc_meta_proto_goTypes = nil
+ file_rpc_meta_proto_depIdxs = nil
+}
diff --git a/go_client/pb/rpc_meta_grpc.pb.go b/go_client/pb/rpc_meta_grpc.pb.go
new file mode 100644
index 0000000..cb68918
--- /dev/null
+++ b/go_client/pb/rpc_meta_grpc.pb.go
@@ -0,0 +1,121 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.6.0
+// - protoc v6.33.0
+// source: rpc_meta.proto
+
+package pb
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ GrpcService_Handle_FullMethodName = "/GrpcService/handle"
+)
+
+// GrpcServiceClient is the client API for GrpcService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type GrpcServiceClient interface {
+ Handle(ctx context.Context, in *RpcRequest, opts ...grpc.CallOption) (*RpcResponse, error)
+}
+
+type grpcServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewGrpcServiceClient(cc grpc.ClientConnInterface) GrpcServiceClient {
+ return &grpcServiceClient{cc}
+}
+
+func (c *grpcServiceClient) Handle(ctx context.Context, in *RpcRequest, opts ...grpc.CallOption) (*RpcResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(RpcResponse)
+ err := c.cc.Invoke(ctx, GrpcService_Handle_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// GrpcServiceServer is the server API for GrpcService service.
+// All implementations must embed UnimplementedGrpcServiceServer
+// for forward compatibility.
+type GrpcServiceServer interface {
+ Handle(context.Context, *RpcRequest) (*RpcResponse, error)
+ mustEmbedUnimplementedGrpcServiceServer()
+}
+
+// UnimplementedGrpcServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedGrpcServiceServer struct{}
+
+func (UnimplementedGrpcServiceServer) Handle(context.Context, *RpcRequest) (*RpcResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method Handle not implemented")
+}
+func (UnimplementedGrpcServiceServer) mustEmbedUnimplementedGrpcServiceServer() {}
+func (UnimplementedGrpcServiceServer) testEmbeddedByValue() {}
+
+// UnsafeGrpcServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to GrpcServiceServer will
+// result in compilation errors.
+type UnsafeGrpcServiceServer interface {
+ mustEmbedUnimplementedGrpcServiceServer()
+}
+
+func RegisterGrpcServiceServer(s grpc.ServiceRegistrar, srv GrpcServiceServer) {
+ // If the following call panics, it indicates UnimplementedGrpcServiceServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&GrpcService_ServiceDesc, srv)
+}
+
+func _GrpcService_Handle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RpcRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(GrpcServiceServer).Handle(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: GrpcService_Handle_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(GrpcServiceServer).Handle(ctx, req.(*RpcRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// GrpcService_ServiceDesc is the grpc.ServiceDesc for GrpcService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var GrpcService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "GrpcService",
+ HandlerType: (*GrpcServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "handle",
+ Handler: _GrpcService_Handle_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "rpc_meta.proto",
+}
diff --git a/go_client/proto/rpc_meta.proto b/go_client/proto/rpc_meta.proto
new file mode 100644
index 0000000..6596c78
--- /dev/null
+++ b/go_client/proto/rpc_meta.proto
@@ -0,0 +1,45 @@
+syntax = "proto3";
+
+option java_package = "com.xiaoyu.rpc.common.vo"; // 生成 Java 类的包名
+
+// 拆分生成多个 Java 文件,避免全部消息挤在一个类里
+option java_multiple_files = true;
+option go_package = "go_client/pb";
+
+// 请求对象
+message RpcRequest {
+ // 对应 String interfaceName
+ string interface_name = 1;
+
+ // 对应 String methodName
+ string method_name = 2;
+
+ // 对应 Class>[] paramTypes
+ // Proto存不了Class对象,只能存全类名(String),比如 "java.lang.String"
+ repeated string param_types = 3;
+
+ // 对应 Object[] parameters
+ // Proto存不了Object,必须存成二进制(bytes)。
+ // 这里用 repeated 表示数组
+ repeated bytes parameters = 4;
+
+ // 请求ID,用于多路复用
+ string request_id = 5;
+}
+
+// 响应对象
+message RpcResponse {
+ // 对应 Object data
+ // 同样无法存Object,只能存序列化后的二进制
+ bytes data = 1;
+
+ // 对应 String message
+ string message = 2;
+
+ // 请求ID,用于多路复用
+ string request_id = 3;
+}
+
+service GrpcService {
+ rpc handle (RpcRequest) returns (RpcResponse);
+}
diff --git a/go_client/run_client.sh b/go_client/run_client.sh
new file mode 100644
index 0000000..c0045e5
--- /dev/null
+++ b/go_client/run_client.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+# Ensure dependencies are installed
+go mod tidy
+
+# Run the Go client
+go run main.go
diff --git a/grpc-demo.iml b/grpc-demo.iml
index 883f51f..0d3a756 100644
--- a/grpc-demo.iml
+++ b/grpc-demo.iml
@@ -2,8 +2,8 @@
-
+
diff --git a/pom.xml b/pom.xml
index 8c5bb09..7645c9e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,7 +1,6 @@
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.xiaoyu.rpc
@@ -13,8 +12,11 @@
rpc-common
rpc-api
rpc-core
+ rpc-transport-netty
+ rpc-spring-boot-starter
rpc-provider
rpc-consumer
+ rpc-benchmark
@@ -22,7 +24,7 @@
17
UTF-8
4.1.86.Final
- 1.18.42
+ 1.18.36
1.5.20
5.6.2
3.24.0
@@ -116,7 +118,7 @@
grpc-stub
1.58.0
-
+
org.apache.tomcat
annotations-api
6.0.53
@@ -133,15 +135,14 @@
1.7.1
-
+
org.apache.maven.plugins
maven-compiler-plugin
3.11.0
- 17
- 17
+ 17
org.projectlombok
diff --git a/rpc-benchmark/pom.xml b/rpc-benchmark/pom.xml
new file mode 100644
index 0000000..92c1a25
--- /dev/null
+++ b/rpc-benchmark/pom.xml
@@ -0,0 +1,105 @@
+
+
+
+ com.xiaoyu.rpc
+ grpc-demo
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ rpc-benchmark
+
+
+ 1.37
+
+
+
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+ provided
+
+
+
+ com.xiaoyu.rpc
+ rpc-core
+ ${project.version}
+
+
+ com.xiaoyu.rpc
+ rpc-api
+ ${project.version}
+
+
+ com.xiaoyu.rpc
+ rpc-transport-netty
+ ${project.version}
+
+
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 17
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.1
+
+
+ package
+
+ shade
+
+
+ benchmarks
+
+
+ org.openjdk.jmh.Main
+
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rpc-benchmark/src/main/java/com/xiaoyu/rpc/benchmark/ProtocolBenchmark.java b/rpc-benchmark/src/main/java/com/xiaoyu/rpc/benchmark/ProtocolBenchmark.java
new file mode 100644
index 0000000..8e0baeb
--- /dev/null
+++ b/rpc-benchmark/src/main/java/com/xiaoyu/rpc/benchmark/ProtocolBenchmark.java
@@ -0,0 +1,118 @@
+package com.xiaoyu.rpc.benchmark;
+
+import com.google.protobuf.ByteString;
+import com.xiaoyu.rpc.api.HelloService;
+import com.xiaoyu.rpc.common.extension.ExtensionLoader;
+import com.xiaoyu.rpc.common.serialization.Serializer;
+import com.xiaoyu.rpc.common.vo.RpcRequest;
+import com.xiaoyu.rpc.common.vo.RpcResponse;
+import com.xiaoyu.rpc.core.config.RpcConfig;
+import com.xiaoyu.rpc.core.server.ServiceRepository;
+import com.xiaoyu.rpc.core.transport.TransportClient;
+import com.xiaoyu.rpc.core.transport.netty.NettyTransportClient;
+import com.xiaoyu.rpc.core.transport.netty.NettyTransportServer;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import java.lang.reflect.Field;
+import java.net.InetSocketAddress;
+import java.util.concurrent.TimeUnit;
+
+@BenchmarkMode({ Mode.Throughput, Mode.AverageTime })
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Benchmark)
+@Fork(value = 1, warmups = 0)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Threads(8)
+public class ProtocolBenchmark {
+
+ @Param({ "netty", "http", "http2" })
+ private String protocol;
+
+ private NettyTransportServer server;
+ private TransportClient client;
+ private InetSocketAddress address;
+ private RpcRequest request;
+ private int port = 9091;
+
+ @Setup
+ public void setup() throws Exception {
+ // Use a random port to avoid conflicts (Address already in use / TIME_WAIT)
+ this.port = 10000 + new java.util.Random().nextInt(50000);
+
+ // 通过反射覆盖本次基准测试的协议配置
+ RpcConfig config = RpcConfig.getInstance();
+ Field protocolField = RpcConfig.class.getDeclaredField("protocol");
+ protocolField.setAccessible(true);
+ protocolField.set(config, protocol);
+
+ // Ensure Serializer is set to something known, e.g., "java" for arg
+ // serialization
+ Field serializerField = RpcConfig.class.getDeclaredField("serializerType");
+ serializerField.setAccessible(true);
+ serializerField.set(config, "java");
+
+ // 注册测试服务实现
+ ServiceRepository.registerService(HelloService.class.getName(), new HelloServiceImpl());
+
+ // 启动服务端
+ server = new NettyTransportServer(port);
+ Thread serverThread = new Thread(() -> {
+ try {
+ server.start();
+ } catch (Exception e) {
+ e.printStackTrace(); // Log server startup errors
+ }
+ });
+ serverThread.setDaemon(true);
+ serverThread.start();
+
+ // Wait for server to start
+ TimeUnit.SECONDS.sleep(2);
+
+ // 初始化客户端
+ client = new NettyTransportClient();
+ address = new InetSocketAddress("127.0.0.1", port);
+
+ // 组装测试请求
+ Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("java");
+ byte[] argBytes = serializer.serialize("Benchmark");
+
+ request = RpcRequest.newBuilder()
+ .setInterfaceName(HelloService.class.getName())
+ .setMethodName("sayHello")
+ .addParamTypes("java.lang.String")
+ .addParameters(ByteString.copyFrom(argBytes))
+ .build();
+ }
+
+ @TearDown
+ public void teardown() {
+ if (server != null) {
+ server.stop();
+ }
+ }
+
+ @Benchmark
+ public Object benchmarkCall() {
+ return client.sendRequest(request, address);
+ }
+
+ public static void main(String[] args) throws RunnerException {
+ Options opt = new OptionsBuilder()
+ .include(ProtocolBenchmark.class.getSimpleName())
+ .build();
+ new Runner(opt).run();
+ }
+
+ public static class HelloServiceImpl implements HelloService {
+ @Override
+ public String sayHello(String name) {
+ return "Hello, " + name;
+ }
+ }
+}
diff --git a/rpc-benchmark/src/main/java/com/xiaoyu/rpc/benchmark/SerializationBenchmark.java b/rpc-benchmark/src/main/java/com/xiaoyu/rpc/benchmark/SerializationBenchmark.java
new file mode 100644
index 0000000..31c656d
--- /dev/null
+++ b/rpc-benchmark/src/main/java/com/xiaoyu/rpc/benchmark/SerializationBenchmark.java
@@ -0,0 +1,96 @@
+package com.xiaoyu.rpc.benchmark;
+
+import com.xiaoyu.rpc.common.extension.ExtensionLoader;
+import com.xiaoyu.rpc.common.serialization.Serializer;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import com.xiaoyu.rpc.common.vo.RpcRequest;
+import java.io.Serializable;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+@BenchmarkMode({ Mode.AverageTime, Mode.Throughput })
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@State(Scope.Benchmark)
+@Fork(value = 1, warmups = 1)
+@Warmup(iterations = 1, time = 1)
+@Measurement(iterations = 2, time = 1)
+public class SerializationBenchmark {
+
+ @Param({ "java", "kryo", "json", "protobuf" })
+ private String serializerName;
+
+ private Serializer serializer;
+ private RpcRequest rpcRequest;
+ private String testString;
+
+ private byte[] serializedRequestBytes;
+ private byte[] serializedStringBytes;
+
+ @Setup
+ public void setup() {
+ serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension(serializerName);
+
+ // Setup String
+ testString = "Hello, Benchmark! This is a test string for RPC serialization comparison.";
+ try {
+ serializedStringBytes = serializer.serialize(testString);
+ } catch (Exception e) {
+ System.err.println("Serializer [" + serializerName + "] failed to serialize String: " + e.getMessage());
+ }
+
+ // Setup RpcRequest (Protobuf Message)
+ // Note: Java/Kryo/Json can also serialize this since it implements Serializable
+ // (via GeneratedMessageV3)
+ // or effectively acts as a POJO for them.
+ RpcRequest.Builder builder = RpcRequest.newBuilder()
+ .setInterfaceName("com.example.HelloService")
+ .setMethodName("sayHello")
+ .addParamTypes("java.lang.String");
+
+ // Add dummy bytes parameter
+ builder.addParameters(com.google.protobuf.ByteString.copyFromUtf8("Benchmark"));
+
+ rpcRequest = builder.build();
+
+ try {
+ serializedRequestBytes = serializer.serialize(rpcRequest);
+ System.out.println(
+ "Serializer [" + serializerName + "] POJO (RpcRequest) Size: " + serializedRequestBytes.length
+ + " bytes");
+ } catch (Exception e) {
+ System.err.println("Serializer [" + serializerName + "] failed to serialize POJO: " + e.getMessage());
+ }
+ }
+
+ @Benchmark
+ public void serializePojo(org.openjdk.jmh.infra.Blackhole bh) {
+ bh.consume(serializer.serialize(rpcRequest));
+ }
+
+ @Benchmark
+ public void deserializePojo(org.openjdk.jmh.infra.Blackhole bh) {
+ bh.consume(serializer.deserialize(serializedRequestBytes, RpcRequest.class));
+ }
+
+ @Benchmark
+ public void serializeString(org.openjdk.jmh.infra.Blackhole bh) {
+ bh.consume(serializer.serialize(testString));
+ }
+
+ @Benchmark
+ public void deserializeString(org.openjdk.jmh.infra.Blackhole bh) {
+ bh.consume(serializer.deserialize(serializedStringBytes, String.class));
+ }
+
+ public static void main(String[] args) throws RunnerException {
+ Options opt = new OptionsBuilder()
+ .include(SerializationBenchmark.class.getSimpleName())
+ .build();
+ new Runner(opt).run();
+ }
+}
diff --git a/rpc-benchmark/src/main/resources/logback.xml b/rpc-benchmark/src/main/resources/logback.xml
new file mode 100644
index 0000000..2a299a7
--- /dev/null
+++ b/rpc-benchmark/src/main/resources/logback.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
diff --git a/rpc-common/src/main/java/com/xiaoyu/rpc/common/extension/ExtensionLoader.java b/rpc-common/src/main/java/com/xiaoyu/rpc/common/extension/ExtensionLoader.java
index 6ffa78d..0285fbc 100644
--- a/rpc-common/src/main/java/com/xiaoyu/rpc/common/extension/ExtensionLoader.java
+++ b/rpc-common/src/main/java/com/xiaoyu/rpc/common/extension/ExtensionLoader.java
@@ -67,14 +67,14 @@ public T getExtension(String name) {
throw new IllegalArgumentException("Extension name should not be null or empty.");
}
- // 1. 获取或创建单例Holder
+ // 先拿到对应名称的缓存槽位,不存在就补一个
Holder
+
+ com.xiaoyu.rpc
+ rpc-transport-netty
+ ${project.version}
+
org.junit.jupiter
junit-jupiter
diff --git a/rpc-consumer/src/main/java/com/xiaoyu/rpc/consumer/ConsumerApp.java b/rpc-consumer/src/main/java/com/xiaoyu/rpc/consumer/ConsumerApp.java
index 04b4323..1da9767 100644
--- a/rpc-consumer/src/main/java/com/xiaoyu/rpc/consumer/ConsumerApp.java
+++ b/rpc-consumer/src/main/java/com/xiaoyu/rpc/consumer/ConsumerApp.java
@@ -22,6 +22,8 @@ public static void main(String[] args) {
} catch (Exception e) {
e.printStackTrace();
+ System.exit(1);
}
+ System.exit(0);
}
}
diff --git a/rpc-consumer/src/test/java/com/xiaoyu/rpc/consumer/FullIntegrationTest.java b/rpc-consumer/src/test/java/com/xiaoyu/rpc/consumer/FullIntegrationTest.java
index 796c41e..cd38854 100644
--- a/rpc-consumer/src/test/java/com/xiaoyu/rpc/consumer/FullIntegrationTest.java
+++ b/rpc-consumer/src/test/java/com/xiaoyu/rpc/consumer/FullIntegrationTest.java
@@ -1,9 +1,21 @@
package com.xiaoyu.rpc.consumer;
-import com.xiaoyu.rpc.core.client.RpcClientProxy;
import com.xiaoyu.rpc.api.HelloService;
+import com.xiaoyu.rpc.common.serialization.Serializer;
+import com.xiaoyu.rpc.common.serialization.SerializerCode;
+import com.xiaoyu.rpc.common.vo.RpcRequest;
+import com.xiaoyu.rpc.core.client.RpcClient;
+import com.xiaoyu.rpc.core.config.RpcConfig;
import com.xiaoyu.rpc.core.server.RpcServer;
+import com.google.protobuf.ByteString;
+import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.util.concurrent.TimeUnit;
+
import static org.junit.jupiter.api.Assertions.*;
public class FullIntegrationTest {
@@ -20,10 +32,17 @@ public String sayHello(String name) {
public void testFullIntegration() throws InterruptedException {
// Use Local Registry to avoid external dependency
System.setProperty("rpc.registry", "local");
+ System.setProperty("rpc.server-port", "9090"); // Use port 9090
// Ensure we use KRYO or JSON/Hessian serializer that supports mundane Java
- // classes (String)
- // because Protobuf serializer requires Protobuf generated classes.
+ // classes
System.setProperty("rpc.serializer", "kryo");
+ System.setProperty("rpc.transport", "netty");
+
+ // 沙箱或受限环境无法监听端口时,跳过此集成测试,避免把环境问题算成代码失败
+ Assumptions.assumeTrue(canBindLocalPort(9090), "No permission to bind local test port 9090");
+
+ // 集成测试固定走 netty,减少跨协议变量,确保该用例只验证端到端调用主链路
+ forceConfig("protocol", "netty");
// Start Server in a thread
Thread serverThread = new Thread(() -> {
@@ -39,18 +58,23 @@ public void testFullIntegration() throws InterruptedException {
serverThread.setDaemon(true);
serverThread.start();
- Thread.sleep(2000); // Wait for server start
+ Thread.sleep(1500); // Wait for server start
try {
System.out.println("Starting Client...");
- HelloService helloService = RpcClientProxy.create(HelloService.class);
+ RpcClient rpcClient = new RpcClient();
+ Serializer serializer = SerializerCode.getSerializerByCode(RpcConfig.getInstance().getSerializerCode());
System.out.println(">>> First Call");
- String result1 = helloService.sayHello("World1");
+ String result1 = (String) rpcClient
+ .sendRequest(buildRequest("World1", serializer), String.class)
+ .get(5, TimeUnit.SECONDS);
System.out.println("Result1: " + result1);
System.out.println(">>> Second Call (Should reuse connection)");
- String result2 = helloService.sayHello("World2");
+ String result2 = (String) rpcClient
+ .sendRequest(buildRequest("World2", serializer), String.class)
+ .get(5, TimeUnit.SECONDS);
System.out.println("Result2: " + result2);
assertNotNull(result1, "Result1 should not be null");
@@ -64,4 +88,33 @@ public void testFullIntegration() throws InterruptedException {
fail("Test failed with exception: " + e.getMessage());
}
}
+
+ private static RpcRequest buildRequest(String name, Serializer serializer) {
+ byte[] argBytes = serializer.serialize(name);
+ return RpcRequest.newBuilder()
+ .setInterfaceName(HelloService.class.getName())
+ .setMethodName("sayHello")
+ .addParamTypes(String.class.getName())
+ .addParameters(ByteString.copyFrom(argBytes))
+ .build();
+ }
+
+ private static boolean canBindLocalPort(int port) {
+ try (ServerSocket ignored = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"))) {
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private static void forceConfig(String fieldName, Object value) {
+ try {
+ RpcConfig config = RpcConfig.getInstance();
+ Field field = RpcConfig.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(config, value);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to force RpcConfig field: " + fieldName, e);
+ }
+ }
}
diff --git a/rpc-core/pom.xml b/rpc-core/pom.xml
index 5b97bf5..ab080e3 100644
--- a/rpc-core/pom.xml
+++ b/rpc-core/pom.xml
@@ -17,10 +17,8 @@
rpc-common
-
- io.netty
- netty-all
-
+
+
org.projectlombok
lombok
@@ -29,6 +27,8 @@
ch.qos.logback
logback-classic
+
+
com.esotericsoftware
kryo
@@ -41,30 +41,29 @@
org.yaml
snakeyaml
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+
com.alibaba.nacos
nacos-client
+
+
net.bytebuddy
byte-buddy
+
+
- io.grpc
- grpc-netty-shaded
-
-
- io.grpc
- grpc-protobuf
-
-
- io.grpc
- grpc-stub
-
-
- com.google.code.gson
- gson
- 2.10.1
+ org.junit.jupiter
+ junit-jupiter
+ test
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/ByteBuddyProxyFactory.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/ByteBuddyProxyFactory.java
index 6db82bb..cacbafa 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/ByteBuddyProxyFactory.java
+++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/ByteBuddyProxyFactory.java
@@ -11,19 +11,25 @@
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
+import java.util.concurrent.CompletableFuture;
public class ByteBuddyProxyFactory implements ProxyFactory {
+ private final RpcClient rpcClient;
+
+ public ByteBuddyProxyFactory() {
+ this.rpcClient = new RpcClient();
+ }
+
@Override
@SuppressWarnings("unchecked")
public T getProxy(Class clazz) {
try {
- return (T) new ByteBuddy()
- .subclass(clazz)
- .method(ElementMatchers.any())
+ return (T) new ByteBuddy().subclass(clazz).method(ElementMatchers.any())
.intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ // 请求中记录接口名 + 方法名,服务端靠这两项定位目标方法
RpcRequest.Builder builder = RpcRequest.newBuilder()
.setInterfaceName(method.getDeclaringClass().getName())
.setMethodName(method.getName());
@@ -46,14 +52,14 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
}
RpcRequest request = builder.build();
- return new RpcClient().sendRequest(request, method.getReturnType());
+ CompletableFuture
+
+ com.xiaoyu.rpc
+ rpc-transport-netty
+ ${project.version}
+
diff --git a/rpc-spring-boot-starter/pom.xml b/rpc-spring-boot-starter/pom.xml
new file mode 100644
index 0000000..0ba800a
--- /dev/null
+++ b/rpc-spring-boot-starter/pom.xml
@@ -0,0 +1,58 @@
+
+
+
+ grpc-demo
+ com.xiaoyu.rpc
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ rpc-spring-boot-starter
+
+
+ 3.2.0
+
+
+
+
+
+ com.xiaoyu.rpc
+ rpc-core
+ ${project.version}
+
+
+
+
+ com.xiaoyu.rpc
+ rpc-transport-netty
+ ${project.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ ${spring-boot.version}
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+ ${spring-boot.version}
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ ${spring-boot.version}
+ true
+
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
diff --git a/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/RpcAutoConfiguration.java b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/RpcAutoConfiguration.java
new file mode 100644
index 0000000..cc7e6f8
--- /dev/null
+++ b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/RpcAutoConfiguration.java
@@ -0,0 +1,99 @@
+package com.xiaoyu.rpc.spring;
+
+import com.xiaoyu.rpc.core.config.RpcConfig;
+import com.xiaoyu.rpc.core.server.RpcServer;
+import com.xiaoyu.rpc.spring.config.RpcProperties;
+import com.xiaoyu.rpc.spring.processor.RpcPostProcessor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * RPC 自动配置类
+ */
+@Slf4j
+@Configuration
+@EnableConfigurationProperties(RpcProperties.class)
+public class RpcAutoConfiguration {
+
+ /**
+ * 将 Spring Boot 配置同步到 RpcConfig (核心框架配置)
+ */
+ @Bean
+ public RpcConfig rpcConfig(RpcProperties properties) {
+ // 通过 System Properties 传递配置,让 RpcConfig 能够读取
+ System.setProperty("rpc.transport", properties.getTransport());
+ System.setProperty("rpc.protocol", properties.getProtocol());
+ System.setProperty("rpc.server-host", properties.getServerHost());
+ System.setProperty("rpc.server-port", String.valueOf(properties.getServerPort()));
+ System.setProperty("rpc.registry", properties.getRegistry());
+ System.setProperty("rpc.registry-address", properties.getRegistryAddress());
+ System.setProperty("rpc.serializer", properties.getSerializer());
+ System.setProperty("rpc.proxy", properties.getProxy());
+ System.setProperty("rpc.load-balancer", properties.getLoadBalancer());
+
+ log.info("RPC 配置已从 Spring Boot 同步: registry={}, port={}",
+ properties.getRegistry(), properties.getServerPort());
+
+ return RpcConfig.getInstance();
+ }
+
+ /**
+ * 创建 RpcServer Bean (仅当 serverEnabled=true 时)
+ */
+ @Bean
+ @ConditionalOnProperty(prefix = "rpc", name = "server-enabled", havingValue = "true", matchIfMissing = true)
+ @ConditionalOnMissingBean
+ public RpcServer rpcServer(RpcConfig rpcConfig) {
+ log.info("创建 RpcServer Bean");
+ return new RpcServer();
+ }
+
+ /**
+ * 创建 RPC Bean 后处理器
+ */
+ @Bean
+ public RpcPostProcessor rpcPostProcessor() {
+ return new RpcPostProcessor();
+ }
+
+ /**
+ * 启动 RpcServer
+ */
+ @Bean
+ @ConditionalOnProperty(prefix = "rpc", name = "server-enabled", havingValue = "true", matchIfMissing = true)
+ public RpcServerRunner rpcServerRunner(RpcServer rpcServer) {
+ return new RpcServerRunner(rpcServer);
+ }
+
+ /**
+ * 使用 CommandLineRunner 启动 RpcServer
+ */
+ @Slf4j
+ public static class RpcServerRunner implements org.springframework.boot.CommandLineRunner {
+ private final RpcServer rpcServer;
+
+ public RpcServerRunner(RpcServer rpcServer) {
+ this.rpcServer = rpcServer;
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+ log.info("启动 RPC Server...");
+ // 在新线程中启动,避免阻塞 Spring Boot 主线程
+ Thread serverThread = new Thread(() -> {
+ try {
+ rpcServer.start();
+ } catch (InterruptedException e) {
+ log.error("RPC Server 启动失败", e);
+ Thread.currentThread().interrupt();
+ }
+ }, "rpc-server-thread");
+ serverThread.setDaemon(true);
+ serverThread.start();
+ }
+ }
+}
diff --git a/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/annotation/RpcReference.java b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/annotation/RpcReference.java
new file mode 100644
index 0000000..6dc6f8e
--- /dev/null
+++ b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/annotation/RpcReference.java
@@ -0,0 +1,23 @@
+package com.xiaoyu.rpc.spring.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 标注在字段上,表示需要注入 RPC 服务代理。
+ * Spring Boot Starter 会自动创建代理并注入。
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RpcReference {
+
+ /**
+ * 服务版本号
+ */
+ String version() default "1.0";
+
+ /**
+ * 负载均衡策略 (roundrobin, random)
+ */
+ String loadBalancer() default "";
+}
diff --git a/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/annotation/RpcService.java b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/annotation/RpcService.java
new file mode 100644
index 0000000..98fef44
--- /dev/null
+++ b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/annotation/RpcService.java
@@ -0,0 +1,24 @@
+package com.xiaoyu.rpc.spring.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 标注在服务实现类上,表示该类是一个 RPC 服务提供者。
+ * Spring Boot Starter 会自动扫描并注册到 RPC 注册中心。
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface RpcService {
+
+ /**
+ * 服务接口类。如果不指定,则默认使用实现类的第一个接口。
+ */
+ Class> interfaceClass() default void.class;
+
+ /**
+ * 服务版本号
+ */
+ String version() default "1.0";
+}
diff --git a/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/config/RpcProperties.java b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/config/RpcProperties.java
new file mode 100644
index 0000000..1f4f134
--- /dev/null
+++ b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/config/RpcProperties.java
@@ -0,0 +1,62 @@
+package com.xiaoyu.rpc.spring.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * RPC 配置属性,映射 application.yml 中的 rpc.* 配置
+ */
+@Data
+@ConfigurationProperties(prefix = "rpc")
+public class RpcProperties {
+
+ /**
+ * 传输层实现 (netty)
+ */
+ private String transport = "netty";
+
+ /**
+ * 协议类型 (netty, http, http2, grpc)
+ */
+ private String protocol = "netty";
+
+ /**
+ * 服务端绑定主机
+ */
+ private String serverHost = "127.0.0.1";
+
+ /**
+ * 服务端绑定端口
+ */
+ private int serverPort = 8080;
+
+ /**
+ * 注册中心类型 (nacos, local)
+ */
+ private String registry = "nacos";
+
+ /**
+ * 注册中心地址
+ */
+ private String registryAddress = "127.0.0.1:8848";
+
+ /**
+ * 序列化方式 (kryo, protobuf, json, java)
+ */
+ private String serializer = "kryo";
+
+ /**
+ * 代理方式 (jdk, bytebuddy)
+ */
+ private String proxy = "bytebuddy";
+
+ /**
+ * 负载均衡策略 (roundrobin, random)
+ */
+ private String loadBalancer = "roundrobin";
+
+ /**
+ * 是否启用服务端 (Provider 模式)
+ */
+ private boolean serverEnabled = true;
+}
diff --git a/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/processor/RpcPostProcessor.java b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/processor/RpcPostProcessor.java
new file mode 100644
index 0000000..78cc43e
--- /dev/null
+++ b/rpc-spring-boot-starter/src/main/java/com/xiaoyu/rpc/spring/processor/RpcPostProcessor.java
@@ -0,0 +1,106 @@
+package com.xiaoyu.rpc.spring.processor;
+
+import com.xiaoyu.rpc.core.client.RpcClientProxy;
+import com.xiaoyu.rpc.core.server.RpcServer;
+import com.xiaoyu.rpc.spring.annotation.RpcReference;
+import com.xiaoyu.rpc.spring.annotation.RpcService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * RPC Bean 后处理器
+ * 负责扫描 @RpcService 并自动注册服务,
+ * 同时处理 @RpcReference 字段并注入客户端代理。
+ */
+@Slf4j
+public class RpcPostProcessor implements BeanPostProcessor, ApplicationContextAware {
+
+ private ApplicationContext applicationContext;
+ private RpcServer rpcServer;
+
+ // 代理缓存,避免重复创建
+ private final Map, Object> proxyCache = new ConcurrentHashMap<>();
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.applicationContext = applicationContext;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ // 处理 @RpcService:把服务注册到 RpcServer
+ Class> beanClass = bean.getClass();
+ if (beanClass.isAnnotationPresent(RpcService.class)) {
+ registerService(bean, beanClass);
+ }
+
+ // 处理 @RpcReference:给字段注入代理对象
+ injectRpcReferences(bean, beanClass);
+
+ return bean;
+ }
+
+ /**
+ * 注册服务到 RpcServer
+ */
+ @SuppressWarnings("unchecked")
+ private void registerService(Object bean, Class> beanClass) {
+ RpcService rpcService = beanClass.getAnnotation(RpcService.class);
+ Class> interfaceClass = rpcService.interfaceClass();
+
+ // 如果未指定接口,则使用第一个实现的接口
+ if (interfaceClass == void.class) {
+ Class>[] interfaces = beanClass.getInterfaces();
+ if (interfaces.length == 0) {
+ throw new RuntimeException("@RpcService 必须实现至少一个接口: " + beanClass.getName());
+ }
+ interfaceClass = interfaces[0];
+ }
+
+ // 延迟获取 RpcServer (因为可能还没初始化)
+ if (rpcServer == null) {
+ rpcServer = applicationContext.getBean(RpcServer.class);
+ }
+
+ log.info("注册 RPC 服务: {} -> {}", interfaceClass.getName(), beanClass.getName());
+ rpcServer.register((Class) interfaceClass, bean);
+ }
+
+ /**
+ * 注入 @RpcReference 标注的字段
+ */
+ private void injectRpcReferences(Object bean, Class> beanClass) {
+ Field[] fields = beanClass.getDeclaredFields();
+ for (Field field : fields) {
+ if (field.isAnnotationPresent(RpcReference.class)) {
+ Class> fieldType = field.getType();
+
+ // 从缓存获取或创建代理
+ Object proxy = proxyCache.computeIfAbsent(fieldType, this::createProxy);
+
+ // 注入
+ field.setAccessible(true);
+ try {
+ field.set(bean, proxy);
+ log.info("注入 RPC 代理: {}.{}", beanClass.getSimpleName(), field.getName());
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("无法注入 RPC 代理到字段: " + field.getName(), e);
+ }
+ }
+ }
+ }
+
+ /**
+ * 创建 RPC 客户端代理
+ */
+ private Object createProxy(Class> interfaceClass) {
+ return RpcClientProxy.create(interfaceClass);
+ }
+}
diff --git a/rpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/rpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..78a58bb
--- /dev/null
+++ b/rpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.xiaoyu.rpc.spring.RpcAutoConfiguration
diff --git a/rpc-transport-netty/pom.xml b/rpc-transport-netty/pom.xml
new file mode 100644
index 0000000..e9b9d62
--- /dev/null
+++ b/rpc-transport-netty/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+ grpc-demo
+ com.xiaoyu.rpc
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ rpc-transport-netty
+
+
+
+ com.xiaoyu.rpc
+ rpc-core
+ ${project.version}
+
+
+ io.netty
+ netty-all
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+
+ io.grpc
+ grpc-netty-shaded
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ io.grpc
+ grpc-stub
+
+
+
+
+ com.google.protobuf
+ protobuf-java
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/ChannelProvider.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/client/ChannelProvider.java
similarity index 93%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/client/ChannelProvider.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/client/ChannelProvider.java
index 343a812..a9ab2ff 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/ChannelProvider.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/client/ChannelProvider.java
@@ -21,7 +21,7 @@ public class ChannelProvider {
public static Channel get(InetSocketAddress inetSocketAddress, Bootstrap bootstrap) {
String key = inetSocketAddress.toString();
- // 1. 尝试从缓存获取
+ // 先尝试复用已有连接
if (channels.containsKey(key)) {
Channel channel = channels.get(key);
if (channel != null && channel.isActive()) {
@@ -31,10 +31,10 @@ public static Channel get(InetSocketAddress inetSocketAddress, Bootstrap bootstr
}
}
- // 2. 建立新连接
+ // 缓存不可用时再新建连接
Channel channel = connect(bootstrap, inetSocketAddress);
- // 3. 放入缓存
+ // 新连接建立成功后放回缓存
if (channel != null) {
channels.put(key, channel);
}
diff --git a/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/client/NettyRpcClientHandler.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/client/NettyRpcClientHandler.java
new file mode 100644
index 0000000..e97146c
--- /dev/null
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/client/NettyRpcClientHandler.java
@@ -0,0 +1,62 @@
+package com.xiaoyu.rpc.core.client;
+
+import com.xiaoyu.rpc.common.vo.RpcResponse;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import java.util.concurrent.CompletableFuture;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.netty.channel.ChannelHandler;
+
+/**
+ * 客户端响应处理器。
+ * 通过 requestId 将响应路由回对应的 CompletableFuture。
+ */
+@ChannelHandler.Sharable
+public class NettyRpcClientHandler extends SimpleChannelInboundHandler {
+ private static final Logger log = LoggerFactory.getLogger(NettyRpcClientHandler.class);
+
+ // 一个连接上可以并发多个请求,靠 requestId 区分各自回调
+ private final java.util.Map> pendingRequests = new java.util.concurrent.ConcurrentHashMap<>();
+
+ public void addFuture(String requestId, CompletableFuture future) {
+ pendingRequests.put(requestId, future);
+ }
+
+ public void removeFuture(String requestId) {
+ pendingRequests.remove(requestId);
+ }
+
+ public void failRequest(String requestId, Throwable cause) {
+ CompletableFuture future = pendingRequests.remove(requestId);
+ if (future != null) {
+ future.completeExceptionally(cause);
+ }
+ }
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, RpcResponse response) {
+ String requestId = response.getRequestId();
+ CompletableFuture future = pendingRequests.remove(requestId);
+
+ if (future != null) {
+ log.info("Client received response for requestId: {}, status: {}", requestId, response.getMessage());
+ future.complete(response);
+ } else {
+ log.warn("Client received response for unknown or timed-out requestId: {}", requestId);
+ }
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ log.error("Client caught exception", cause);
+ // 连接级异常通常影响当前连接上的全部在途请求,统一失败返回给上层
+ for (CompletableFuture future : pendingRequests.values()) {
+ future.completeExceptionally(cause);
+ }
+ pendingRequests.clear();
+ ctx.close();
+ }
+}
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/Protocol.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/Protocol.java
similarity index 100%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/Protocol.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/Protocol.java
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/ProtocolFactory.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/ProtocolFactory.java
similarity index 73%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/ProtocolFactory.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/ProtocolFactory.java
index 62eed19..3677fce 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/ProtocolFactory.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/ProtocolFactory.java
@@ -9,7 +9,7 @@ public class ProtocolFactory {
private static final Logger log = LoggerFactory.getLogger(ProtocolFactory.class);
public static Protocol getProtocol(String name) {
- // 默认使用 Netty
+ // 配置为空时使用默认协议,保证最小可用
if (name == null || name.trim().isEmpty()) {
name = "netty";
}
@@ -18,16 +18,13 @@ public static Protocol getProtocol(String name) {
return ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(name);
} catch (Exception e) {
log.error("Failed to load protocol: " + name, e);
- // Fallback or rethrow? Let's use Netty as fallback for robustness if strictness
- // isn't required
- // OR rethrow to fail fast.
- // Given the original code had a default case, let's keep the fail-safe behavior
- // for now but log error.
+ // 非默认协议加载失败时,兜底回退到 Netty,避免服务直接不可用
if (!"netty".equalsIgnoreCase(name)) {
log.warn("Falling back to default NettyProtocol");
return new NettyProtocol();
}
+ // 默认协议都加载失败,继续抛出异常交给上层处理
throw e;
}
}
-}
\ No newline at end of file
+}
diff --git a/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcClientResponseHandler.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcClientResponseHandler.java
new file mode 100644
index 0000000..6fd54e0
--- /dev/null
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcClientResponseHandler.java
@@ -0,0 +1,77 @@
+package com.xiaoyu.rpc.core.protocol.grpc;
+
+import com.xiaoyu.rpc.common.vo.RpcResponse;
+import com.xiaoyu.rpc.core.client.NettyRpcClientHandler;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http2.Http2DataFrame;
+import io.netty.handler.codec.http2.Http2Frame;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.handler.codec.http2.Http2HeadersFrame;
+
+/**
+ * 将 gRPC/HTTP2 帧转换为内部 RpcResponse,并交给通用客户端处理器完成 requestId 关联。
+ */
+class GrpcClientResponseHandler extends SimpleChannelInboundHandler {
+
+ private final NettyRpcClientHandler clientHandler;
+ private final String requestId;
+
+ GrpcClientResponseHandler(NettyRpcClientHandler clientHandler, String requestId) {
+ this.clientHandler = clientHandler;
+ this.requestId = requestId;
+ }
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, Http2Frame frame) throws Exception {
+ if (frame instanceof Http2DataFrame) {
+ Http2DataFrame dataFrame = (Http2DataFrame) frame;
+ ByteBuf content = dataFrame.content();
+ if (content.readableBytes() < 5) {
+ clientHandler.failRequest(requestId, new IllegalStateException("Invalid gRPC frame: missing 5-byte prefix"));
+ return;
+ }
+
+ byte compressedFlag = content.readByte();
+ if (compressedFlag != 0) {
+ clientHandler.failRequest(requestId, new UnsupportedOperationException("Compressed gRPC payload is not supported"));
+ return;
+ }
+
+ int length = content.readInt();
+ if (content.readableBytes() < length) {
+ clientHandler.failRequest(requestId, new IllegalStateException("Invalid gRPC frame: payload length mismatch"));
+ return;
+ }
+
+ ByteBuf slice = content.readSlice(length);
+ RpcResponse response;
+ if (slice.nioBufferCount() > 0) {
+ response = RpcResponse.parseFrom(slice.nioBuffer());
+ } else {
+ byte[] bytes = new byte[length];
+ slice.readBytes(bytes);
+ response = RpcResponse.parseFrom(bytes);
+ }
+ ctx.fireChannelRead(response);
+ return;
+ }
+
+ if (frame instanceof Http2HeadersFrame) {
+ Http2Headers headers = ((Http2HeadersFrame) frame).headers();
+ CharSequence grpcStatus = headers.get("grpc-status");
+ if (grpcStatus != null && !"0".contentEquals(grpcStatus)) {
+ CharSequence grpcMessage = headers.get("grpc-message");
+ String message = grpcMessage == null ? "unknown grpc error" : grpcMessage.toString();
+ clientHandler.failRequest(requestId, new RuntimeException("gRPC error status=" + grpcStatus + ", message=" + message));
+ }
+ }
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ clientHandler.failRequest(requestId, cause);
+ ctx.close();
+ }
+}
diff --git a/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcProtocol.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcProtocol.java
new file mode 100644
index 0000000..3271df3
--- /dev/null
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcProtocol.java
@@ -0,0 +1,105 @@
+package com.xiaoyu.rpc.core.protocol.grpc;
+
+import com.xiaoyu.rpc.common.vo.RpcRequest;
+import com.xiaoyu.rpc.core.client.NettyRpcClientHandler;
+import com.xiaoyu.rpc.core.protocol.Protocol;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http2.DefaultHttp2DataFrame;
+import io.netty.handler.codec.http2.DefaultHttp2Headers;
+import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame;
+import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.handler.codec.http2.Http2Settings;
+import io.netty.handler.codec.http2.Http2StreamChannel;
+import io.netty.handler.codec.http2.Http2StreamChannelBootstrap;
+import io.netty.handler.codec.http2.Http2MultiplexHandler;
+import io.netty.util.ReferenceCountUtil;
+
+import java.net.InetSocketAddress;
+
+public class GrpcProtocol implements Protocol {
+
+ @Override
+ public String getName() {
+ return "grpc";
+ }
+
+ @Override
+ public void config(ChannelPipeline pipeline, boolean isServer, ChannelHandler serverHandler) {
+ if (isServer) {
+ // 先接入 HTTP/2 帧编解码,处理握手并产出 Frame
+ pipeline.addLast(Http2FrameCodecBuilder.forServer().build());
+
+ // 再通过 MultiplexHandler 为每个 Stream 创建子 Channel
+ pipeline.addLast(new Http2MultiplexHandler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(Channel ch) throws Exception {
+ ChannelPipeline p = ch.pipeline();
+ // 在子 Channel 中添加 gRPC 适配器
+ p.addLast(new GrpcServerHandler(serverHandler));
+ // 添加业务处理器 (复用现有的 NettyRpcHandler)
+ p.addLast(serverHandler);
+ }
+ }));
+ } else {
+ pipeline.addLast(Http2FrameCodecBuilder.forClient()
+ .autoAckSettingsFrame(true)
+ .autoAckPingFrame(true)
+ .initialSettings(Http2Settings.defaultSettings().maxHeaderListSize(8192))
+ .build());
+ pipeline.addLast(new Http2MultiplexHandler(new ChannelInboundHandlerAdapter() {
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ // 连接级残留帧统一释放,避免引用计数对象泄漏
+ ReferenceCountUtil.release(msg);
+ }
+ }));
+ }
+ }
+
+ @Override
+ public void sendRequest(Channel channel, RpcRequest request, NettyRpcClientHandler clientHandler) throws Exception {
+ Http2StreamChannelBootstrap streamBootstrap = new Http2StreamChannelBootstrap(channel);
+ streamBootstrap.open().addListener(openFuture -> {
+ if (!openFuture.isSuccess()) {
+ clientHandler.failRequest(request.getRequestId(), openFuture.cause());
+ return;
+ }
+
+ Http2StreamChannel streamChannel = (Http2StreamChannel) openFuture.getNow();
+ streamChannel.pipeline().addLast(new GrpcClientResponseHandler(clientHandler, request.getRequestId()));
+ streamChannel.pipeline().addLast(clientHandler);
+
+ byte[] payload = request.toByteArray();
+ io.netty.buffer.ByteBuf body = streamChannel.alloc().buffer(payload.length + 5);
+ body.writeByte(0); // compressed-flag
+ body.writeInt(payload.length);
+ body.writeBytes(payload);
+
+ Http2Headers headers = new DefaultHttp2Headers()
+ .method("POST")
+ .path("/GrpcService/handle")
+ .scheme("http")
+ .set(HttpHeaderNames.CONTENT_TYPE, "application/grpc")
+ .set(HttpHeaderNames.TE, "trailers");
+
+ if (channel.remoteAddress() instanceof InetSocketAddress) {
+ InetSocketAddress remote = (InetSocketAddress) channel.remoteAddress();
+ headers.authority(remote.getHostString() + ":" + remote.getPort());
+ }
+
+ streamChannel.write(new DefaultHttp2HeadersFrame(headers, false));
+ streamChannel.writeAndFlush(new DefaultHttp2DataFrame(body, true)).addListener(writeFuture -> {
+ if (!writeFuture.isSuccess()) {
+ clientHandler.failRequest(request.getRequestId(), writeFuture.cause());
+ }
+ });
+ });
+ }
+}
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcServerHandler.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcServerHandler.java
similarity index 83%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcServerHandler.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcServerHandler.java
index 8a57a6e..2468985 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcServerHandler.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcServerHandler.java
@@ -13,6 +13,7 @@
@Slf4j
public class GrpcServerHandler extends ChannelDuplexHandler {
+ // 透传的业务处理器(例如 NettyRpcHandler)
private final io.netty.channel.ChannelHandler busineesHandler;
public GrpcServerHandler(io.netty.channel.ChannelHandler busineesHandler) {
@@ -23,6 +24,7 @@ public GrpcServerHandler(io.netty.channel.ChannelHandler busineesHandler) {
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Http2Frame) {
try {
+ // 只在这里处理 gRPC 对应的 HTTP/2 Frame,转换成内部 RpcRequest
processFrame(ctx, (Http2Frame) msg);
} finally {
ReferenceCountUtil.release(msg);
@@ -46,6 +48,7 @@ private void processFrame(ChannelHandlerContext ctx, Http2Frame frame) throws Ex
Http2DataFrame dataFrame = (Http2DataFrame) frame;
ByteBuf content = dataFrame.content();
+ // gRPC 数据帧固定前缀:1 字节压缩标记 + 4 字节消息长度
if (content.readableBytes() < 5)
return;
@@ -53,24 +56,20 @@ private void processFrame(ChannelHandlerContext ctx, Http2Frame frame) throws Ex
int length = content.readInt();
if (content.readableBytes() < length) {
- // Return reader index to start if not enough data (simplified, normally
- // requires buffering)
+ // 当前帧数据不足,回退读指针等待后续数据(简化处理,生产环境建议引入缓冲聚合)
content.resetReaderIndex();
return;
}
- // Zero-Copy Optimization:
- // Use slicing to avoid allocating an intermediate byte[]
+ // 尽量避免中间大数组拷贝:先切片,再按底层存储类型选择解析路径
ByteBuf slice = content.readSlice(length);
RpcRequest rpcRequest;
if (slice.nioBufferCount() > 0) {
- // Zero-Copy: Direct access via NIO ByteBuffer
+ // 直接走 NIO Buffer 解析,少一次复制
rpcRequest = RpcRequest.parseFrom(slice.nioBuffer());
} else {
- // Fallback: Use InputStream (avoids large byte[] allocation)
- // Note: Protobuf CodedInputStream still copies when string/bytes parsed, but we
- // avoid the big chunk copy
+ // 兜底路径:内存布局不支持 NIO Buffer 时退回字节数组解析
byte[] bytes = new byte[length];
slice.readBytes(bytes);
rpcRequest = RpcRequest.parseFrom(bytes);
@@ -86,6 +85,7 @@ public void write(ChannelHandlerContext ctx, Object msg, io.netty.channel.Channe
RpcResponse response = (RpcResponse) msg;
try {
byte[] bytes = response.toByteArray();
+ // gRPC 响应体同样要补上 5 字节前缀(压缩位 + 长度)
ByteBuf out = ctx.alloc().buffer();
out.writeByte(0);
out.writeInt(bytes.length);
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpProtocol.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpProtocol.java
similarity index 90%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpProtocol.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpProtocol.java
index 6c23ea9..866ecbd 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpProtocol.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpProtocol.java
@@ -20,11 +20,11 @@ public String getName() {
@Override
public void config(ChannelPipeline pipeline, boolean isServer, io.netty.channel.ChannelHandler serverHandler) {
- // 1. 获取序列化器
+ // 读取当前配置的序列化器
RpcConfig rpcConfig = RpcConfig.getInstance();
Serializer serializer = SerializerCode.getSerializerByCode(rpcConfig.getSerializerCode());
- // 2. HTTP 编解码基础
+ // 先挂载 HTTP 基础编解码器
if (isServer) {
pipeline.addLast(new HttpServerCodec());
} else {
@@ -32,7 +32,7 @@ public void config(ChannelPipeline pipeline, boolean isServer, io.netty.channel.
}
pipeline.addLast(new HttpObjectAggregator(512 * 1024));
- // 3. HTTP 与 RpcObject 的转换层
+ // 再挂载 HTTP 报文与 RPC 对象之间的转换器
if (isServer) {
// 服务端:解码 Request,编码 Response
pipeline.addLast(new HttpRpcDecoder(serializer, RpcRequest.class));
@@ -58,8 +58,8 @@ public void sendRequest(io.netty.channel.Channel channel, RpcRequest request,
channel.writeAndFlush(request).addListener(future -> {
if (!future.isSuccess()) {
- clientHandler.getFuture().completeExceptionally(future.cause());
+ clientHandler.failRequest(request.getRequestId(), future.cause());
}
});
}
-}
\ No newline at end of file
+}
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcDecoder.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcDecoder.java
similarity index 76%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcDecoder.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcDecoder.java
index d75b987..2efc5e4 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcDecoder.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcDecoder.java
@@ -9,6 +9,7 @@
import lombok.extern.slf4j.Slf4j;
import java.util.List;
+
@Slf4j
public class HttpRpcDecoder extends MessageToMessageDecoder {
private final Serializer serializer;
@@ -21,20 +22,20 @@ public HttpRpcDecoder(Serializer serializer, Class> genericClass) {
@Override
protected void decode(ChannelHandlerContext ctx, FullHttpMessage msg, List out) {
- // 读取 Body 数据
+ // FullHttpMessage 已经由聚合器拼成完整报文,这里可以一次性读取 body
ByteBuf content = msg.content();
byte[] bytes = new byte[content.readableBytes()];
content.readBytes(bytes);
- // 反序列化
+ // 直接按目标类型反序列化为 RpcRequest / RpcResponse
Object obj = serializer.deserialize(bytes, genericClass);
- // 如果是 Request,可以从 Header 中校验方法名(可选)
+ // 方法名放在 Header 里,主要用于排查和链路观察,不参与核心反序列化流程
if (msg instanceof FullHttpRequest) {
String methodName = ((FullHttpRequest) msg).headers().get("Rpc-Method");
- log.info("当前解码获取到的的methodName:{}",methodName);
+ // log.info("当前解码获取到的的methodName:{}",methodName);
}
out.add(obj);
}
-}
\ No newline at end of file
+}
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcEncoder.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcEncoder.java
similarity index 84%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcEncoder.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcEncoder.java
index 1716672..69946bc 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcEncoder.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcEncoder.java
@@ -23,6 +23,7 @@ public HttpRpcEncoder(Serializer serializer) {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, List out) {
+ // RPC 对象先序列化成二进制,再包成 HTTP 消息体
byte[] body = serializer.serialize(msg);
FullHttpMessage httpMessage;
@@ -34,9 +35,9 @@ protected void encode(ChannelHandlerContext ctx, Object msg, List out) {
HttpMethod.POST,
"/",
Unpooled.wrappedBuffer(body));
- // 将方法名放入 Header
+ // 方法名放 Header,便于服务端排查请求来源
httpRequest.headers().set("Rpc-Method", request.getMethodName());
- log.info("使用 HTTP 协议发送请求,方法名: {},正在Encode", request.getMethodName());
+ // log.info("使用 HTTP 协议发送请求,方法名: {},正在Encode", request.getMethodName());
httpRequest.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/x-rpc");
httpMessage = httpRequest;
} else {
@@ -47,8 +48,8 @@ protected void encode(ChannelHandlerContext ctx, Object msg, List out) {
Unpooled.wrappedBuffer(body));
}
- // 设置必要的 HTTP 长度头
+ // 显式设置长度,避免对端按分块模式误判读取边界
httpMessage.headers().set(HttpHeaderNames.CONTENT_LENGTH, body.length);
out.add(httpMessage);
}
-}
\ No newline at end of file
+}
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http2/Http2Protocol.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http2/Http2Protocol.java
similarity index 77%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http2/Http2Protocol.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http2/Http2Protocol.java
index d3f4613..80f509a 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/http2/Http2Protocol.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/http2/Http2Protocol.java
@@ -67,7 +67,7 @@ protected void initChannel(Http2StreamChannel ch) {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 如果还有残留的设置帧传到这里,说明 FrameCodec 没拦截住
- // 打印一下以便调试,或者直接释放
+ // 这里直接释放,避免引用计数对象泄漏
ReferenceCountUtil.release(msg);
}
@@ -86,22 +86,29 @@ public void sendRequest(io.netty.channel.Channel channel, RpcRequest request,
RpcConfig rpcConfig = RpcConfig.getInstance();
Serializer serializer = SerializerCode.getSerializerByCode(rpcConfig.getSerializerCode());
- // 使用 bootstrap 创建新流
+ // HTTP/2 每个请求走独立 Stream,底层 TCP 连接仍然复用同一个 Channel
io.netty.handler.codec.http2.Http2StreamChannelBootstrap streamBootstrap = new io.netty.handler.codec.http2.Http2StreamChannelBootstrap(
channel);
- Http2StreamChannel streamChannel = streamBootstrap.open().get(5, java.util.concurrent.TimeUnit.SECONDS);
-
- // 在流通道中构建完整的处理链
- streamChannel.pipeline().addLast(new Http2StreamFrameToHttpObjectCodec(false));
- streamChannel.pipeline().addLast(new io.netty.handler.codec.http.HttpObjectAggregator(512 * 1024));
- streamChannel.pipeline().addLast(new HttpRpcEncoder(serializer));
- streamChannel.pipeline().addLast(new HttpRpcDecoder(serializer, RpcResponse.class));
- streamChannel.pipeline().addLast(clientHandler);
-
- streamChannel.writeAndFlush(request).addListener(future -> {
- if (!future.isSuccess()) {
- clientHandler.getFuture().completeExceptionally(future.cause());
+
+ streamBootstrap.open().addListener(f -> {
+ if (!f.isSuccess()) {
+ clientHandler.failRequest(request.getRequestId(), f.cause());
+ return;
}
+
+ Http2StreamChannel streamChannel = (Http2StreamChannel) f.getNow();
+ // 每个 Stream 都有独立 pipeline,避免多请求之间相互干扰
+ streamChannel.pipeline().addLast(new Http2StreamFrameToHttpObjectCodec(false));
+ streamChannel.pipeline().addLast(new io.netty.handler.codec.http.HttpObjectAggregator(512 * 1024));
+ streamChannel.pipeline().addLast(new HttpRpcEncoder(serializer));
+ streamChannel.pipeline().addLast(new HttpRpcDecoder(serializer, RpcResponse.class));
+ streamChannel.pipeline().addLast(clientHandler);
+
+ streamChannel.writeAndFlush(request).addListener(writeFuture -> {
+ if (!writeFuture.isSuccess()) {
+ clientHandler.failRequest(request.getRequestId(), writeFuture.cause());
+ }
+ });
});
}
-}
\ No newline at end of file
+}
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcDecoder.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcDecoder.java
similarity index 70%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcDecoder.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcDecoder.java
index 6873329..020b396 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcDecoder.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcDecoder.java
@@ -25,32 +25,38 @@ public MyRpcDecoder(Class> genericClass) {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) {
- // 1. 校验魔数,4Bytes
+ // 先校验魔数(4 字节)
int magic = in.readInt();
log.info("正在解码数据...魔数为:{}", magic);
if (magic != MAGIC_NUMBER) {
throw new RuntimeException("未知协议魔数: " + magic);
}
- // 2. 读取消息类型,1Byte
+ // 读取消息类型(1 字节)
byte packageType = in.readByte();
log.info("正在解码数据...消息类型为:{}", packageType);
- // 3. 读取序列化器标识并获取实例,1Bytes
+ // 读取序列化器标识并拿到对应实现(1 字节)
byte serializerCode = in.readByte();
Serializer serializer = SerializerCode.getSerializerByCode(serializerCode);
log.info("正在解码数据...序列化器标识为:{}", serializerCode);
- // 4. 读取 Body 长度
+ // 读取消息体长度
int length = in.readInt();
+ int maxFrameSize = com.xiaoyu.rpc.core.config.RpcConfig.getInstance().getMaxMessageSize();
+ if (length > maxFrameSize || length < 0) {
+ log.error("拒绝过大的报文或非法长度: {} bytes, 远程地址: {}", length, ctx.channel().remoteAddress());
+ ctx.close(); // 直接断开物理连接,防止持续攻击
+ throw new RuntimeException("拒绝过大的报文: " + length);
+ }
- // 5. 读取 Body 数据
+ // 按长度读取消息体数据
byte[] body = new byte[length];
in.readBytes(body);
- // 6. 反序列化
+ // 反序列化为请求或响应对象
Class> clazz = (packageType == 0x01) ? RpcRequest.class : RpcResponse.class;
Object obj = serializer.deserialize(body, clazz);
out.add(obj);
}
-}
\ No newline at end of file
+}
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcEncoder.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcEncoder.java
similarity index 79%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcEncoder.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcEncoder.java
index ad727cc..d30148a 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcEncoder.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcEncoder.java
@@ -16,26 +16,26 @@ public MyRpcEncoder(Serializer serializer) {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) {
- // 1. 写入魔数 (4字节)
+ // 先写固定魔数(4 字节)
out.writeInt(MAGIC_NUMBER);
- // 2. 写入消息类型 (1字节)
+ // 写消息类型(1 字节)
if (msg instanceof RpcRequest) {
out.writeByte(0x01); // 请求
} else {
out.writeByte(0x02); // 响应
}
- // 3. 写入序列化器标识 (1字节)
+ // 写序列化器标识(1 字节)
out.writeByte(serializer.getCode());
- // 4. 获取序列化后的字节数组
+ // 序列化消息体
byte[] body = serializer.serialize(msg);
- // 5. 写入 Body 长度 (4字节)
+ // 写消息体长度(4 字节)
out.writeInt(body.length);
- // 6. 写入 Body 数据
+ // 写消息体内容
out.writeBytes(body);
}
-}
\ No newline at end of file
+}
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/netty/NettyProtocol.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/netty/NettyProtocol.java
similarity index 71%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/netty/NettyProtocol.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/netty/NettyProtocol.java
index ad54936..38d9dbe 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/netty/NettyProtocol.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/protocol/netty/NettyProtocol.java
@@ -17,21 +17,19 @@ public String getName() {
@Override
public void config(ChannelPipeline pipeline, boolean isServer, io.netty.channel.ChannelHandler serverHandler) {
- // 1. 获取配置
+ // 读取当前序列化配置
RpcConfig rpcConfig = RpcConfig.getInstance();
byte code = rpcConfig.getSerializerCode();
Serializer serializer = SerializerCode.getSerializerByCode(code);
- // 2. 判断解码类型
- // 如果是服务端(isServer=true),我要读 Request
- // 如果是客户端(isServer=false),我要读 Response
+ // 服务端解码 RpcRequest,客户端解码 RpcResponse
if (isServer) {
pipeline.addLast(new MyRpcDecoder(RpcRequest.class));
} else {
pipeline.addLast(new MyRpcDecoder(RpcResponse.class));
}
- // 3. 编码器 (收发都需要编码)
+ // 编码器在收发两侧都需要
pipeline.addLast(new MyRpcEncoder(serializer));
if (isServer && serverHandler != null) {
@@ -43,17 +41,14 @@ public void config(ChannelPipeline pipeline, boolean isServer, io.netty.channel.
public void sendRequest(io.netty.channel.Channel channel, RpcRequest request,
com.xiaoyu.rpc.core.client.NettyRpcClientHandler clientHandler) throws Exception {
// Netty 协议直接复用主通道
- // 如果 pipeline 里还没有 handler (第一次),加上它
- if (channel.pipeline().get(com.xiaoyu.rpc.core.client.NettyRpcClientHandler.class) != null) {
- channel.pipeline().replace(com.xiaoyu.rpc.core.client.NettyRpcClientHandler.class, "handler", clientHandler);
- } else {
- channel.pipeline().addLast("handler", clientHandler);
+ if (channel.pipeline().get(com.xiaoyu.rpc.core.client.NettyRpcClientHandler.class) == null) {
+ channel.pipeline().addLast(clientHandler);
}
channel.writeAndFlush(request).addListener(future -> {
if (!future.isSuccess()) {
- clientHandler.getFuture().completeExceptionally(future.cause());
+ clientHandler.failRequest(request.getRequestId(), future.cause());
}
});
}
-}
\ No newline at end of file
+}
diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/server/NettyRpcHandler.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/server/NettyRpcHandler.java
similarity index 82%
rename from rpc-core/src/main/java/com/xiaoyu/rpc/core/server/NettyRpcHandler.java
rename to rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/server/NettyRpcHandler.java
index 58e5528..e349d47 100644
--- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/server/NettyRpcHandler.java
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/server/NettyRpcHandler.java
@@ -20,25 +20,21 @@
@ChannelHandler.Sharable
public class NettyRpcHandler extends SimpleChannelInboundHandler {
- // 模拟注册中心
- private static final Map SERVICE_MAP = new ConcurrentHashMap<>();
-
- public static void registerService(String interfaceName, Object serviceBean) {
- SERVICE_MAP.put(interfaceName, serviceBean);
- }
+ // 移除内部 Map,改用 ServiceRepository
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequest request) throws Exception {
RpcResponse.Builder responseBuilder = RpcResponse.newBuilder();
+ responseBuilder.setRequestId(request.getRequestId());
try {
- // 1. 获取实现类
- Object serviceBean = SERVICE_MAP.get(request.getInterfaceName());
+ // 从 ServiceRepository 取到目标服务实现
+ Object serviceBean = ServiceRepository.getService(request.getInterfaceName());
if (serviceBean == null) {
throw new RuntimeException("未找到服务实现: " + request.getInterfaceName());
}
- // 2. 解析参数类型 (List -> Class>[])
+ // 将参数类型名还原为 Class>[]
// Proto 存的是类名字符串,我们需要反射还原成 Class 对象
List paramTypeNames = request.getParamTypesList();
Class>[] parameterTypes = new Class[paramTypeNames.size()];
@@ -47,7 +43,7 @@ protected void channelRead0(ChannelHandlerContext ctx, RpcRequest request) throw
parameterTypes[i] = Class.forName(paramTypeNames.get(i));
}
- // 3. 解析参数值 (List -> Object[])
+ // 将参数字节反序列化为方法入参
// Proto 存的是二进制,我们需要反序列化回 Java 对象
List paramByteList = request.getParametersList();
Object[] parameters = new Object[paramByteList.size()];
@@ -60,12 +56,12 @@ protected void channelRead0(ChannelHandlerContext ctx, RpcRequest request) throw
parameters[i] = serializer.deserialize(bytes, parameterTypes[i]);
}
- // 4. 反射调用
+ // 通过反射调用目标方法
Class> serviceClass = serviceBean.getClass();
Method method = serviceClass.getMethod(request.getMethodName(), parameterTypes);
Object result = method.invoke(serviceBean, parameters);
- // 5. 封装成功结果 (Object -> byte[] -> ByteString)
+ // 把返回值序列化后写入响应
byte[] resultBytes;
if (result == null) {
resultBytes = new byte[0];
@@ -83,7 +79,7 @@ protected void channelRead0(ChannelHandlerContext ctx, RpcRequest request) throw
responseBuilder.setData(ByteString.EMPTY);
}
- // 6. 发送响应
+ // 返回响应
ctx.writeAndFlush(responseBuilder.build());
}
-}
\ No newline at end of file
+}
diff --git a/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransport.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransport.java
new file mode 100644
index 0000000..9af75bb
--- /dev/null
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransport.java
@@ -0,0 +1,21 @@
+package com.xiaoyu.rpc.core.transport.netty;
+
+import com.xiaoyu.rpc.core.transport.Transport;
+import com.xiaoyu.rpc.core.transport.TransportClient;
+import com.xiaoyu.rpc.core.transport.TransportServer;
+
+/**
+ * Netty 传输层实现
+ */
+public class NettyTransport implements Transport {
+
+ @Override
+ public TransportServer createServer(int port) {
+ return new NettyTransportServer(port);
+ }
+
+ @Override
+ public TransportClient createClient() {
+ return new NettyTransportClient();
+ }
+}
diff --git a/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransportClient.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransportClient.java
new file mode 100644
index 0000000..8aedb45
--- /dev/null
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransportClient.java
@@ -0,0 +1,108 @@
+package com.xiaoyu.rpc.core.transport.netty;
+
+import com.xiaoyu.rpc.common.serialization.Serializer;
+import com.xiaoyu.rpc.common.serialization.SerializerCode;
+import com.xiaoyu.rpc.common.vo.RpcRequest;
+import com.xiaoyu.rpc.common.vo.RpcResponse;
+import com.xiaoyu.rpc.core.client.ChannelProvider;
+import com.xiaoyu.rpc.core.client.NettyRpcClientHandler;
+import com.xiaoyu.rpc.core.config.RpcConfig;
+import com.xiaoyu.rpc.core.protocol.Protocol;
+import com.xiaoyu.rpc.core.protocol.ProtocolFactory;
+import com.xiaoyu.rpc.core.transport.TransportClient;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import lombok.extern.slf4j.Slf4j;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class NettyTransportClient implements TransportClient {
+
+ private static volatile EventLoopGroup eventLoopGroup;
+ private static volatile Bootstrap bootstrap;
+
+ private static Bootstrap getBootstrap() {
+ if (bootstrap == null) {
+ synchronized (NettyTransportClient.class) {
+ if (bootstrap == null) {
+ // Bootstrap 和 EventLoopGroup 进程内复用,避免每次请求都创建线程池
+ eventLoopGroup = new NioEventLoopGroup();
+ Bootstrap newBootstrap = new Bootstrap();
+ newBootstrap.group(eventLoopGroup)
+ .channel(NioSocketChannel.class)
+ .handler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(SocketChannel ch) {
+ String protocolName = RpcConfig.getInstance().getProtocol();
+ Protocol protocol = ProtocolFactory.getProtocol(protocolName);
+ protocol.config(ch.pipeline(), false, null);
+ }
+ });
+ bootstrap = newBootstrap;
+ }
+ }
+ }
+ return bootstrap;
+ }
+
+ @Override
+ public CompletableFuture sendRequest(RpcRequest request, InetSocketAddress address) {
+ String protocolName = RpcConfig.getInstance().getProtocol();
+
+ try {
+ // 使用 ChannelProvider 获取连接
+ Channel channel = ChannelProvider.get(address, getBootstrap());
+ if (channel == null || !channel.isActive()) {
+ throw new RuntimeException("无法连接到服务器: " + address);
+ }
+
+ // Reuse handler from pipeline
+ NettyRpcClientHandler clientHandler = channel.pipeline().get(NettyRpcClientHandler.class);
+ if (clientHandler == null) {
+ // Should be added by initChannel, but for safety in some custom protocols:
+ clientHandler = new NettyRpcClientHandler();
+ channel.pipeline().addLast(clientHandler);
+ }
+
+ // Generate ID and set to request
+ // requestId 是客户端关联响应的关键键值,必须在发送前写入
+ String requestId = java.util.UUID.randomUUID().toString();
+ RpcRequest.Builder builder = request.toBuilder();
+ builder.setRequestId(requestId);
+ RpcRequest newRequest = builder.build();
+
+ CompletableFuture resultFuture = new CompletableFuture<>();
+ // 先注册 future 再发送,避免极端情况下响应先到导致找不到回调
+ clientHandler.addFuture(requestId, resultFuture);
+
+ Protocol protocol = ProtocolFactory.getProtocol(protocolName);
+ protocol.sendRequest(channel, newRequest, clientHandler);
+
+ // 彻底移除 resultFuture.get(),直接返回异步 Future
+ return resultFuture.thenApply(result -> {
+ if (result instanceof RpcResponse) {
+ RpcResponse rpcResponse = (RpcResponse) result;
+ if (!"Success".equals(rpcResponse.getMessage())) {
+ throw new RuntimeException("服务端报错: " + rpcResponse.getMessage());
+ }
+ return rpcResponse;
+ } else {
+ throw new RuntimeException("服务端返回的不是 RpcResponse 类型");
+ }
+ });
+ } catch (Exception e) {
+ log.error("RPC请求发起失败", e);
+ CompletableFuture future = new CompletableFuture<>();
+ future.completeExceptionally(e);
+ return future;
+ }
+ }
+}
diff --git a/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransportServer.java b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransportServer.java
new file mode 100644
index 0000000..f986517
--- /dev/null
+++ b/rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransportServer.java
@@ -0,0 +1,61 @@
+package com.xiaoyu.rpc.core.transport.netty;
+
+import com.xiaoyu.rpc.core.config.RpcConfig;
+import com.xiaoyu.rpc.core.protocol.Protocol;
+import com.xiaoyu.rpc.core.protocol.ProtocolFactory;
+import com.xiaoyu.rpc.core.server.NettyRpcHandler;
+import com.xiaoyu.rpc.core.transport.TransportServer;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class NettyTransportServer implements TransportServer {
+
+ private final int port;
+ private EventLoopGroup bossGroup;
+ private EventLoopGroup workerGroup;
+
+ public NettyTransportServer(int port) {
+ this.port = port;
+ }
+
+ @Override
+ public void start() throws InterruptedException {
+ // boss 负责接收连接,worker 负责连接上的读写事件
+ bossGroup = new NioEventLoopGroup();
+ workerGroup = new NioEventLoopGroup();
+ try {
+ ServerBootstrap b = new ServerBootstrap();
+ b.group(bossGroup, workerGroup)
+ .channel(NioServerSocketChannel.class)
+ .childHandler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(SocketChannel ch) {
+ // 协议实现通过配置切换,服务端业务处理统一复用 NettyRpcHandler
+ String protocolName = RpcConfig.getInstance().getProtocol();
+ Protocol protocol = ProtocolFactory.getProtocol(protocolName);
+ protocol.config(ch.pipeline(), true, new NettyRpcHandler());
+ }
+ });
+
+ log.info("RPC Server (Netty) started on port {}...", port);
+ b.bind(port).sync().channel().closeFuture().sync();
+ } finally {
+ stop();
+ }
+ }
+
+ @Override
+ public void stop() {
+ // shutdownGracefully 会等待队列任务处理后再退出,避免直接中断 I/O
+ if (bossGroup != null)
+ bossGroup.shutdownGracefully();
+ if (workerGroup != null)
+ workerGroup.shutdownGracefully();
+ }
+}
diff --git a/rpc-core/src/main/resources/META-INF/rpc/com.xiaoyu.rpc.core.protocol.Protocol b/rpc-transport-netty/src/main/resources/META-INF/rpc/com.xiaoyu.rpc.core.protocol.Protocol
similarity index 100%
rename from rpc-core/src/main/resources/META-INF/rpc/com.xiaoyu.rpc.core.protocol.Protocol
rename to rpc-transport-netty/src/main/resources/META-INF/rpc/com.xiaoyu.rpc.core.protocol.Protocol
diff --git a/rpc-transport-netty/src/main/resources/META-INF/rpc/com.xiaoyu.rpc.core.transport.Transport b/rpc-transport-netty/src/main/resources/META-INF/rpc/com.xiaoyu.rpc.core.transport.Transport
new file mode 100644
index 0000000..b008ffa
--- /dev/null
+++ b/rpc-transport-netty/src/main/resources/META-INF/rpc/com.xiaoyu.rpc.core.transport.Transport
@@ -0,0 +1 @@
+netty=com.xiaoyu.rpc.core.transport.netty.NettyTransport
diff --git a/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/client/NettyRpcClientHandlerTest.java b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/client/NettyRpcClientHandlerTest.java
new file mode 100644
index 0000000..7781c57
--- /dev/null
+++ b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/client/NettyRpcClientHandlerTest.java
@@ -0,0 +1,80 @@
+package com.xiaoyu.rpc.core.client;
+
+import com.google.protobuf.ByteString;
+import com.xiaoyu.rpc.common.vo.RpcResponse;
+import io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("NettyRpcClientHandler 并发回调测试")
+public class NettyRpcClientHandlerTest {
+
+ @Test
+ @DisplayName("按 requestId 路由并支持乱序响应")
+ void testRouteResponseByRequestId() throws Exception {
+ NettyRpcClientHandler handler = new NettyRpcClientHandler();
+ EmbeddedChannel channel = new EmbeddedChannel(handler);
+
+ CompletableFuture f1 = new CompletableFuture<>();
+ CompletableFuture f2 = new CompletableFuture<>();
+ handler.addFuture("req-1", f1);
+ handler.addFuture("req-2", f2);
+
+ channel.writeInbound(response("req-2", "ok-2"));
+ channel.writeInbound(response("req-1", "ok-1"));
+
+ RpcResponse r2 = (RpcResponse) f2.get(1, TimeUnit.SECONDS);
+ RpcResponse r1 = (RpcResponse) f1.get(1, TimeUnit.SECONDS);
+ assertEquals("ok-2", r2.getMessage());
+ assertEquals("ok-1", r1.getMessage());
+ }
+
+ @Test
+ @DisplayName("未知 requestId 的响应不会污染已挂起请求")
+ void testUnknownRequestId() {
+ NettyRpcClientHandler handler = new NettyRpcClientHandler();
+ EmbeddedChannel channel = new EmbeddedChannel(handler);
+
+ CompletableFuture future = new CompletableFuture<>();
+ handler.addFuture("known", future);
+
+ channel.writeInbound(response("unknown", "ignored"));
+ assertFalse(future.isDone(), "Unknown response should not complete unrelated future");
+ }
+
+ @Test
+ @DisplayName("连接异常时所有挂起请求应失败并清空")
+ void testExceptionCaughtFailsAllPending() {
+ NettyRpcClientHandler handler = new NettyRpcClientHandler();
+ EmbeddedChannel channel = new EmbeddedChannel(handler);
+
+ CompletableFuture f1 = new CompletableFuture<>();
+ CompletableFuture f2 = new CompletableFuture<>();
+ handler.addFuture("a", f1);
+ handler.addFuture("b", f2);
+
+ RuntimeException cause = new RuntimeException("boom");
+ channel.pipeline().fireExceptionCaught(cause);
+
+ assertTrue(f1.isCompletedExceptionally());
+ assertTrue(f2.isCompletedExceptionally());
+ assertFalse(channel.isActive(), "Channel should be closed on exception");
+
+ assertThrows(ExecutionException.class, f1::get);
+ assertThrows(ExecutionException.class, f2::get);
+ }
+
+ private static RpcResponse response(String requestId, String message) {
+ return RpcResponse.newBuilder()
+ .setRequestId(requestId)
+ .setMessage(message)
+ .setData(ByteString.EMPTY)
+ .build();
+ }
+}
diff --git a/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/ProtocolTest.java b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/ProtocolTest.java
new file mode 100644
index 0000000..e075d35
--- /dev/null
+++ b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/ProtocolTest.java
@@ -0,0 +1,93 @@
+package com.xiaoyu.rpc.core.protocol;
+
+import com.xiaoyu.rpc.core.protocol.http.HttpProtocol;
+import com.xiaoyu.rpc.core.protocol.netty.NettyProtocol;
+import com.xiaoyu.rpc.core.protocol.http2.Http2Protocol;
+import com.xiaoyu.rpc.core.protocol.grpc.GrpcProtocol;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 协议 SPI 单元测试
+ */
+@DisplayName("Protocol 协议测试")
+public class ProtocolTest {
+
+ @Test
+ @DisplayName("测试 Netty 协议加载")
+ void testNettyProtocolLoading() {
+ Protocol protocol = ProtocolFactory.getProtocol("netty");
+
+ assertNotNull(protocol, "Netty protocol should be loaded");
+ assertTrue(protocol instanceof NettyProtocol, "Should be instance of NettyProtocol");
+ }
+
+ @Test
+ @DisplayName("测试 HTTP 协议加载")
+ void testHttpProtocolLoading() {
+ Protocol protocol = ProtocolFactory.getProtocol("http");
+
+ assertNotNull(protocol, "HTTP protocol should be loaded");
+ assertTrue(protocol instanceof HttpProtocol, "Should be instance of HttpProtocol");
+ }
+
+ @Test
+ @DisplayName("测试 HTTP2 协议加载")
+ void testHttp2ProtocolLoading() {
+ Protocol protocol = ProtocolFactory.getProtocol("http2");
+
+ assertNotNull(protocol, "HTTP2 protocol should be loaded");
+ assertTrue(protocol instanceof Http2Protocol, "Should be instance of Http2Protocol");
+ }
+
+ @Test
+ @DisplayName("测试 gRPC 协议加载")
+ void testGrpcProtocolLoading() {
+ Protocol protocol = ProtocolFactory.getProtocol("grpc");
+
+ assertNotNull(protocol, "gRPC protocol should be loaded");
+ assertTrue(protocol instanceof GrpcProtocol, "Should be instance of GrpcProtocol");
+ }
+
+ @Test
+ @DisplayName("测试未知协议回退到 Netty")
+ void testUnknownProtocolFallback() {
+ Protocol protocol = ProtocolFactory.getProtocol("unknown_protocol");
+
+ assertNotNull(protocol, "Unknown protocol should fallback");
+ assertTrue(protocol instanceof NettyProtocol, "Should fallback to NettyProtocol");
+ }
+
+ @Test
+ @DisplayName("测试空协议名默认回退到 Netty")
+ void testNullProtocolFallback() {
+ Protocol protocol = ProtocolFactory.getProtocol(null);
+
+ assertNotNull(protocol, "Null protocol should fallback");
+ assertTrue(protocol instanceof NettyProtocol, "Null protocol should fallback to NettyProtocol");
+ }
+
+ @Test
+ @DisplayName("测试空白协议名默认回退到 Netty")
+ void testBlankProtocolFallback() {
+ Protocol protocol = ProtocolFactory.getProtocol(" ");
+
+ assertNotNull(protocol, "Blank protocol should fallback");
+ assertTrue(protocol instanceof NettyProtocol, "Blank protocol should fallback to NettyProtocol");
+ }
+
+ @Test
+ @DisplayName("测试 ProtocolFactory 多次获取相同协议")
+ void testProtocolCaching() {
+ Protocol first = ProtocolFactory.getProtocol("netty");
+ Protocol second = ProtocolFactory.getProtocol("netty");
+
+ assertNotNull(first, "First should not be null");
+ assertNotNull(second, "Second should not be null");
+ // 由于 SPI ExtensionLoader 使用缓存,两次获取应该是同一实例
+ assertSame(first, second, "Same protocol should return same instance");
+ }
+}
diff --git a/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcClientResponseHandlerTest.java b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcClientResponseHandlerTest.java
new file mode 100644
index 0000000..ca0808f
--- /dev/null
+++ b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcClientResponseHandlerTest.java
@@ -0,0 +1,77 @@
+package com.xiaoyu.rpc.core.protocol.grpc;
+
+import com.google.protobuf.ByteString;
+import com.xiaoyu.rpc.common.vo.RpcResponse;
+import com.xiaoyu.rpc.core.client.NettyRpcClientHandler;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http2.DefaultHttp2DataFrame;
+import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame;
+import io.netty.handler.codec.http2.Http2Headers;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("gRPC 客户端响应处理器测试")
+class GrpcClientResponseHandlerTest {
+
+ @Test
+ @DisplayName("应把 DataFrame 解码为 RpcResponse 并完成 future")
+ void testDecodeDataFrameToRpcResponse() throws Exception {
+ NettyRpcClientHandler clientHandler = new NettyRpcClientHandler();
+ EmbeddedChannel channel = new EmbeddedChannel(
+ new GrpcClientResponseHandler(clientHandler, "req-1"),
+ clientHandler);
+
+ CompletableFuture future = new CompletableFuture<>();
+ clientHandler.addFuture("req-1", future);
+
+ RpcResponse response = RpcResponse.newBuilder()
+ .setRequestId("req-1")
+ .setMessage("Success")
+ .setData(ByteString.copyFromUtf8("ok"))
+ .build();
+
+ byte[] payload = response.toByteArray();
+ ByteBuf buf = Unpooled.buffer();
+ buf.writeByte(0);
+ buf.writeInt(payload.length);
+ buf.writeBytes(payload);
+
+ channel.writeInbound(new DefaultHttp2DataFrame(buf, true));
+
+ assertTrue(future.isDone(), "Future should be completed");
+ Object result = future.get();
+ assertInstanceOf(RpcResponse.class, result);
+ assertEquals("req-1", ((RpcResponse) result).getRequestId());
+ assertEquals("Success", ((RpcResponse) result).getMessage());
+ }
+
+ @Test
+ @DisplayName("收到 grpc-status 非 0 时应异常完成 future")
+ void testFailFutureOnGrpcErrorStatus() {
+ NettyRpcClientHandler clientHandler = new NettyRpcClientHandler();
+ EmbeddedChannel channel = new EmbeddedChannel(
+ new GrpcClientResponseHandler(clientHandler, "req-2"),
+ clientHandler);
+
+ CompletableFuture future = new CompletableFuture<>();
+ clientHandler.addFuture("req-2", future);
+
+ Http2Headers trailers = new io.netty.handler.codec.http2.DefaultHttp2Headers()
+ .set("grpc-status", "13")
+ .set("grpc-message", "internal");
+ channel.writeInbound(new DefaultHttp2HeadersFrame(trailers, true));
+
+ assertTrue(future.isCompletedExceptionally(), "Future should be completed exceptionally");
+ ExecutionException ex = assertThrows(ExecutionException.class, future::get);
+ String message = ex.getCause().getMessage();
+ assertNotNull(message, "Error message should not be null");
+ assertTrue(message.toLowerCase().contains("grpc"), "Error message should include grpc details");
+ }
+}
diff --git a/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcServerHandlerTest.java b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcServerHandlerTest.java
new file mode 100644
index 0000000..10d1567
--- /dev/null
+++ b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcServerHandlerTest.java
@@ -0,0 +1,87 @@
+package com.xiaoyu.rpc.core.protocol.grpc;
+
+import com.google.protobuf.ByteString;
+import com.xiaoyu.rpc.common.vo.RpcRequest;
+import com.xiaoyu.rpc.common.vo.RpcResponse;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http2.DefaultHttp2DataFrame;
+import io.netty.handler.codec.http2.Http2DataFrame;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.handler.codec.http2.Http2HeadersFrame;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("gRPC 服务端处理器帧测试")
+public class GrpcServerHandlerTest {
+
+ @Test
+ @DisplayName("数据帧不足 5 字节前缀时忽略")
+ void testIgnoreShortDataFrame() {
+ EmbeddedChannel channel = new EmbeddedChannel(new GrpcServerHandler(new ChannelInboundHandlerAdapter()));
+ Http2DataFrame frame = new DefaultHttp2DataFrame(Unpooled.wrappedBuffer(new byte[] { 1, 2, 3, 4 }), true);
+
+ channel.writeInbound(frame);
+ assertNull(channel.readInbound(), "Short frame should not produce RpcRequest");
+ }
+
+ @Test
+ @DisplayName("完整 gRPC 数据帧可还原 RpcRequest")
+ void testDecodeGrpcRequestFrame() {
+ EmbeddedChannel channel = new EmbeddedChannel(new GrpcServerHandler(new ChannelInboundHandlerAdapter()));
+ RpcRequest request = RpcRequest.newBuilder()
+ .setInterfaceName("com.example.DemoService")
+ .setMethodName("hello")
+ .build();
+ byte[] payload = request.toByteArray();
+
+ ByteBuf buf = Unpooled.buffer();
+ buf.writeByte(0);
+ buf.writeInt(payload.length);
+ buf.writeBytes(payload);
+
+ channel.writeInbound(new DefaultHttp2DataFrame(buf, true));
+ Object inbound = channel.readInbound();
+
+ assertInstanceOf(RpcRequest.class, inbound);
+ assertEquals("hello", ((RpcRequest) inbound).getMethodName());
+ }
+
+ @Test
+ @DisplayName("RpcResponse 写出时应包含 headers/data/trailers")
+ void testWriteRpcResponseAsGrpcFrames() {
+ EmbeddedChannel channel = new EmbeddedChannel(new GrpcServerHandler(new ChannelInboundHandlerAdapter()));
+ RpcResponse response = RpcResponse.newBuilder()
+ .setRequestId("req-1")
+ .setMessage("Success")
+ .setData(ByteString.copyFromUtf8("ok"))
+ .build();
+
+ assertTrue(channel.writeOutbound(response));
+
+ Object first = channel.readOutbound();
+ Object second = channel.readOutbound();
+ Object third = channel.readOutbound();
+
+ assertInstanceOf(Http2HeadersFrame.class, first);
+ assertInstanceOf(Http2DataFrame.class, second);
+ assertInstanceOf(Http2HeadersFrame.class, third);
+
+ Http2Headers headers = ((Http2HeadersFrame) first).headers();
+ assertEquals("200", headers.status().toString());
+ assertEquals("application/grpc", headers.get("content-type").toString());
+
+ ByteBuf data = ((Http2DataFrame) second).content();
+ assertEquals(0, data.readByte(), "gRPC compressed flag should be 0");
+ int len = data.readInt();
+ assertTrue(len > 0, "Payload length should be positive");
+
+ Http2Headers trailers = ((Http2HeadersFrame) third).headers();
+ assertEquals("0", trailers.get("grpc-status").toString());
+ assertTrue(((Http2HeadersFrame) third).isEndStream(), "Trailer frame should end stream");
+ }
+}
diff --git a/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcCodecTest.java b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcCodecTest.java
new file mode 100644
index 0000000..9ed3266
--- /dev/null
+++ b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/http/HttpRpcCodecTest.java
@@ -0,0 +1,72 @@
+package com.xiaoyu.rpc.core.protocol.http;
+
+import com.google.protobuf.ByteString;
+import com.xiaoyu.rpc.common.extension.ExtensionLoader;
+import com.xiaoyu.rpc.common.serialization.Serializer;
+import com.xiaoyu.rpc.common.vo.RpcRequest;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpVersion;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("HTTP 编解码边界测试")
+public class HttpRpcCodecTest {
+
+ @Test
+ @DisplayName("RpcRequest 编码为 HTTP POST 报文")
+ void testEncodeRequest() {
+ Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("java");
+ EmbeddedChannel channel = new EmbeddedChannel(new HttpRpcEncoder(serializer));
+
+ RpcRequest request = RpcRequest.newBuilder()
+ .setInterfaceName("com.example.DemoService")
+ .setMethodName("hello")
+ .addParamTypes("java.lang.String")
+ .addParameters(ByteString.copyFrom(serializer.serialize("world")))
+ .build();
+
+ assertTrue(channel.writeOutbound(request));
+ Object outbound = channel.readOutbound();
+ assertInstanceOf(FullHttpRequest.class, outbound);
+
+ FullHttpRequest httpRequest = (FullHttpRequest) outbound;
+ assertEquals(HttpMethod.POST, httpRequest.method());
+ assertEquals("/", httpRequest.uri());
+ assertEquals("hello", httpRequest.headers().get("Rpc-Method"));
+ assertEquals("application/x-rpc", httpRequest.headers().get("Content-Type"));
+
+ byte[] body = bytes(httpRequest.content());
+ RpcRequest decoded = serializer.deserialize(body, RpcRequest.class);
+ assertEquals("hello", decoded.getMethodName());
+ }
+
+ @Test
+ @DisplayName("HTTP 响应解码为目标对象")
+ void testDecodeResponse() {
+ Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("java");
+ EmbeddedChannel channel = new EmbeddedChannel(new HttpRpcDecoder(serializer, String.class));
+
+ byte[] body = serializer.serialize("pong");
+ FullHttpResponse response = new io.netty.handler.codec.http.DefaultFullHttpResponse(
+ HttpVersion.HTTP_1_1,
+ HttpResponseStatus.OK,
+ io.netty.buffer.Unpooled.wrappedBuffer(body));
+
+ assertTrue(channel.writeInbound(response));
+ Object inbound = channel.readInbound();
+ assertEquals("pong", inbound);
+ }
+
+ private static byte[] bytes(ByteBuf buf) {
+ byte[] bytes = new byte[buf.readableBytes()];
+ buf.getBytes(buf.readerIndex(), bytes);
+ return bytes;
+ }
+}
diff --git a/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcCodecTest.java b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcCodecTest.java
new file mode 100644
index 0000000..ab80176
--- /dev/null
+++ b/rpc-transport-netty/src/test/java/com/xiaoyu/rpc/core/protocol/netty/MyRpcCodecTest.java
@@ -0,0 +1,74 @@
+package com.xiaoyu.rpc.core.protocol.netty;
+
+import com.google.protobuf.ByteString;
+import com.xiaoyu.rpc.common.extension.ExtensionLoader;
+import com.xiaoyu.rpc.common.serialization.Serializer;
+import com.xiaoyu.rpc.common.serialization.SerializerCode;
+import com.xiaoyu.rpc.common.vo.RpcRequest;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("自定义 Netty 协议编解码测试")
+public class MyRpcCodecTest {
+
+ @Test
+ @DisplayName("请求报文编码后可被解码还原")
+ void testRoundTripRequest() {
+ Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("java");
+ EmbeddedChannel encoderChannel = new EmbeddedChannel(new MyRpcEncoder(serializer));
+ EmbeddedChannel decoderChannel = new EmbeddedChannel(new MyRpcDecoder(RpcRequest.class));
+
+ RpcRequest request = RpcRequest.newBuilder()
+ .setInterfaceName("com.example.DemoService")
+ .setMethodName("echo")
+ .addParamTypes("java.lang.String")
+ .addParameters(ByteString.copyFrom(serializer.serialize("abc")))
+ .build();
+
+ assertTrue(encoderChannel.writeOutbound(request));
+ ByteBuf encoded = encoderChannel.readOutbound();
+ assertTrue(decoderChannel.writeInbound(encoded));
+
+ Object decoded = decoderChannel.readInbound();
+ assertInstanceOf(RpcRequest.class, decoded);
+ assertEquals("echo", ((RpcRequest) decoded).getMethodName());
+ }
+
+ @Test
+ @DisplayName("非法魔数应抛异常")
+ void testInvalidMagicNumber() {
+ EmbeddedChannel channel = new EmbeddedChannel(new MyRpcDecoder(RpcRequest.class));
+ ByteBuf invalid = Unpooled.buffer();
+ invalid.writeInt(0x11223344);
+ invalid.writeByte(0x01);
+ invalid.writeByte(SerializerCode.JAVA_SERIALIZER);
+ invalid.writeInt(0);
+
+ assertThrows(RuntimeException.class, () -> {
+ channel.writeInbound(invalid);
+ channel.checkException();
+ });
+ }
+
+ @Test
+ @DisplayName("超长消息应被拒绝并关闭连接")
+ void testOversizedFrameRejected() {
+ EmbeddedChannel channel = new EmbeddedChannel(new MyRpcDecoder(RpcRequest.class));
+ ByteBuf invalid = Unpooled.buffer();
+ invalid.writeInt(0xAABBCCDD);
+ invalid.writeByte(0x01);
+ invalid.writeByte(SerializerCode.JAVA_SERIALIZER);
+ invalid.writeInt(100 * 1024 * 1024);
+
+ assertThrows(RuntimeException.class, () -> {
+ channel.writeInbound(invalid);
+ channel.checkException();
+ });
+ assertFalse(channel.isOpen(), "Channel should be closed after oversized frame");
+ }
+}