gRPC

gRPC là một framework mã nguồn mở, hiệu suất cao, được phát triển bởi Google, dùng để xây dựng các ứng dụng phân tán và kết nối các dịch vụ thông qua giao tiếp giữa client và server. Nó dựa trên giao thức HTTP/2 và sử dụng Protocol Buffers (Protobuf) làm định dạng trao đổi dữ liệu. Dưới đây là giải thích chi tiết và dễ hiểu về gRPC, phù hợp để bạn học từ cơ bản:


1. gRPC là gì?

  • gRPC (gRPC Remote Procedure Calls) là một cách để các ứng dụng hoặc dịch vụ gọi hàm (function) từ xa như thể chúng được gọi cục bộ, nhưng thực tế các hàm này chạy trên một máy tính khác (server).

  • Nó được thiết kế để:

    • Hiệu suất cao: Nhờ HTTP/2, gRPC hỗ trợ truyền dữ liệu nhanh, nén header, và kết nối song công (bidirectional streaming).

    • Đa nền tảng: Hỗ trợ nhiều ngôn ngữ lập trình như Go, Java, Python, C++, Node.js, v.v.

    • Dễ sử dụng: Tự động tạo mã client và server từ các định nghĩa giao diện (interface).

    • Khả năng mở rộng: Phù hợp với các hệ thống lớn như microservices.


2. Cách gRPC hoạt động

gRPC sử dụng mô hình client-server với các bước cơ bản sau:

  1. Định nghĩa giao diện dịch vụ:

    • Bạn viết một tệp .proto sử dụng ngôn ngữ Protocol Buffers để mô tả:

      • Các dịch vụ (services) và các phương thức (methods) mà client có thể gọi.

      • Cấu trúc dữ liệu (messages) được gửi/nhận.

    • Ví dụ tệp .proto:

      syntax = "proto3";
      
      service Greeter {
        rpc SayHello (HelloRequest) returns (HelloReply);
      }
      
      message HelloRequest {
        string name = 1;
      }
      
      message HelloReply {
        string message = 1;
      }
  2. Tạo mã tự động:

    • Sử dụng công cụ protoc (Protocol Buffers compiler) để tạo mã client và server trong ngôn ngữ bạn chọn (VD: Python, Go, Java).

    • Mã này bao gồm các hàm gọi và xử lý yêu cầu.

  3. Triển khai server:

    • Bạn viết logic cho server để xử lý các phương thức được định nghĩa (VD: SayHello trả về "Hello, [name]!").

  4. Gọi từ client:

    • Client sử dụng mã được tạo để gọi phương thức từ xa, gửi yêu cầu và nhận phản hồi.


3. Các đặc điểm nổi bật của gRPC

  • Dựa trên HTTP/2:

    • Hỗ trợ multiplexing: Nhiều yêu cầu có thể được gửi đồng thời trên cùng một kết nối.

    • Streaming: Hỗ trợ các luồng dữ liệu (client-streaming, server-streaming, hoặc bidirectional streaming).

    • Nén header: Giảm kích thước dữ liệu truyền.

  • Protocol Buffers:

    • Định dạng nhị phân (binary), nhỏ gọn và nhanh hơn JSON hay XML.

    • Đảm bảo tính tương thích giữa các phiên bản (backward/forward compatibility).

  • Hỗ trợ nhiều kiểu giao tiếp:

    • Unary RPC: Gọi một lần, nhận một phản hồi (giống REST).

    • Client-streaming RPC: Client gửi nhiều thông điệp, server trả về một phản hồi.

    • Server-streaming RPC: Client gửi một yêu cầu, server trả về nhiều thông điệp.

    • Bidirectional streaming RPC: Cả client và server gửi/nhận nhiều thông điệp liên tục.

  • Tích hợp mạnh mẽ:

    • Hỗ trợ xác thực (authentication), mã hóa (TLS), cân bằng tải (load balancing), và giám sát (monitoring).


4. So sánh gRPC với REST

