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. - -![gRPC Data Processing Flow](docs/images/grpc_processing_flow.png) - -### 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. + +![CI/CD Workflow Result](docs/images/image.png) + + +--- + +## 🔬 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. + +![gRPC Data Processing Flow](docs/images/grpc_processing_flow.png) + +### 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** 协议。 + +![Java](https://img.shields.io/badge/Java-17%2B-blue?style=flat-square&logo=java) +![Netty](https://img.shields.io/badge/Netty-4.1.x-green?style=flat-square) +![Nacos](https://img.shields.io/badge/Nacos-2.x-orange?style=flat-square) +![License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square) + +--- + +## 📖 简介 + +本项目是一个旨在演示标准协议与动态调用融合的高性能插件化 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 提供代码健康度的即时反馈。 + +![CI/CD Workflow Result](docs/images/image.png) + +--- + +## 🔬 技术深度解析: gRPC 协议实现 + +XiaoYu RPC 互操作性的核心在于基于 Netty HTTP/2 栈自定义实现的 gRPC 传输协议。 + +![gRPC Data Processing Flow](docs/images/grpc_processing_flow.png) + +### 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 holder = cachedInstances.get(name); if (holder == null) { cachedInstances.putIfAbsent(name, new Holder<>()); holder = cachedInstances.get(name); } - // 2. 双重检查锁创建实例 + // 实例按需创建,使用双重检查避免重复初始化 Object instance = holder.get(); if (instance == null) { synchronized (holder) { diff --git a/rpc-common/src/main/proto/rpc_meta.proto b/rpc-common/src/main/proto/rpc_meta.proto index a047375..c53164a 100644 --- a/rpc-common/src/main/proto/rpc_meta.proto +++ b/rpc-common/src/main/proto/rpc_meta.proto @@ -1,11 +1,11 @@ syntax = "proto3"; -option java_package = "com.xiaoyu.rpc.common.vo";//对应的路径名称 +option java_package = "com.xiaoyu.rpc.common.vo"; // 生成 Java 类的包名 -// 2. 必须设为 true,这样才会生成 RpcRequest.java 和 RpcResponse.java 两个文件 +// 拆分生成多个 Java 文件,避免全部消息挤在一个类里 option java_multiple_files = true; -// 3. 对应你的 RpcRequest +// 请求对象 message RpcRequest { // 对应 String interfaceName string interface_name = 1; @@ -21,9 +21,12 @@ message RpcRequest { // Proto存不了Object,必须存成二进制(bytes)。 // 这里用 repeated 表示数组 repeated bytes parameters = 4; + + // 请求ID,用于多路复用 + string request_id = 5; } -// 4. 对应你的 RpcResponse +// 响应对象 message RpcResponse { // 对应 Object data // 同样无法存Object,只能存序列化后的二进制 @@ -32,8 +35,10 @@ message RpcResponse { // 对应 String message string message = 2; + // 请求ID,用于多路复用 + string request_id = 3; } service GrpcService { rpc handle (RpcRequest) returns (RpcResponse); -} \ No newline at end of file +} diff --git a/rpc-consumer/pom.xml b/rpc-consumer/pom.xml index 4469038..dbb4e79 100644 --- a/rpc-consumer/pom.xml +++ b/rpc-consumer/pom.xml @@ -20,6 +20,11 @@ com.xiaoyu.rpc rpc-core + + 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 future = rpcClient.sendRequest(request, method.getReturnType()); + // 如果业务接口声明的返回类型是异步的,直接返回 Future;否则阻塞等待结果 + if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) { + return future; + } + return future.get(); } - })) - .make() - .load(clazz.getClassLoader()) - .getLoaded() - .getConstructor() - .newInstance(); + })).make().load(clazz.getClassLoader()).getLoaded().getConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("ByteBuddy代理创建失败", e); } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/JdkProxyFactory.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/JdkProxyFactory.java index 788e7c4..1f30ac4 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/JdkProxyFactory.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/JdkProxyFactory.java @@ -9,6 +9,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.concurrent.CompletableFuture; public class JdkProxyFactory implements ProxyFactory { @@ -43,7 +44,12 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } RpcRequest request = builder.build(); - return new RpcClient().sendRequest(request, method.getReturnType()); + CompletableFuture future = new RpcClient().sendRequest(request, method.getReturnType()); + // 如果业务接口声明的返回类型是异步的,直接返回 Future;否则阻塞等待结果 + if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) { + return future; + } + return future.get(); } }); } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/NettyRpcClientHandler.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/NettyRpcClientHandler.java deleted file mode 100644 index 4762c73..0000000 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/NettyRpcClientHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.xiaoyu.rpc.core.client; - -import com.xiaoyu.rpc.common.vo.RpcResponse; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.CompletableFuture; - -// 这是一个 Netty 的 Handler,专门负责“收信” -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -// 这是一个 Netty 的 Handler,专门负责“收信” -public class NettyRpcClientHandler extends SimpleChannelInboundHandler { - private static final Logger log = LoggerFactory.getLogger(NettyRpcClientHandler.class); - - private CompletableFuture future; - - public void setFuture(CompletableFuture future) { - this.future = future; - } - - @Override - protected void channelRead0(ChannelHandlerContext ctx, RpcResponse response) { - // 【关键修复点】 - // 之前你写的是 future.complete(response.getData()); 导致传回去的是 ByteString - // 现在我们把整个 response 对象传回去,让 Proxy 去判断状态和拆包 - log.info("客户端收到响应状态: {}", response.getMessage()); - future.complete(response); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - ctx.close(); - } - - public CompletableFuture getFuture() { - return future; - } -} \ No newline at end of file diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/RpcClient.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/RpcClient.java index 30c9fa8..3dd96ab 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/RpcClient.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/RpcClient.java @@ -1,90 +1,69 @@ package com.xiaoyu.rpc.core.client; -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.common.extension.ExtensionLoader; +import com.xiaoyu.rpc.common.vo.RpcRequest; import com.xiaoyu.rpc.core.config.RpcConfig; -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 com.xiaoyu.rpc.core.protocol.Protocol; -import com.xiaoyu.rpc.core.protocol.ProtocolFactory; import com.xiaoyu.rpc.core.registry.ServiceDiscovery; +import com.xiaoyu.rpc.core.transport.Transport; +import com.xiaoyu.rpc.core.transport.TransportClient; import java.net.InetSocketAddress; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; +import java.util.Objects; public class RpcClient { - private static final EventLoopGroup eventLoopGroup; - private static final Bootstrap bootstrap; + private final TransportClient transportClient; + private final ServiceDiscovery serviceDiscovery; + + public RpcClient() { + RpcConfig config = RpcConfig.getInstance(); + // 初始化服务发现 + this.serviceDiscovery = ExtensionLoader.getExtensionLoader(ServiceDiscovery.class) + .getExtension(config.getRegistryType()); - static { - eventLoopGroup = new NioEventLoopGroup(); - bootstrap = new Bootstrap(); - bootstrap.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); - } - }); + // 初始化传输层客户端 + Transport transport = ExtensionLoader.getExtensionLoader(Transport.class).getExtension(config.getTransport()); + this.transportClient = transport.createClient(); } - public Object sendRequest(RpcRequest request, Class returnType) { - String protocolName = RpcConfig.getInstance().getProtocol(); - NettyRpcClientHandler clientHandler = new NettyRpcClientHandler(); + RpcClient(TransportClient transportClient, ServiceDiscovery serviceDiscovery) { + this.transportClient = Objects.requireNonNull(transportClient, "transportClient"); + this.serviceDiscovery = Objects.requireNonNull(serviceDiscovery, "serviceDiscovery"); + } + public java.util.concurrent.CompletableFuture sendRequest(RpcRequest request, Class returnType) { try { - ServiceDiscovery serviceDiscovery = ExtensionLoader.getExtensionLoader(ServiceDiscovery.class) - .getExtension(RpcConfig.getInstance().getRegistryType()); + // 先做一次服务发现(同步查找,通常会命中本地缓存) InetSocketAddress address = serviceDiscovery.lookupService(request.getInterfaceName()); if (address == null) { - throw new RuntimeException("未发现服务: " + request.getInterfaceName()); - } - - // 使用 ChannelProvider 获取连接 - Channel channel = ChannelProvider.get(address, bootstrap); - if (channel == null || !channel.isActive()) { - throw new RuntimeException("无法连接到服务器: " + address); + java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); + future.completeExceptionally(new RuntimeException("未发现服务: " + request.getInterfaceName())); + return future; } - CompletableFuture resultFuture = new CompletableFuture<>(); - clientHandler.setFuture(resultFuture); - - Protocol protocol = ProtocolFactory.getProtocol(protocolName); - protocol.sendRequest(channel, request, clientHandler); + // 交给传输层发送,返回异步 Future + java.util.concurrent.CompletableFuture transportFuture = transportClient.sendRequest(request, + address); - Object result = resultFuture.get(5, TimeUnit.SECONDS); + // 在回调里把响应体反序列化成目标返回类型 + return transportFuture.thenApply(result -> { + if (result instanceof com.xiaoyu.rpc.common.vo.RpcResponse) { + com.xiaoyu.rpc.common.vo.RpcResponse response = (com.xiaoyu.rpc.common.vo.RpcResponse) result; - if (result instanceof RpcResponse) { - RpcResponse rpcResponse = (RpcResponse) result; + byte[] data = response.getData().toByteArray(); - if (!"Success".equals(rpcResponse.getMessage())) { - throw new RuntimeException("服务端报错: " + rpcResponse.getMessage()); + com.xiaoyu.rpc.common.serialization.Serializer serializer = com.xiaoyu.rpc.common.serialization.SerializerCode + .getSerializerByCode(RpcConfig.getInstance().getSerializerCode()); + return serializer.deserialize(data, returnType); } - - byte[] data = rpcResponse.getData().toByteArray(); - - Serializer serializer = SerializerCode.getSerializerByCode(RpcConfig.getInstance().getSerializerCode()); - return serializer.deserialize(data, returnType); - } else { - throw new RuntimeException("服务端返回的不是 RpcResponse 类型"); - } + throw new RuntimeException("Unexpected response type: " + result.getClass()); + }); } catch (Exception e) { - throw new RuntimeException("RPC请求发送失败", e); + java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); + future.completeExceptionally(e); + return future; } } } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/RpcClientProxy.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/RpcClientProxy.java index 47bd130..6bf87db 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/RpcClientProxy.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/client/RpcClientProxy.java @@ -6,8 +6,9 @@ public class RpcClientProxy { public static T create(Class clazz) { + // 代理类型由配置决定(jdk / bytebuddy 等),这里统一走 SPI 扩展点加载 String proxyType = RpcConfig.getInstance().getProxyType(); ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getExtension(proxyType); return proxyFactory.getProxy(clazz); } -} \ No newline at end of file +} diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/config/RpcConfig.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/config/RpcConfig.java index b81bd30..a408e58 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/config/RpcConfig.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/config/RpcConfig.java @@ -32,6 +32,10 @@ public class RpcConfig { private String proxyType = "jdk"; // 负载均衡器 private String loadBalancer = "roundrobin"; + // 传输层类型 + private String transport = "netty"; + // 最大报文长度 + private Integer maxMessageSize = 8 * 1024 * 1024; private RpcConfig() { loadConfig(); @@ -71,9 +75,12 @@ private void loadConfig() { this.registryType = (String) rpcConfig.getOrDefault("registry", "nacos"); this.proxyType = (String) rpcConfig.getOrDefault("proxy", "jdk"); this.loadBalancer = (String) rpcConfig.getOrDefault("load-balancer", "roundrobin"); + this.transport = (String) rpcConfig.getOrDefault("transport", "netty"); + this.maxMessageSize = (Integer) rpcConfig.getOrDefault("max-message-size", 8 * 1024 * 1024); - log.info("配置加载成功: 序列化方式={}, 服务器={}:{},使用的协议={}, 注册中心={}, 代理方式={}, 负载均衡={}", - serializerType, serverHost, serverPort, protocol, registryAddress, proxyType, loadBalancer); + log.info("配置加载成功: 序列化方式={}, 服务器={}:{},使用的协议={}, 注册中心={}, 代理方式={}, 负载均衡={}, 传输层={}, 最大报文={}", + serializerType, serverHost, serverPort, protocol, registryAddress, proxyType, loadBalancer, + transport, maxMessageSize); } else { log.warn("配置文件格式错误,使用默认配置"); setDefaultConfig(); @@ -105,6 +112,18 @@ private void loadConfig() { this.serializerType = serializerStr; log.info("检测到 System Property 覆盖序列化方式: {}", this.serializerType); } + + String transportStr = System.getProperty("rpc.transport"); + if (transportStr != null) { + this.transport = transportStr; + log.info("检测到 System Property 覆盖传输层: {}", this.transport); + } + + String protocolStr = System.getProperty("rpc.protocol"); + if (protocolStr != null) { + this.protocol = protocolStr; + log.info("检测到 System Property 覆盖协议: {}", this.protocol); + } } /** @@ -115,6 +134,8 @@ private void setDefaultConfig() { this.serverPort = 8080; this.serverHost = "127.0.0.1"; this.protocol = "netty"; + this.transport = "netty"; + this.maxMessageSize = 8 * 1024 * 1024; } /** @@ -158,6 +179,14 @@ public String getLoadBalancer() { return loadBalancer; } + public String getTransport() { + return transport; + } + + public Integer getMaxMessageSize() { + return maxMessageSize; + } + @Override public String toString() { return "RpcConfig{" + @@ -169,6 +198,8 @@ public String toString() { ", registryType='" + registryType + '\'' + ", proxyType='" + proxyType + '\'' + ", loadBalancer='" + loadBalancer + '\'' + + ", transport='" + transport + '\'' + + ", maxMessageSize=" + maxMessageSize + '}'; } } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcProtocol.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcProtocol.java deleted file mode 100644 index 510d7f2..0000000 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/protocol/grpc/GrpcProtocol.java +++ /dev/null @@ -1,46 +0,0 @@ -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.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.http2.Http2FrameCodecBuilder; -import io.netty.handler.codec.http2.Http2MultiplexHandler; - -public class GrpcProtocol implements Protocol { - - @Override - public String getName() { - return "grpc"; - } - - @Override - public void config(ChannelPipeline pipeline, boolean isServer, ChannelHandler serverHandler) { - if (isServer) { - // 1. Http2FrameCodec (处理握手、并转为 Frame 对象) - pipeline.addLast(Http2FrameCodecBuilder.forServer().build()); - - // 2. 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 { - throw new UnsupportedOperationException("Client side grpc not supported yet"); - } - } - - @Override - public void sendRequest(Channel channel, RpcRequest request, NettyRpcClientHandler clientHandler) throws Exception { - throw new UnsupportedOperationException("Client side generic grpc not supported yet"); - } -} diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/LocalRegistry.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/LocalRegistry.java index 24a7874..3185946 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/LocalRegistry.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/LocalRegistry.java @@ -19,4 +19,10 @@ public InetSocketAddress lookupService(String serviceName) { System.out.println("LocalRegistry: Looking up " + serviceName); return SERVICES.get(serviceName); } + + @Override + public void clearRegistry() { + SERVICES.clear(); + System.out.println("LocalRegistry: Cleared all services."); + } } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/ServiceRegistry.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/ServiceRegistry.java index 97dda86..a0b0992 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/ServiceRegistry.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/ServiceRegistry.java @@ -16,4 +16,9 @@ public interface ServiceRegistry { * @param inetSocketAddress 服务地址 */ void registerService(String serviceName, InetSocketAddress inetSocketAddress); + + /** + * 注销所有服务 (用于优雅下线) + */ + void clearRegistry(); } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceDiscovery.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceDiscovery.java index d580772..c53ebda 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceDiscovery.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceDiscovery.java @@ -3,37 +3,67 @@ import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.naming.NamingService; import com.alibaba.nacos.api.naming.pojo.Instance; -import lombok.extern.slf4j.Slf4j; -import com.xiaoyu.rpc.core.registry.ServiceDiscovery; - -import java.net.InetSocketAddress; -import com.xiaoyu.rpc.core.config.RpcConfig; +import com.alibaba.nacos.api.naming.listener.EventListener; +import com.alibaba.nacos.api.naming.listener.Event; +import com.alibaba.nacos.api.naming.listener.NamingEvent; import com.xiaoyu.rpc.common.extension.ExtensionLoader; +import com.xiaoyu.rpc.core.config.RpcConfig; import com.xiaoyu.rpc.core.loadbalancer.LoadBalancer; -import java.net.InetSocketAddress; -import java.util.List; - +import com.xiaoyu.rpc.core.registry.ServiceDiscovery; +import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + public class NacosServiceDiscovery implements ServiceDiscovery { private static final Logger log = LoggerFactory.getLogger(NacosServiceDiscovery.class); private final NamingService namingService; private final LoadBalancer loadBalancer; + // 本地缓存,用于容错和防抖 + private static final java.util.Map> serviceCache = new java.util.concurrent.ConcurrentHashMap<>(); + // 已订阅的服务集合 + private static final java.util.Set subscribedServices = java.util.concurrent.ConcurrentHashMap.newKeySet(); public NacosServiceDiscovery() { - this.namingService = NacosUtils.getNacosNamingService(); - String loadBalancerCode = RpcConfig.getInstance().getLoadBalancer(); - this.loadBalancer = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getExtension(loadBalancerCode); + this(NacosUtils.getNacosNamingService(), + ExtensionLoader.getExtensionLoader(LoadBalancer.class) + .getExtension(RpcConfig.getInstance().getLoadBalancer())); + } + + NacosServiceDiscovery(NamingService namingService, LoadBalancer loadBalancer) { + this.namingService = namingService; + this.loadBalancer = loadBalancer; } @Override public InetSocketAddress lookupService(String serviceName) { try { + // 第一次查找时订阅服务变更 + if (subscribedServices.add(serviceName)) { + // add 返回 true 说明此前未订阅,避免同一个服务被重复订阅 + subscribeService(serviceName); + } + + // 优先从 Nacos 拉取最新实例列表 List instances = namingService.getAllInstances(serviceName); - if (instances.size() == 0) { - log.error("未找到服务: {}", serviceName); + + if (instances.isEmpty()) { + log.warn("Nacos 返回实例列表为空,尝试使用本地缓存: {}", serviceName); + instances = serviceCache.get(serviceName); + } else { + // 更新本地缓存 + serviceCache.put(serviceName, instances); + } + + if (instances == null || instances.isEmpty()) { + log.error("未找到服务且本地无缓存: {}", serviceName); throw new RuntimeException("未找到服务: " + serviceName); } @@ -47,13 +77,40 @@ public InetSocketAddress lookupService(String serviceName) { log.info("负载均衡选择服务地址: {}", targetAddress); String[] array = targetAddress.split(":"); - String host = array[0]; - int port = Integer.parseInt(array[1]); + return new InetSocketAddress(array[0], Integer.parseInt(array[1])); - return new InetSocketAddress(host, port); } catch (NacosException e) { - log.error("获取服务实例时发生错误:", e); + log.error("获取服务实例时发生网络异常,尝试回滚到本地缓存:", e); + // Nacos 短暂不可用时,优先用最近一次成功拉取到的实例兜底 + List cachedInstances = serviceCache.get(serviceName); + if (cachedInstances != null && !cachedInstances.isEmpty()) { + List addressList = cachedInstances.stream() + .map(instance -> instance.getIp() + ":" + instance.getPort()) + .collect(java.util.stream.Collectors.toList()); + String targetAddress = loadBalancer.select(addressList); + String[] array = targetAddress.split(":"); + return new InetSocketAddress(array[0], Integer.parseInt(array[1])); + } + throw new RuntimeException("服务发现失败且无缓存可用: " + serviceName, e); } - return null; + } + + /** + * 订阅服务变更,实现本地缓存的实时更新 + */ + private void subscribeService(String serviceName) throws NacosException { + namingService.subscribe(serviceName, new EventListener() { + @Override + public void onEvent(Event event) { + if (event instanceof NamingEvent) { + NamingEvent namingEvent = (NamingEvent) event; + List instances = namingEvent.getInstances(); + log.info("监听到服务变更,更新本地缓存: {} -> 实例数 {}", serviceName, instances.size()); + if (instances != null && !instances.isEmpty()) { + serviceCache.put(serviceName, instances); + } + } + } + }); } } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceRegistry.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceRegistry.java index 91186f5..67a13ca 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceRegistry.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceRegistry.java @@ -22,4 +22,9 @@ public void registerService(String serviceName, InetSocketAddress inetSocketAddr throw new RuntimeException("注册服务失败", e); } } + + @Override + public void clearRegistry() { + NacosUtils.clearRegistry(); + } } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosUtils.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosUtils.java index a5704fa..bbc3708 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosUtils.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/registry/nacos/NacosUtils.java @@ -29,8 +29,7 @@ public class NacosUtils { public static NamingService getNacosNamingService() { try { - // 从配置中获取 Nacos 地址,暂时先硬编码或者后续从 RpcConfig 获取 - // 这里我们先假定 RpcConfig 会提供 registryAddress,如果没提供就默认 + // 优先读取配置中的 Nacos 地址,未配置时退回本地默认地址 String registryAddress = RpcConfig.getInstance().getRegistryAddress(); if (registryAddress == null || registryAddress.isEmpty()) { registryAddress = "127.0.0.1:8848"; diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/serialization/JsonSerializerImpl.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/serialization/JsonSerializerImpl.java index 9c97acd..90ff37d 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/serialization/JsonSerializerImpl.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/serialization/JsonSerializerImpl.java @@ -12,6 +12,25 @@ public class JsonSerializerImpl implements Serializer { public JsonSerializerImpl() { this.gson = new GsonBuilder() + .registerTypeHierarchyAdapter(com.google.protobuf.ByteString.class, + new TypeAdapter() { + @Override + public void write(com.google.gson.stream.JsonWriter out, + com.google.protobuf.ByteString value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + out.value(java.util.Base64.getEncoder().encodeToString(value.toByteArray())); + } + + @Override + public com.google.protobuf.ByteString read(com.google.gson.stream.JsonReader in) + throws java.io.IOException { + String s = in.nextString(); + return com.google.protobuf.ByteString.copyFrom(java.util.Base64.getDecoder().decode(s)); + } + }) .setDateFormat("yyyy-MM-dd HH:mm:ss") .create(); } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/serialization/KryoSerializerImpl.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/serialization/KryoSerializerImpl.java index bb46f55..95c4b4f 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/serialization/KryoSerializerImpl.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/serialization/KryoSerializerImpl.java @@ -3,11 +3,6 @@ import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; -import com.esotericsoftware.kryo.serializers.JavaSerializer; -import lombok.extern.slf4j.Slf4j; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import com.xiaoyu.rpc.common.serialization.Serializer; import com.xiaoyu.rpc.common.serialization.SerializerCode; @@ -19,52 +14,88 @@ public class KryoSerializerImpl implements Serializer { private static final Logger log = LoggerFactory.getLogger(KryoSerializerImpl.class); - // 确保每个线程只创建一个 Kryo 对象并在该线程内复用,避免了并发冲突,也避免了每次序列化都 new Kryo() 的昂贵开销 + + // ThreadLocal for Kryo instances to ensure thread safety and reuse private static final ThreadLocal kryoThreadLocal = ThreadLocal.withInitial(() -> { Kryo kryo = new Kryo(); + kryo.setReferences(true); + kryo.setRegistrationRequired(false); - kryo.setReferences(true);// 处理循环引用的类 + // Register custom serializer for Protobuf objects (RpcRequest, RpcResponse) + // This avoids using JavaSerializer which is slow and uses efficient Protobuf + // methods directly. + com.esotericsoftware.kryo.Serializer protobufSerializer = new com.esotericsoftware.kryo.Serializer() { + @Override + public void write(Kryo kryo, Output output, Object object) { + if (object instanceof com.google.protobuf.AbstractMessage) { + byte[] bytes = ((com.google.protobuf.AbstractMessage) object).toByteArray(); + output.writeInt(bytes.length, true); + output.writeBytes(bytes); + } else { + // Fallback should not happen if registered correctly, but safe to have + throw new RuntimeException( + "Unsupported Protobuf message type for custom serializer: " + object.getClass()); + } + } - // 关闭注册行为(为了开发方便,不强制要求注册类,虽然性能略低但在 RPC 场景通用性更好) - kryo.setRegistrationRequired(false); + @Override + public Object read(Kryo kryo, Input input, Class type) { + try { + int length = input.readInt(true); + byte[] bytes = input.readBytes(length); + // Use reflection to call static parseFrom(byte[]) method + // Caching the method would be even faster but this is already much faster than + // Java serialization + return type.getMethod("parseFrom", byte[].class).invoke(null, (Object) bytes); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize protobuf: " + type.getName(), e); + } + } + }; - // 对于 Protobuf 类,使用 Java 序列化作为后备方案 - kryo.addDefaultSerializer(com.google.protobuf.GeneratedMessageV3.class, JavaSerializer.class); + kryo.register(RpcRequest.class, protobufSerializer); + kryo.register(RpcResponse.class, protobufSerializer); + + // Standard registrations + kryo.register(String.class); + kryo.register(Object[].class); + kryo.register(Class[].class); return kryo; }); + // Reuse Output buffer to avoid repeated allocation of ByteArrayOutputStream + private static final ThreadLocal outputThreadLocal = ThreadLocal.withInitial(() -> new Output(4096, -1)); + @Override public byte[] serialize(Object obj) { - try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - Output output = new Output(byteArrayOutputStream)) { + Kryo kryo = kryoThreadLocal.get(); + Output output = outputThreadLocal.get(); + output.reset(); // Clear buffer for new serialization - Kryo kryo = kryoThreadLocal.get(); - // 使用 writeClassAndObject 写入类型信息和对象数据 + try { kryo.writeClassAndObject(output, obj); - - output.flush(); - log.debug("Kryo 序列化成功: {}", obj.getClass().getName()); - return byteArrayOutputStream.toByteArray(); + return output.toBytes(); } catch (Exception e) { log.error("Kryo 序列化失败: {}", obj.getClass().getName(), e); throw new RuntimeException("Kryo 序列化失败: " + obj.getClass().getName(), e); + } finally { + kryo.reset(); // Reset Kryo references } } @Override public T deserialize(byte[] bytes, Class clazz) { - try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); - Input input = new Input(byteArrayInputStream)) { - - Kryo kryo = kryoThreadLocal.get(); - // 使用 readClassAndObject 读取类型信息和对象数据 + Kryo kryo = kryoThreadLocal.get(); + // Input is lightweight, passing byte array directly + try (Input input = new Input(bytes)) { Object obj = kryo.readClassAndObject(input); - log.debug("Kryo 反序列化成功: {}", obj.getClass().getName()); return clazz.cast(obj); } catch (Exception e) { log.error("Kryo 反序列化失败, 目标类型: {}", clazz.getName(), e); throw new RuntimeException("Kryo 反序列化失败: " + clazz.getName(), e); + } finally { + kryo.reset(); // Reset Kryo references } } diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/server/RpcServer.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/server/RpcServer.java index 8c83e7d..beac1b6 100644 --- a/rpc-core/src/main/java/com/xiaoyu/rpc/core/server/RpcServer.java +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/server/RpcServer.java @@ -2,46 +2,49 @@ import com.xiaoyu.rpc.core.config.RpcConfig; import com.xiaoyu.rpc.common.extension.ExtensionLoader; -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; -import com.xiaoyu.rpc.core.protocol.Protocol; -import com.xiaoyu.rpc.core.protocol.ProtocolFactory; import com.xiaoyu.rpc.core.registry.ServiceRegistry; -import com.xiaoyu.rpc.common.vo.RpcRequest; +import com.xiaoyu.rpc.core.transport.Transport; +import com.xiaoyu.rpc.core.transport.TransportServer; +import lombok.extern.slf4j.Slf4j; import java.net.InetSocketAddress; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +@Slf4j public class RpcServer { - private static final Logger log = LoggerFactory.getLogger(RpcServer.class); private final String serverHost; private final int serverPort; - private final String protocolName; private final ServiceRegistry serviceRegistry; + private final TransportServer transportServer; public RpcServer() { RpcConfig config = RpcConfig.getInstance(); this.serverHost = config.getServerHost(); this.serverPort = config.getServerPort(); - this.protocolName = config.getProtocol(); this.serviceRegistry = ExtensionLoader.getExtensionLoader(ServiceRegistry.class) .getExtension(config.getRegistryType()); + + // 获取传输层实现 + Transport transport = ExtensionLoader.getExtensionLoader(Transport.class).getExtension(config.getTransport()); + this.transportServer = transport.createServer(this.serverPort); + + // 注册 JVM 关闭挂钩 (优雅下线) + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + log.info("检测到 JVM 关闭信号,正在执行优雅下线..."); + // 先注销服务,阻止新流量进入 + serviceRegistry.clearRegistry(); + // 再关闭网络层,让存量请求有机会处理完成 + transportServer.stop(); + log.info("优雅下线完成。"); + })); } public void register(Class interfaceClass, T serviceImpl) { String serviceName = interfaceClass.getName(); - // 1. 本地注册 (Netty Handler) - NettyRpcHandler.registerService(serviceName, serviceImpl); + // 先做本地注册,便于请求分发时快速定位实现类 + ServiceRepository.registerService(serviceName, serviceImpl); - // 2. 远程注册 (Nacos / Local) + // 再注册到注册中心(Nacos / Local) try { serviceRegistry.registerService(serviceName, new InetSocketAddress(serverHost, serverPort)); log.info("Service registered: {}", serviceName); @@ -51,26 +54,6 @@ public void register(Class interfaceClass, T serviceImpl) { } public void start() throws InterruptedException { - EventLoopGroup bossGroup = new NioEventLoopGroup(); - EventLoopGroup workerGroup = new NioEventLoopGroup(); - try { - ServerBootstrap b = new ServerBootstrap(); - b.group(bossGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .childHandler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel ch) { - Protocol protocol = ProtocolFactory.getProtocol(protocolName); - protocol.config(ch.pipeline(), true, new NettyRpcHandler()); - } - }); - - log.info("RPC Server started on port {}...", serverPort); - b.bind(serverPort).sync().channel().closeFuture().sync(); - } finally { - bossGroup.shutdownGracefully(); - workerGroup.shutdownGracefully(); - } + transportServer.start(); } - -} \ No newline at end of file +} diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/server/ServiceRepository.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/server/ServiceRepository.java new file mode 100644 index 0000000..b6c9cd1 --- /dev/null +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/server/ServiceRepository.java @@ -0,0 +1,23 @@ +package com.xiaoyu.rpc.core.server; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 服务注册仓库 + * 用于存放本地已注册的服务实例 + */ +public class ServiceRepository { + + // 进程内服务表:key 是接口全限定名,value 是具体实现对象 + private static final Map SERVICE_MAP = new ConcurrentHashMap<>(); + + public static void registerService(String interfaceName, Object serviceBean) { + // 同一接口重复注册时以后一次为准,便于启动期覆盖旧实现 + SERVICE_MAP.put(interfaceName, serviceBean); + } + + public static Object getService(String interfaceName) { + return SERVICE_MAP.get(interfaceName); + } +} diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/transport/Transport.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/transport/Transport.java new file mode 100644 index 0000000..7b158b2 --- /dev/null +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/transport/Transport.java @@ -0,0 +1,26 @@ +package com.xiaoyu.rpc.core.transport; + +import com.xiaoyu.rpc.common.extension.SPI; + +/** + * 传输层 SPI 接口 + * 用于屏蔽底层网络通信框架 (Netty, Mina, etc.) + */ +@SPI("netty") +public interface Transport { + + /** + * 创建服务端 + * + * @param port 监听端口 + * @return 服务端实例 + */ + TransportServer createServer(int port); + + /** + * 创建客户端 + * + * @return 客户端实例 + */ + TransportClient createClient(); +} diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/transport/TransportClient.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/transport/TransportClient.java new file mode 100644 index 0000000..0fe8fea --- /dev/null +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/transport/TransportClient.java @@ -0,0 +1,20 @@ +package com.xiaoyu.rpc.core.transport; + +import com.xiaoyu.rpc.common.vo.RpcRequest; +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; + +/** + * 传输层客户端接口 + */ +public interface TransportClient { + + /** + * 发送 RPC 请求 + * + * @param request 请求对象 + * @param address 目标地址 + * @return 响应结果 (CompletableFuture) + */ + CompletableFuture sendRequest(RpcRequest request, InetSocketAddress address); +} diff --git a/rpc-core/src/main/java/com/xiaoyu/rpc/core/transport/TransportServer.java b/rpc-core/src/main/java/com/xiaoyu/rpc/core/transport/TransportServer.java new file mode 100644 index 0000000..80b04f2 --- /dev/null +++ b/rpc-core/src/main/java/com/xiaoyu/rpc/core/transport/TransportServer.java @@ -0,0 +1,19 @@ +package com.xiaoyu.rpc.core.transport; + +/** + * 传输层服务端接口 + */ +public interface TransportServer { + + /** + * 启动服务 + * + * @throws InterruptedException 如果启动过程被中断 + */ + void start() throws InterruptedException; + + /** + * 停止服务 + */ + void stop(); +} diff --git a/rpc-core/src/main/resources/logback.xml b/rpc-core/src/main/resources/logback.xml new file mode 100644 index 0000000..1572866 --- /dev/null +++ b/rpc-core/src/main/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger -- %msg%n + + + + + + + + + + + + + + + diff --git a/rpc-core/src/main/resources/rpc-config.yaml b/rpc-core/src/main/resources/rpc-config.yaml index 89f7256..0cf374c 100644 --- a/rpc-core/src/main/resources/rpc-config.yaml +++ b/rpc-core/src/main/resources/rpc-config.yaml @@ -1,5 +1,5 @@ rpc: - protocol: "grpc" + protocol: "netty" server-host: "127.0.0.1" server-port: 8080 registry: "nacos" @@ -7,3 +7,4 @@ rpc: serializer: "protobuf" proxy: "bytebuddy" load-balancer: "roundrobin" + max-message-size: 8388608 # 8MB diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/client/RpcClientTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/client/RpcClientTest.java new file mode 100644 index 0000000..4e4403b --- /dev/null +++ b/rpc-core/src/test/java/com/xiaoyu/rpc/core/client/RpcClientTest.java @@ -0,0 +1,114 @@ +package com.xiaoyu.rpc.core.client; + +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 com.xiaoyu.rpc.common.vo.RpcResponse; +import com.xiaoyu.rpc.core.config.RpcConfig; +import com.xiaoyu.rpc.core.registry.ServiceDiscovery; +import com.xiaoyu.rpc.core.transport.TransportClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("RpcClient 异常与边界测试") +public class RpcClientTest { + + @BeforeEach + void setUp() throws Exception { + System.setProperty("rpc.serializer", "java"); + resetRpcConfigSingleton(); + } + + @AfterEach + void tearDown() throws Exception { + System.clearProperty("rpc.serializer"); + resetRpcConfigSingleton(); + } + + @Test + @DisplayName("服务发现为空时返回异常 Future") + void testServiceNotFound() { + TransportClient transportClient = (request, address) -> CompletableFuture.completedFuture(null); + ServiceDiscovery serviceDiscovery = serviceName -> null; + RpcClient rpcClient = new RpcClient(transportClient, serviceDiscovery); + + CompletableFuture future = rpcClient.sendRequest(minimalRequest(), String.class); + + assertTrue(future.isCompletedExceptionally(), "Future should be completed exceptionally"); + ExecutionException ex = assertThrows(ExecutionException.class, () -> future.get(1, TimeUnit.SECONDS)); + assertTrue(ex.getCause().getMessage().contains("未发现服务"), "Error should mention service not found"); + } + + @Test + @DisplayName("传输层抛异常时返回异常 Future") + void testTransportThrows() { + TransportClient transportClient = (request, address) -> { + throw new RuntimeException("transport down"); + }; + ServiceDiscovery serviceDiscovery = serviceName -> new InetSocketAddress("127.0.0.1", 8080); + RpcClient rpcClient = new RpcClient(transportClient, serviceDiscovery); + + CompletableFuture future = rpcClient.sendRequest(minimalRequest(), String.class); + + assertTrue(future.isCompletedExceptionally(), "Future should be completed exceptionally"); + ExecutionException ex = assertThrows(ExecutionException.class, () -> future.get(1, TimeUnit.SECONDS)); + assertTrue(ex.getCause().getMessage().contains("transport down"), "Error should keep transport failure"); + } + + @Test + @DisplayName("返回非 RpcResponse 类型时应失败") + void testUnexpectedResponseType() { + TransportClient transportClient = (request, address) -> CompletableFuture.completedFuture("not-rpc-response"); + ServiceDiscovery serviceDiscovery = serviceName -> new InetSocketAddress("127.0.0.1", 8080); + RpcClient rpcClient = new RpcClient(transportClient, serviceDiscovery); + + CompletableFuture future = rpcClient.sendRequest(minimalRequest(), String.class); + + assertTrue(future.isCompletedExceptionally(), "Future should be completed exceptionally"); + ExecutionException ex = assertThrows(ExecutionException.class, () -> future.get(1, TimeUnit.SECONDS)); + assertTrue(ex.getCause().getMessage().contains("Unexpected response type"), "Error should mention type mismatch"); + } + + @Test + @DisplayName("RpcResponse 正常反序列化返回目标类型") + void testSuccessfulDeserialize() throws Exception { + Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("java"); + byte[] body = serializer.serialize("hello"); + RpcResponse response = RpcResponse.newBuilder() + .setRequestId("req-1") + .setMessage("Success") + .setData(ByteString.copyFrom(body)) + .build(); + + TransportClient transportClient = (request, address) -> CompletableFuture.completedFuture(response); + ServiceDiscovery serviceDiscovery = serviceName -> new InetSocketAddress("127.0.0.1", 8080); + RpcClient rpcClient = new RpcClient(transportClient, serviceDiscovery); + + Object result = rpcClient.sendRequest(minimalRequest(), String.class).get(1, TimeUnit.SECONDS); + assertEquals("hello", result, "Response payload should be deserialized to String"); + } + + private static RpcRequest minimalRequest() { + return RpcRequest.newBuilder() + .setInterfaceName("com.example.DemoService") + .setMethodName("ping") + .build(); + } + + private static void resetRpcConfigSingleton() throws Exception { + Field field = RpcConfig.class.getDeclaredField("instance"); + field.setAccessible(true); + field.set(null, null); + } +} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/config/RpcConfigTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/config/RpcConfigTest.java new file mode 100644 index 0000000..7d53bef --- /dev/null +++ b/rpc-core/src/test/java/com/xiaoyu/rpc/core/config/RpcConfigTest.java @@ -0,0 +1,139 @@ +package com.xiaoyu.rpc.core.config; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * RPC 配置单元测试 + */ +@DisplayName("RpcConfig 配置测试") +public class RpcConfigTest { + + @BeforeEach + void setUp() throws Exception { + // 重置单例以便每个测试独立 + resetSingleton(); + // 清理测试用的系统属性 + System.clearProperty("rpc.registry"); + System.clearProperty("rpc.serializer"); + System.clearProperty("rpc.server-port"); + System.clearProperty("rpc.transport"); + System.clearProperty("rpc.protocol"); + } + + @AfterEach + void tearDown() throws Exception { + // 清理系统属性 + System.clearProperty("rpc.registry"); + System.clearProperty("rpc.serializer"); + System.clearProperty("rpc.server-port"); + System.clearProperty("rpc.transport"); + System.clearProperty("rpc.protocol"); + // 重置单例 + resetSingleton(); + } + + private void resetSingleton() throws Exception { + Field instanceField = RpcConfig.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + instanceField.set(null, null); + } + + @Test + @DisplayName("测试单例模式") + void testSingletonPattern() { + RpcConfig config1 = RpcConfig.getInstance(); + RpcConfig config2 = RpcConfig.getInstance(); + + assertSame(config1, config2, "RpcConfig should be singleton"); + } + + @Test + @DisplayName("测试默认配置值") + void testDefaultConfigValues() { + RpcConfig config = RpcConfig.getInstance(); + + assertNotNull(config.getSerializerType(), "Serializer type should not be null"); + assertNotNull(config.getServerHost(), "Server host should not be null"); + assertNotNull(config.getServerPort(), "Server port should not be null"); + assertNotNull(config.getProtocol(), "Protocol should not be null"); + } + + @Test + @DisplayName("测试系统属性覆盖 - 注册中心类型") + void testSystemPropertyOverrideRegistry() throws Exception { + System.setProperty("rpc.registry", "local"); + resetSingleton(); + + RpcConfig config = RpcConfig.getInstance(); + assertEquals("local", config.getRegistryType(), "Registry type should be overridden by system property"); + } + + @Test + @DisplayName("测试系统属性覆盖 - 序列化器") + void testSystemPropertyOverrideSerializer() throws Exception { + System.setProperty("rpc.serializer", "kryo"); + resetSingleton(); + + RpcConfig config = RpcConfig.getInstance(); + assertEquals("kryo", config.getSerializerType(), "Serializer should be overridden by system property"); + } + + @Test + @DisplayName("测试系统属性覆盖 - 服务端口") + void testSystemPropertyOverridePort() throws Exception { + System.setProperty("rpc.server-port", "9999"); + resetSingleton(); + + RpcConfig config = RpcConfig.getInstance(); + assertEquals(9999, config.getServerPort(), "Server port should be overridden by system property"); + } + + @Test + @DisplayName("测试系统属性覆盖 - 传输层") + void testSystemPropertyOverrideTransport() throws Exception { + System.setProperty("rpc.transport", "netty"); + resetSingleton(); + + RpcConfig config = RpcConfig.getInstance(); + assertEquals("netty", config.getTransport(), "Transport should be overridden by system property"); + } + + @Test + @DisplayName("测试系统属性覆盖 - 协议") + void testSystemPropertyOverrideProtocol() throws Exception { + System.setProperty("rpc.protocol", "grpc"); + resetSingleton(); + + RpcConfig config = RpcConfig.getInstance(); + assertEquals("grpc", config.getProtocol(), "Protocol should be overridden by system property"); + } + + @Test + @DisplayName("测试 getSerializerCode 方法") + void testGetSerializerCode() { + System.setProperty("rpc.serializer", "kryo"); + RpcConfig config = RpcConfig.getInstance(); + + byte code = config.getSerializerCode(); + assertTrue(code > 0, "Serializer code should be positive"); + } + + @Test + @DisplayName("测试 toString 方法") + void testToString() { + RpcConfig config = RpcConfig.getInstance(); + String str = config.toString(); + + assertNotNull(str, "toString should not return null"); + assertTrue(str.contains("RpcConfig"), "toString should contain class name"); + assertTrue(str.contains("serializerType"), "toString should contain serializerType"); + assertTrue(str.contains("serverPort"), "toString should contain serverPort"); + } +} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/extension/ExtensionLoaderTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/extension/ExtensionLoaderTest.java new file mode 100644 index 0000000..c238114 --- /dev/null +++ b/rpc-core/src/test/java/com/xiaoyu/rpc/core/extension/ExtensionLoaderTest.java @@ -0,0 +1,120 @@ +package com.xiaoyu.rpc.core.extension; + +import com.xiaoyu.rpc.common.extension.ExtensionLoader; +import com.xiaoyu.rpc.common.serialization.Serializer; +import com.xiaoyu.rpc.core.loadbalancer.LoadBalancer; +import com.xiaoyu.rpc.core.client.ProxyFactory; + +import com.xiaoyu.rpc.core.registry.ServiceRegistry; +import com.xiaoyu.rpc.core.registry.ServiceDiscovery; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * ExtensionLoader SPI 机制单元测试 + */ +@DisplayName("SPI ExtensionLoader 测试") +public class ExtensionLoaderTest { + + @Test + @DisplayName("测试加载 Serializer 扩展") + void testLoadSerializerExtensions() { + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Serializer.class); + + // 测试所有支持的序列化器 + assertNotNull(loader.getExtension("java"), "Java serializer should be loaded"); + assertNotNull(loader.getExtension("kryo"), "Kryo serializer should be loaded"); + assertNotNull(loader.getExtension("protobuf"), "Protobuf serializer should be loaded"); + assertNotNull(loader.getExtension("json"), "JSON serializer should be loaded"); + } + + @Test + @DisplayName("测试获取所有支持扩展名") + void testGetSupportedExtensions() { + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Serializer.class); + + var extensions = loader.getSupportedExtensions(); + assertTrue(extensions.contains("java"), "Should contain 'java' extension"); + assertTrue(extensions.contains("kryo"), "Should contain 'kryo' extension"); + assertTrue(extensions.contains("protobuf"), "Should contain 'protobuf' extension"); + assertTrue(extensions.contains("json"), "Should contain 'json' extension"); + assertEquals(4, extensions.size(), "Should have exactly 4 serializer extensions"); + } + + @Test + @DisplayName("测试扩展单例缓存") + void testExtensionCaching() { + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Serializer.class); + + Serializer first = loader.getExtension("kryo"); + Serializer second = loader.getExtension("kryo"); + + assertSame(first, second, "Same extension should return same instance (singleton)"); + } + + @Test + @DisplayName("测试加载不存在扩展时抛出异常") + void testLoadNonExistentExtension() { + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Serializer.class); + + assertThrows(RuntimeException.class, () -> { + loader.getExtension("non_existent"); + }, "Loading non-existent extension should throw exception"); + } + + @Test + @DisplayName("测试 LoadBalancer 扩展加载") + void testLoadBalancerExtensions() { + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(LoadBalancer.class); + + assertNotNull(loader.getExtension("random"), "Random load balancer should be loaded"); + assertNotNull(loader.getExtension("roundrobin"), "RoundRobin load balancer should be loaded"); + + var extensions = loader.getSupportedExtensions(); + assertEquals(2, extensions.size(), "Should have exactly 2 load balancer extensions"); + } + + @Test + @DisplayName("测试 ProxyFactory 扩展加载") + void testProxyFactoryExtensions() { + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(ProxyFactory.class); + + // jdk 代理实现不依赖网络与注册中心,单元测试里直接实例化即可 + assertNotNull(loader.getExtension("jdk"), "JDK proxy factory should be loaded"); + + // bytebuddy 在当前实现里会进一步初始化 RpcClient(包含注册中心/传输层依赖), + // 这里仅校验其扩展声明已被正确加载,避免把单元测试耦合到外部环境 + var extensions = loader.getSupportedExtensions(); + assertTrue(extensions.contains("bytebuddy"), "Should contain 'bytebuddy' extension"); + assertEquals(2, extensions.size(), "Should have exactly 2 proxy factory extensions"); + } + + @Test + @DisplayName("测试 ServiceRegistry 扩展加载") + void testServiceRegistryExtensions() { + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(ServiceRegistry.class); + + // local registry should be loadable without external dependencies + assertNotNull(loader.getExtension("local"), "Local service registry should be loaded"); + + var extensions = loader.getSupportedExtensions(); + assertTrue(extensions.contains("local"), "Should contain 'local' extension"); + assertTrue(extensions.contains("nacos"), "Should contain 'nacos' extension"); + } + + @Test + @DisplayName("测试 ServiceDiscovery 扩展加载") + void testServiceDiscoveryExtensions() { + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(ServiceDiscovery.class); + + // local registry should be loadable without external dependencies + assertNotNull(loader.getExtension("local"), "Local service discovery should be loaded"); + + var extensions = loader.getSupportedExtensions(); + assertTrue(extensions.contains("local"), "Should contain 'local' extension"); + assertTrue(extensions.contains("nacos"), "Should contain 'nacos' extension"); + } +} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/loadbalancer/LoadBalancerTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/loadbalancer/LoadBalancerTest.java new file mode 100644 index 0000000..0f241d3 --- /dev/null +++ b/rpc-core/src/test/java/com/xiaoyu/rpc/core/loadbalancer/LoadBalancerTest.java @@ -0,0 +1,106 @@ +package com.xiaoyu.rpc.core.loadbalancer; + +import com.xiaoyu.rpc.common.extension.ExtensionLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 负载均衡器单元测试 + */ +@DisplayName("LoadBalancer 负载均衡器测试") +public class LoadBalancerTest { + + private final List servers = Arrays.asList( + "127.0.0.1:8080", + "127.0.0.1:8081", + "127.0.0.1:8082"); + + @Test + @DisplayName("测试 Random 负载均衡器加载") + void testRandomLoadBalancerLoading() { + LoadBalancer lb = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getExtension("random"); + assertNotNull(lb, "Random load balancer should be loaded"); + assertTrue(lb instanceof RandomLoadBalancer, "Should be instance of RandomLoadBalancer"); + } + + @Test + @DisplayName("测试 RoundRobin 负载均衡器加载") + void testRoundRobinLoadBalancerLoading() { + LoadBalancer lb = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getExtension("roundrobin"); + assertNotNull(lb, "RoundRobin load balancer should be loaded"); + assertTrue(lb instanceof RoundRobinLoadBalancer, "Should be instance of RoundRobinLoadBalancer"); + } + + @Test + @DisplayName("测试 Random 负载均衡器选择服务器") + void testRandomLoadBalancerSelect() { + LoadBalancer lb = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getExtension("random"); + + // 多次选择确保都在列表中 + for (int i = 0; i < 10; i++) { + String selected = lb.select(servers); + assertNotNull(selected, "Selected server should not be null"); + assertTrue(servers.contains(selected), "Selected server should be in the list"); + } + } + + @Test + @DisplayName("测试 RoundRobin 负载均衡器轮询") + void testRoundRobinLoadBalancerSelect() { + // 创建新的 RoundRobin 实例来确保从头开始 + LoadBalancer lb = new RoundRobinLoadBalancer(); + + // 测试轮询模式 + String first = lb.select(servers); + String second = lb.select(servers); + String third = lb.select(servers); + String fourth = lb.select(servers); // 应该回到第一个 + + assertNotNull(first, "First selection should not be null"); + assertNotNull(second, "Second selection should not be null"); + assertNotNull(third, "Third selection should not be null"); + assertNotNull(fourth, "Fourth selection should not be null"); + + // 确保四次选择覆盖了所有服务器 + Set selected = new HashSet<>(Arrays.asList(first, second, third)); + assertEquals(3, selected.size(), "RoundRobin should cycle through all 3 servers"); + + // 第四次选择应该与前三次之一相同(循环) + assertTrue(servers.contains(fourth), "Fourth selection should be in server list"); + } + + @Test + @DisplayName("测试负载均衡器处理单节点列表") + void testLoadBalancerWithSingleServer() { + LoadBalancer randomLb = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getExtension("random"); + LoadBalancer rrLb = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getExtension("roundrobin"); + + List singleServer = Arrays.asList("127.0.0.1:9999"); + + assertEquals("127.0.0.1:9999", randomLb.select(singleServer), "Random should return the only server"); + assertEquals("127.0.0.1:9999", rrLb.select(singleServer), "RoundRobin should return the only server"); + } + + @RepeatedTest(5) + @DisplayName("测试 Random 负载均衡器随机性") + void testRandomnessOfRandomLoadBalancer() { + LoadBalancer lb = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getExtension("random"); + + Set selections = new HashSet<>(); + // 多次选择,应该最终选中所有服务器 + for (int i = 0; i < 100; i++) { + selections.add(lb.select(servers)); + } + + // 在100次随机选择后,应该覆盖所有3个服务器 + assertEquals(3, selections.size(), "Random load balancer should eventually select all servers"); + } +} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/registry/LocalRegistryTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/registry/LocalRegistryTest.java new file mode 100644 index 0000000..184586b --- /dev/null +++ b/rpc-core/src/test/java/com/xiaoyu/rpc/core/registry/LocalRegistryTest.java @@ -0,0 +1,102 @@ +package com.xiaoyu.rpc.core.registry; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import java.net.InetSocketAddress; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 本地注册中心单元测试 + */ +@DisplayName("LocalRegistry 本地注册中心测试") +public class LocalRegistryTest { + + private LocalRegistry registry; + + @BeforeEach + void setUp() { + registry = new LocalRegistry(); + } + + @AfterEach + void tearDown() { + registry.clearRegistry(); + } + + @Test + @DisplayName("测试注册和查找服务") + void testRegisterAndLookupService() { + String serviceName = "com.example.TestService"; + InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8080); + + // 注册服务 + registry.registerService(serviceName, address); + + // 查找服务 + InetSocketAddress result = registry.lookupService(serviceName); + + assertNotNull(result, "Should find registered service"); + assertEquals(address.getHostName(), result.getHostName(), "Host should match"); + assertEquals(address.getPort(), result.getPort(), "Port should match"); + } + + @Test + @DisplayName("测试查找不存在的服务") + void testLookupNonExistentService() { + InetSocketAddress result = registry.lookupService("non.existent.Service"); + assertNull(result, "Should return null for non-existent service"); + } + + @Test + @DisplayName("测试注册多个服务") + void testRegisterMultipleServices() { + String service1 = "com.example.Service1"; + String service2 = "com.example.Service2"; + InetSocketAddress address1 = new InetSocketAddress("127.0.0.1", 8081); + InetSocketAddress address2 = new InetSocketAddress("127.0.0.1", 8082); + + registry.registerService(service1, address1); + registry.registerService(service2, address2); + + InetSocketAddress result1 = registry.lookupService(service1); + InetSocketAddress result2 = registry.lookupService(service2); + + assertNotNull(result1, "Should find first service"); + assertNotNull(result2, "Should find second service"); + assertEquals(8081, result1.getPort(), "First service port should match"); + assertEquals(8082, result2.getPort(), "Second service port should match"); + } + + @Test + @DisplayName("测试覆盖注册服务") + void testOverwriteService() { + String serviceName = "com.example.OverwriteService"; + InetSocketAddress oldAddress = new InetSocketAddress("127.0.0.1", 8080); + InetSocketAddress newAddress = new InetSocketAddress("127.0.0.1", 9090); + + registry.registerService(serviceName, oldAddress); + registry.registerService(serviceName, newAddress); // 覆盖 + + InetSocketAddress result = registry.lookupService(serviceName); + + assertNotNull(result, "Should find service"); + assertEquals(9090, result.getPort(), "Port should be updated to new address"); + } + + @Test + @DisplayName("测试清空注册中心") + void testClearRegistry() { + String serviceName = "com.example.ToBeCleared"; + registry.registerService(serviceName, new InetSocketAddress("127.0.0.1", 8088)); + + assertNotNull(registry.lookupService(serviceName), "Service should exist before clear"); + + registry.clearRegistry(); + + assertNull(registry.lookupService(serviceName), "Service should be removed after clear"); + } +} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceDiscoveryTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceDiscoveryTest.java new file mode 100644 index 0000000..7913d92 --- /dev/null +++ b/rpc-core/src/test/java/com/xiaoyu/rpc/core/registry/nacos/NacosServiceDiscoveryTest.java @@ -0,0 +1,169 @@ +package com.xiaoyu.rpc.core.registry.nacos; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.listener.EventListener; +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.xiaoyu.rpc.core.loadbalancer.LoadBalancer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("NacosServiceDiscovery 缓存与回退测试") +public class NacosServiceDiscoveryTest { + + @BeforeEach + @SuppressWarnings("unchecked") + void setUp() throws Exception { + Field cacheField = NacosServiceDiscovery.class.getDeclaredField("serviceCache"); + cacheField.setAccessible(true); + ((Map>) cacheField.get(null)).clear(); + + Field subscribedField = NacosServiceDiscovery.class.getDeclaredField("subscribedServices"); + subscribedField.setAccessible(true); + ((Set) subscribedField.get(null)).clear(); + } + + @Test + @DisplayName("正常发现服务并且同服务只订阅一次") + void testLookupAndSubscribeOnce() { + AtomicInteger subscribeCount = new AtomicInteger(); + List instances = List.of(instance("10.0.0.1", 8080), instance("10.0.0.2", 8081)); + + NamingService namingService = namingServiceProxy((method, args) -> { + if ("getAllInstances".equals(method) && args.length == 1) { + return instances; + } + if ("subscribe".equals(method) && args.length == 2 && args[1] instanceof EventListener) { + subscribeCount.incrementAndGet(); + return null; + } + return null; + }); + + LoadBalancer loadBalancer = addresses -> addresses.get(0); + NacosServiceDiscovery discovery = new NacosServiceDiscovery(namingService, loadBalancer); + + InetSocketAddress first = discovery.lookupService("svc-a"); + InetSocketAddress second = discovery.lookupService("svc-a"); + + assertEquals("10.0.0.1", first.getHostString()); + assertEquals(8080, first.getPort()); + assertEquals("10.0.0.1", second.getHostString()); + assertEquals(1, subscribeCount.get(), "Same service should only subscribe once"); + } + + @Test + @DisplayName("Nacos 异常时回退到本地缓存") + @SuppressWarnings("unchecked") + void testFallbackToCacheOnNacosError() throws Exception { + Field cacheField = NacosServiceDiscovery.class.getDeclaredField("serviceCache"); + cacheField.setAccessible(true); + Map> cache = (Map>) cacheField.get(null); + cache.put("svc-b", new ArrayList<>(List.of(instance("127.0.0.1", 9000)))); + + NamingService namingService = namingServiceProxy((method, args) -> { + if ("getAllInstances".equals(method) && args.length == 1) { + throw new NacosException(500, "network down"); + } + if ("subscribe".equals(method)) { + return null; + } + return null; + }); + + LoadBalancer loadBalancer = addresses -> addresses.get(0); + NacosServiceDiscovery discovery = new NacosServiceDiscovery(namingService, loadBalancer); + + InetSocketAddress address = discovery.lookupService("svc-b"); + assertEquals("127.0.0.1", address.getHostString()); + assertEquals(9000, address.getPort()); + } + + @Test + @DisplayName("Nacos 空列表且无缓存时抛异常") + void testNoInstanceAndNoCache() { + NamingService namingService = namingServiceProxy((method, args) -> { + if ("getAllInstances".equals(method) && args.length == 1) { + return List.of(); + } + if ("subscribe".equals(method)) { + return null; + } + return null; + }); + + LoadBalancer loadBalancer = addresses -> addresses.get(0); + NacosServiceDiscovery discovery = new NacosServiceDiscovery(namingService, loadBalancer); + + RuntimeException ex = assertThrows(RuntimeException.class, () -> discovery.lookupService("svc-empty")); + assertTrue(ex.getMessage().contains("未找到服务"), "Should throw not found error"); + } + + private static Instance instance(String ip, int port) { + Instance i = new Instance(); + i.setIp(ip); + i.setPort(port); + return i; + } + + private static NamingService namingServiceProxy(Invocation invocation) { + return (NamingService) Proxy.newProxyInstance( + NacosServiceDiscoveryTest.class.getClassLoader(), + new Class[] { NamingService.class }, + (proxy, method, args) -> { + if (method.getDeclaringClass() == Object.class) { + return switch (method.getName()) { + case "toString" -> "NamingServiceProxy"; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + default -> null; + }; + } + Object result = invocation.invoke(method.getName(), args == null ? new Object[0] : args); + if (result == null && method.getReturnType().isPrimitive()) { + if (method.getReturnType() == boolean.class) { + return false; + } + if (method.getReturnType() == byte.class) { + return (byte) 0; + } + if (method.getReturnType() == short.class) { + return (short) 0; + } + if (method.getReturnType() == int.class) { + return 0; + } + if (method.getReturnType() == long.class) { + return 0L; + } + if (method.getReturnType() == float.class) { + return 0F; + } + if (method.getReturnType() == double.class) { + return 0D; + } + if (method.getReturnType() == char.class) { + return '\0'; + } + } + return result; + }); + } + + @FunctionalInterface + private interface Invocation { + Object invoke(String method, Object[] args) throws Throwable; + } +} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/serialization/SerializerTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/serialization/SerializerTest.java new file mode 100644 index 0000000..38a3a17 --- /dev/null +++ b/rpc-core/src/test/java/com/xiaoyu/rpc/core/serialization/SerializerTest.java @@ -0,0 +1,185 @@ +package com.xiaoyu.rpc.core.serialization; + +import com.xiaoyu.rpc.common.extension.ExtensionLoader; +import com.xiaoyu.rpc.common.serialization.Serializer; +import com.xiaoyu.rpc.common.serialization.SerializerCode; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.Serializable; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 序列化器单元测试 + * 测试 Java、Kryo、JSON 序列化器的序列化/反序列化功能 + */ +@DisplayName("Serializer 序列化器测试") +public class SerializerTest { + + /** + * 用于测试的简单可序列化对象 + */ + public static class TestMessage implements Serializable { + private static final long serialVersionUID = 1L; + + private String interfaceName; + private String methodName; + private String[] paramTypes; + + public TestMessage() { + } + + public TestMessage(String interfaceName, String methodName, String[] paramTypes) { + this.interfaceName = interfaceName; + this.methodName = methodName; + this.paramTypes = paramTypes; + } + + public String getInterfaceName() { + return interfaceName; + } + + public void setInterfaceName(String interfaceName) { + this.interfaceName = interfaceName; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public String[] getParamTypes() { + return paramTypes; + } + + public void setParamTypes(String[] paramTypes) { + this.paramTypes = paramTypes; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TestMessage that = (TestMessage) o; + return Objects.equals(interfaceName, that.interfaceName) && + Objects.equals(methodName, that.methodName); + } + } + + @ParameterizedTest + @ValueSource(strings = { "java", "kryo", "json" }) + @DisplayName("测试序列化和反序列化 TestMessage") + void testSerializeTestMessage(String serializerName) { + Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension(serializerName); + + // 创建测试消息 + TestMessage message = new TestMessage( + "com.example.HelloService", + "sayHello", + new String[] { "java.lang.String" }); + + // 序列化 + byte[] bytes = serializer.serialize(message); + assertNotNull(bytes, "Serialized bytes should not be null"); + assertTrue(bytes.length > 0, "Serialized bytes should have content"); + + // 反序列化 + TestMessage deserialized = serializer.deserialize(bytes, TestMessage.class); + assertNotNull(deserialized, "Deserialized message should not be null"); + assertEquals(message.getInterfaceName(), deserialized.getInterfaceName(), "Interface name should match"); + assertEquals(message.getMethodName(), deserialized.getMethodName(), "Method name should match"); + } + + @ParameterizedTest + @ValueSource(strings = { "java", "kryo", "json" }) + @DisplayName("测试序列化和反序列化简单字符串") + void testSerializeString(String serializerName) { + Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension(serializerName); + + String original = "Hello, World!"; + + // 序列化 + byte[] bytes = serializer.serialize(original); + assertNotNull(bytes, "Serialized bytes should not be null"); + + // 反序列化 + String deserialized = serializer.deserialize(bytes, String.class); + assertEquals(original, deserialized, "Deserialized string should match original"); + } + + @Test + @DisplayName("测试 Java 序列化器代码") + void testJavaSerializerCode() { + Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("java"); + assertEquals(SerializerCode.JAVA_SERIALIZER, serializer.getCode(), "Java serializer should have code 0x01"); + } + + @Test + @DisplayName("测试 Kryo 序列化器代码") + void testKryoSerializerCode() { + Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("kryo"); + assertEquals(SerializerCode.KRYO_SERIALIZER, serializer.getCode(), "Kryo serializer should have code 0x02"); + } + + @Test + @DisplayName("测试 Protobuf 序列化器代码") + void testProtobufSerializerCode() { + Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("protobuf"); + assertEquals(SerializerCode.PROTOBUF_SERIALIZER, serializer.getCode(), + "Protobuf serializer should have code 0x03"); + } + + @Test + @DisplayName("测试 JSON 序列化器代码") + void testJsonSerializerCode() { + Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("json"); + // JSON serializer uses code 0x04 + assertEquals((byte) 0x04, serializer.getCode(), "JSON serializer should have code 0x04"); + } + + @Test + @DisplayName("测试 SerializerCode 按名称获取序列化器") + void testGetSerializerByName() { + Serializer javaSerializer = SerializerCode.getSerializerByName("java"); + assertNotNull(javaSerializer, "Should get Java serializer by name"); + + Serializer kryoSerializer = SerializerCode.getSerializerByName("kryo"); + assertNotNull(kryoSerializer, "Should get Kryo serializer by name"); + + // 测试大小写不敏感 + Serializer protobufSerializer = SerializerCode.getSerializerByName("PROTOBUF"); + assertNotNull(protobufSerializer, "Should get Protobuf serializer by uppercase name"); + } + + @Test + @DisplayName("测试 SerializerCode 按代码获取序列化器") + void testGetSerializerByCode() { + Serializer javaSerializer = SerializerCode.getSerializerByCode(SerializerCode.JAVA_SERIALIZER); + assertNotNull(javaSerializer, "Should get serializer by code 0x01"); + + Serializer kryoSerializer = SerializerCode.getSerializerByCode(SerializerCode.KRYO_SERIALIZER); + assertNotNull(kryoSerializer, "Should get serializer by code 0x02"); + + Serializer protobufSerializer = SerializerCode.getSerializerByCode(SerializerCode.PROTOBUF_SERIALIZER); + assertNotNull(protobufSerializer, "Should get serializer by code 0x03"); + } + + @Test + @DisplayName("测试空对象序列化") + void testSerializeNull() { + Serializer jsonSerializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension("json"); + + byte[] bytes = jsonSerializer.serialize(null); + assertNotNull(bytes, "Serialized null should return empty array"); + } +} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/server/ServiceRepositoryTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/server/ServiceRepositoryTest.java new file mode 100644 index 0000000..6e93859 --- /dev/null +++ b/rpc-core/src/test/java/com/xiaoyu/rpc/core/server/ServiceRepositoryTest.java @@ -0,0 +1,52 @@ +package com.xiaoyu.rpc.core.server; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("ServiceRepository 服务仓库测试") +public class ServiceRepositoryTest { + + @BeforeEach + @SuppressWarnings("unchecked") + void setUp() throws Exception { + Field mapField = ServiceRepository.class.getDeclaredField("SERVICE_MAP"); + mapField.setAccessible(true); + ((Map) mapField.get(null)).clear(); + } + + @Test + @DisplayName("测试注册并查询服务") + void testRegisterAndGetService() { + String serviceName = "com.example.TestService"; + Object serviceBean = new Object(); + + ServiceRepository.registerService(serviceName, serviceBean); + + assertSame(serviceBean, ServiceRepository.getService(serviceName), "Should return same registered bean"); + } + + @Test + @DisplayName("测试覆盖注册服务") + void testOverwriteService() { + String serviceName = "com.example.TestService"; + Object oldBean = new Object(); + Object newBean = new Object(); + + ServiceRepository.registerService(serviceName, oldBean); + ServiceRepository.registerService(serviceName, newBean); + + assertSame(newBean, ServiceRepository.getService(serviceName), "Latest registration should overwrite old one"); + } + + @Test + @DisplayName("测试查询不存在的服务") + void testGetNonExistentService() { + assertNull(ServiceRepository.getService("com.example.NotFound"), "Unknown service should return null"); + } +} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/test/LoadBalancerTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/test/LoadBalancerTest.java deleted file mode 100644 index c04a4da..0000000 --- a/rpc-core/src/test/java/com/xiaoyu/rpc/core/test/LoadBalancerTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.xiaoyu.rpc.core.test; - -import com.xiaoyu.rpc.common.extension.ExtensionLoader; -import com.xiaoyu.rpc.core.loadbalancer.LoadBalancer; -import java.util.Arrays; -import java.util.List; - -public class LoadBalancerTest { - public static void main(String[] args) { - System.out.println("--- 测试 LoadBalancer SPI ---"); - - // 1. 测试 SPI 机制是否能加载 - LoadBalancer randomLb = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getExtension("random"); - LoadBalancer roundRobinLb = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getExtension("roundrobin"); - - System.out.println("Random LB Loaded: " + (randomLb != null)); - System.out.println("RoundRobin LB Loaded: " + (roundRobinLb != null)); - - List servers = Arrays.asList("127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"); - - // 2. 测试 RoundRobin - System.out.println("\n--- 测试 RoundRobin ---"); - for (int i = 0; i < 5; i++) { - System.out.println("Select: " + roundRobinLb.select(servers)); - } - - // 3. 测试 Random - System.out.println("\n--- 测试 Random ---"); - for (int i = 0; i < 5; i++) { - System.out.println("Select: " + randomLb.select(servers)); - } - } -} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/test/ProtocolSpiTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/test/ProtocolSpiTest.java deleted file mode 100644 index eec203e..0000000 --- a/rpc-core/src/test/java/com/xiaoyu/rpc/core/test/ProtocolSpiTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.xiaoyu.rpc.core.test; - -import com.xiaoyu.rpc.core.protocol.http.HttpProtocol; -import com.xiaoyu.rpc.core.protocol.netty.NettyProtocol; -import com.xiaoyu.rpc.core.protocol.Protocol; -import com.xiaoyu.rpc.core.protocol.ProtocolFactory; - -public class ProtocolSpiTest { - public static void main(String[] args) { - System.out.println("Starting Protocol SPI Test..."); - - // 1. Test loading Netty protocol (dynamic SPI or fallback logic) - Protocol netty = ProtocolFactory.getProtocol("netty"); - System.out.println("Loaded 'netty': " + netty.getClass().getName()); - if (!(netty instanceof NettyProtocol)) { - throw new RuntimeException("Expected NettyProtocol but got " + netty.getClass().getName()); - } - - // 2. Test loading HTTP protocol - Protocol http = ProtocolFactory.getProtocol("http"); - System.out.println("Loaded 'http': " + http.getClass().getName()); - if (!(http instanceof HttpProtocol)) { - throw new RuntimeException("Expected HttpProtocol but got " + http.getClass().getName()); - } - - // 3. Test loading unknown protocol (should fallback to Netty with warning log) - System.out.println("Testing unknown protocol fallback..."); - Protocol unknown = ProtocolFactory.getProtocol("unknown_proto"); - System.out.println("Loaded 'unknown_proto': " + unknown.getClass().getName()); - if (!(unknown instanceof NettyProtocol)) { - throw new RuntimeException( - "Fallback failed: Expected NettyProtocol but got " + unknown.getClass().getName()); - } - - System.out.println("Protocol SPI Test Passed Successfully!"); - } -} diff --git a/rpc-core/src/test/java/com/xiaoyu/rpc/core/test/SpiTest.java b/rpc-core/src/test/java/com/xiaoyu/rpc/core/test/SpiTest.java deleted file mode 100644 index 71f8cf7..0000000 --- a/rpc-core/src/test/java/com/xiaoyu/rpc/core/test/SpiTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.xiaoyu.rpc.core.test; - -import com.xiaoyu.rpc.common.serialization.Serializer; -import com.xiaoyu.rpc.common.serialization.SerializerCode; -import com.xiaoyu.rpc.common.extension.ExtensionLoader; - -public class SpiTest { - public static void main(String[] args) { - System.out.println("Beginning SPI Test..."); - - // 1. Test ExtensionLoader directly - System.out.println("\n--- Testing ExtensionLoader ---"); - ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Serializer.class); - System.out.println("Supported extensions: " + loader.getSupportedExtensions()); - - try { - Serializer javaSerializer = loader.getExtension("java"); - System.out.println("Loaded 'java': " + javaSerializer.getClass().getName()); - - Serializer kryoSerializer = loader.getExtension("kryo"); - System.out.println("Loaded 'kryo': " + kryoSerializer.getClass().getName()); - - Serializer protoSerializer = loader.getExtension("protobuf"); - System.out.println("Loaded 'protobuf': " + protoSerializer.getClass().getName()); - - } catch (Exception e) { - e.printStackTrace(); - } - - // 2. Test SerializerCode (which uses ExtensionLoader internally) - System.out.println("\n--- Testing SerializerCode ---"); - try { - Serializer s1 = SerializerCode.getSerializerByName("java"); - System.out.println("getSerializerByName('java') -> " + s1.getClass().getName() + ", Code: " + s1.getCode()); - - Serializer s2 = SerializerCode.getSerializerByCode((byte) 0x02); // Kryo - System.out.println("getSerializerByCode(0x02) -> " + s2.getClass().getName() + ", Code: " + s2.getCode()); - - Serializer s3 = SerializerCode.getSerializerByName("protobuf"); - System.out.println( - "getSerializerByName('protobuf') -> " + s3.getClass().getName() + ", Code: " + s3.getCode()); - - // Test Case Insensitivity - Serializer s4 = SerializerCode.getSerializerByName("PROTOBUF"); - System.out.println( - "getSerializerByName('PROTOBUF') -> " + s4.getClass().getName() + ", Code: " + s4.getCode()); - - } catch (Exception e) { - e.printStackTrace(); - } - - System.out.println("\nSPI Test Finished."); - } -} diff --git a/rpc-provider/pom.xml b/rpc-provider/pom.xml index eef6629..b4752d2 100644 --- a/rpc-provider/pom.xml +++ b/rpc-provider/pom.xml @@ -20,6 +20,11 @@ com.xiaoyu.rpc rpc-core + + 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"); + } +}