Tiêu chí
gRPC
REST

Giao thức

HTTP/2

HTTP/1.1 hoặc HTTP/2

Định dạng dữ liệu

Protocol Buffers (binary)

JSON, XML (text)

Hiệu suất

Cao hơn (nhờ nhị phân và HTTP/2)

Thấp hơn (do text và overhead)

Streaming

Hỗ trợ đầy đủ

Hạn chế (thường dùng WebSocket)

Mô hình

RPC (gọi hàm)

Tài nguyên (resource-based)

Dễ dùng

Cần học Protobuf, phức tạp hơn

Dễ tiếp cận hơn với JSON

Ứng dụng

Microservices, hệ thống lớn

API công khai, ứng dụng web


5. Khi nào nên dùng gRPC?

  • Phù hợp:

    • Hệ thống microservices cần giao tiếp nhanh giữa các dịch vụ.

    • Ứng dụng yêu cầu streaming dữ liệu (VD: chat, IoT).

    • Hệ thống nội bộ (internal APIs) với yêu cầu hiệu suất cao.

    • Ứng dụng đa ngôn ngữ cần giao tiếp thống nhất.

  • Không phù hợp:

    • API công khai (public APIs) vì trình duyệt không hỗ trợ HTTP/2 và Protobuf dễ dàng.

    • Các dự án nhỏ, đơn giản không cần hiệu suất cao.


Dưới đây là một ví dụ cơ bản về cách triển khai gRPC với Java, bao gồm cả việc định nghĩa dịch vụ, tạo mã, triển khai server và client.


Ví dụ: Dịch vụ Greeter

Chúng ta sẽ xây dựng một dịch vụ gRPC đơn giản có tên Greeter, trong đó:

  • Client gửi một yêu cầu HelloRequest với tên người dùng.

  • Server trả về một phản hồi HelloReply với lời chào.


1. Chuẩn bị môi trường

  • Cần cài đặt:

    • JDK (phiên bản 8 hoặc cao hơn).

    • Maven (hoặc Gradle) để quản lý dự án.

    • Công cụ protoc (Protocol Buffers compiler) được tích hợp trong plugin Maven.

  • Cấu trúc dự án:

    grpc-java-example/
    ├── pom.xml
    └── src/
        └── main/
            ├── java/
            │   └── com/
            │       └── example/
            │            └── grpc/
            │                 ├── GreeterClient.java
            │                 └── GreeterServer.java
            └── proto/
                 └── greeter.proto

2. Định nghĩa dịch vụ trong tệp .proto

Tạo tệp src/main/proto/greeter.proto với nội dung sau:

syntax = "proto3";

option java_package = "com.example.grpc";
option java_multiple_files = true;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
  • java_package: Chỉ định package cho mã Java được tạo.

  • java_multiple_files: Tạo các lớp riêng biệt cho mỗi message/service.


3. Cấu hình Maven

Tạo tệp pom.xml để tích hợp gRPC và tự động tạo mã từ tệp .proto:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>grpc-java-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <grpc.version>1.64.0</grpc.version>
        <protobuf.version>3.25.1</protobuf.version>
    </properties>

    <dependencies>
        <!-- gRPC dependencies -->
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty</artifactId>
            <version>${grpc.version}</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>${grpc.version}</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>${grpc.version}</version>
        </dependency>
        <!-- Protocol Buffers -->
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>${protobuf.version}</version>
        </dependency>
    </dependencies>

    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.7.1</version>
            </extension>
        </extensions>
        <plugins>
            <!-- Protobuf compiler plugin -->
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!-- Java compiler -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4. Tạo mã từ tệp .proto

Chạy lệnh Maven để tạo mã Java:

mvn clean compile

Lệnh này sẽ:

  • Biên dịch tệp greeter.proto.

  • Tạo các lớp trong thư mục target/generated-sources/protobuf, ví dụ:

    • HelloRequest.java

    • HelloReply.java

    • GreeterGrpc.java (chứa stub cho client/server).


5. Triển khai Server

Tạo tệp src/main/java/com/example/grpc/GreeterServer.java:

package com.example.grpc;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;

public class GreeterServer {
    private Server server;

    private void start() throws IOException {
        int port = 50051;
        server = ServerBuilder.forPort(port)
                .addService(new GreeterImpl())
                .build()
                .start();
        System.out.println("Server started, listening on " + port);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.err.println("Shutting down gRPC server");
            GreeterServer.this.stop();
            System.err.println("Server shut down");
        }));
    }

    private void stop() {
        if (server != null) {
            server.shutdown();
        }
    }

    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        final GreeterServer server = new GreeterServer();
        server.start();
        server.blockUntilShutdown();
    }

    // Triển khai dịch vụ Greeter
    static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
        @Override
        public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
            HelloReply reply = HelloReply.newBuilder()
                    .setMessage("Hello, " + req.getName() + "!")
                    .build();
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
        }
    }
}
  • Giải thích:

    • GreeterImpl kế thừa từ GreeterGrpc.GreeterImplBase để triển khai phương thức sayHello.

    • Server chạy trên cổng 50051 và xử lý yêu cầu từ client.

    • StreamObserver được dùng để gửi phản hồi về client.


6. Triển khai Client

Tạo tệp src/main/java/com/example/grpc/GreeterClient.java:

package com.example.grpc;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class GreeterClient {
    private final ManagedChannel channel;
    private final GreeterGrpc.GreeterBlockingStub blockingStub;

    public GreeterClient(String host, int port) {
        this.channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext() // Không dùng TLS để đơn giản
                .build();
        this.blockingStub = GreeterGrpc.newBlockingStub(channel);
    }

    public void shutdown() throws InterruptedException {
        channel.shutdown();
    }

    public void greet(String name) {
        HelloRequest request = HelloRequest.newBuilder()
                .setName(name)
                .build();
        HelloReply response;
        try {
            response = blockingStub.sayHello(request);
            System.out.println("Greeting: " + response.getMessage());
        } catch (Exception e) {
            System.err.println("RPC failed: " + e.getMessage());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        GreeterClient client = new GreeterClient("localhost", 50051);
        try {
            client.greet("World");
        } finally {
            client.shutdown();
        }
    }
}
  • Giải thích:

    • Client sử dụng ManagedChannel để kết nối tới server.

    • GreeterGrpc.GreeterBlockingStub là stub kiểu blocking, phù hợp cho các cuộc gọi unary đơn giản.

    • Client gửi tên "World" và nhận phản hồi từ server.


7. Chạy ứng dụng

  1. Biên dịch dự án:

    mvn clean compile
  2. Chạy server:

    • Mở một terminal và chạy:

      mvn exec:java -Dexec.mainClass="com.example.grpc.GreeterServer"
    • Kết quả: Server started, listening on 50051.

  3. Chạy client:

    • Mở terminal khác và chạy:

      mvn exec:java -Dexec.mainClass="com.example.grpc.GreeterClient"
    • Kết quả: Greeting: Hello, World!.


8. Mở rộng (Tùy chọn)

Nếu bạn muốn thử các tính năng nâng cao:

  • Streaming: Thêm các phương thức streaming vào greeter.proto (client-streaming, server-streaming, hoặc bidirectional).

    rpc SayHelloStream (stream HelloRequest) returns (HelloReply);
  • Xác thực: Bật TLS bằng cách thêm chứng chỉ vào server và client.

  • Xử lý lỗi: Sử dụng StatusRuntimeException để quản lý lỗi trong gRPC.


9. Lưu ý

  • Ví dụ này sử dụng kết nối không mã hóa (usePlaintext()). Trong môi trường sản xuất, bạn nên dùng TLS.

  • Nếu gặp lỗi liên quan đến phiên bản Protobuf hoặc gRPC, hãy kiểm tra tính tương thích giữa grpc-javaprotobuf-java trong pom.xml.


10. Tài liệu tham khảo

Last updated