Skip to main content

Command Palette

Search for a command to run...

Google remote procedure call - Giao thức giao tiếp client-server

Updated
13 min read

I. Code generation từ .proto file (Java gRPC plugin)

Trong gRPC, file .proto đóng vai trò như hợp đồng (contract) giữa client và server.

1. proto là gì?

Đây là nơi định nghĩa:

  • Message (cấu trúc dữ liệu request/response)
  • Service (các API)

Ví dụ:

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}

2. Code generation hoạt động thế nào?

Thay vì viết code thủ công, gRPC sử dụng protoc để generate:

  • Model class (POJO từ message)
  • Stub (client gọi server)
  • Base server class (để implement logic)

Trong java ta dùng các thư viện để gen code từ proto

  • protobuf-maven-plugin – để chạy protoc
  • protoc-gen-grpc-java – để generate gRPC stub

Còn trong spring thì ta dùng thêm net.devh:grpc-spring-boot-starter để tích hợp gRPC với Spring Boot, giúp:

  • Auto config server/client
  • Inject stub như Spring Bean
  • Dễ dàng dùng interceptor, security

Cụ thể ta có thể cấu hình plugin maven như sau:

<properties>
    <java.version>17</java.version>
    <grpc.version>1.61.0</grpc.version>
    <protobuf.version>3.25.3</protobuf.version>
</properties>

<dependencies>
    <!-- gRPC Spring Boot Starter -->
    <dependency>
        <groupId>net.devh</groupId>
        <artifactId>grpc-spring-boot-starter</artifactId>
        <version>2.15.0.RELEASE</version>
    </dependency>

    <!-- Protobuf -->
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>${protobuf.version}</version>
    </dependency>

    <!-- gRPC -->
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-stub</artifactId>
        <version>${grpc.version}</version>
    </dependency>

    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf</artifactId>
        <version>${grpc.version}</version>
    </dependency>
</dependencies>

<build>
    <extensions>
        <!-- Detect OS để tải đúng binary protoc -->
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.7.1</version>
        </extension>
    </extensions>

    <plugins>
        <!-- Plugin generate code từ proto -->
        <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>
    </plugins>
</build>

Ví dụ: với proto sau:

syntax = "proto3";

package auth;

option java_multiple_files = true;
option java_package = "com.aqbtech.common.proto.auth";

service AuthService {
  rpc Login (LoginRequest) returns (LoginResponse);
}

message LoginRequest {
  string username = 1;
  string password = 2;
}

message LoginResponse {
  string access_token = 1;
  string refresh_token = 2;
}

Sau khi chạy protoc, ta sẽ có:

  • LoginRequest, LoginResponse class đại diện cho dữ liệu (immutable, builder pattern)
  • AuthServiceGrpcchứa:
    • AuthServiceImplBase (server implement)
    • AuthServiceBlockingStub
    • AuthServiceStub
    • AuthServiceFutureStub

Ta có 1 service implement như sau:

public class AuthServiceImpl extends AuthServiceGrpc.AuthServiceImplBase {

    @Override
    public void login(LoginRequest request,
                      StreamObserver<LoginResponse> responseObserver) {

        LoginResponse response = LoginResponse.newBuilder()
                .setAccessToken("token")
                .setRefreshToken("refresh")
                .build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

Trong đó StreamObserver là một interface trong gRPC Java, dùng để nhận hoặc gửi dữ liệu theo dạng stream giữa client và server.

Interface này có 3 phương thức chính:

publicinterfaceStreamObserver<V> {
void onNext(V value);
void onError(Throwable t);
void onCompleted();
}
  • onNext(V value)Gửi hoặc nhận một message
  • onError(Throwable t)Báo lỗi và kết thúc stream
  • onCompleted()Kết thúc stream thành công

3. Các loại stub trong Java

  • Blocking stub: gọi đồng bộ (dễ dùng, nhưng block thread)
  • Async stub: bất đồng bộ (non-blocking, dùng callback)
  • Future stub: trả về ListenableFuture

3.1. Blocking sub

Blocking stub là stub gọi đồng bộ (synchronous), nghĩa là:

  • Thread sẽ bị block cho đến khi nhận được response
  • Code viết đơn giản, giống gọi function bình thường

Ví dụ:

LoginResponse response = blockingStub.login(request);

Đặc điểm: Không phù hợp với: High throughput system, UI thread

3.2. Async stub

Async stub là stub gọi bất đồng bộ (non-blocking):

  • Không block thread
  • Nhận kết quả thông qua callback (StreamObserver)

Callback là một cơ chế trong lập trình bất đồng bộ, trong đó:

  • Một hàm (function/method) được truyền vào như một tham số
  • Hàm này sẽ được gọi lại (call back) khi một sự kiện xảy ra (ví dụ: nhận được response, xảy ra lỗi, hoàn tất xử lý)

Trong ngữ cảnh gRPC Async stub, khi gọi async:

asyncStub.login(request, new StreamObserver<LoginResponse>() {
    @Override
    public void onNext(LoginResponse value) {
        // xử lý khi nhận được response
    }

    @Override
    public void onError(Throwable t) {
        // xử lý khi có lỗi
    }

    @Override
    public void onCompleted() {
        // xử lý khi kết thúc
    }
});

Thì StreamObserver chính là callback object.

Cách hoạt động

  1. Client gửi request
  2. Không chờ response (non-blocking)
  3. Khi server trả dữ liệu gRPC runtime sẽ gọi onNext()
  4. Nếu có lỗi thì gọi onError()
  5. Khi hoàn tất thì gọi onCompleted()

Đặc điểm của callback

  • Không chạy ngay lập tức
  • Được thực thi khi có kết quả trả về
  • Thường được dùng trong:
    • I/O (network, file)
    • API bất đồng bộ
    • Event-driven system

3.3. Future stub

Future stub là dạng trung gian giữa blocking và async:

  • Trả về ListenableFuture
  • Có thể:
    • block (get())
    • hoặc xử lý async

Ví dụ:

ListenableFuture<LoginResponse> future = futureStub.login(request);

LoginResponse response = future.get();

Đặc điểm:

  • Linh hoạt hơn blocking
  • Dễ integrate với framework async (CompletableFuture, Guava)
  • Phù hợp khi cần control flow rõ ràng hơn async callback

4. Lợi ích của gRPC

  • Giảm lỗi do không đồng bộ contract
  • Tăng tốc độ phát triển
  • Dễ maintain khi hệ thống lớn

II. Cách cách gửi dữ liệu trong gRPC

Trong gRPC, cách client gửi dữ liệu phụ thuộc vào kiểu RPC được định nghĩa trong .proto. Có các kiểu gửi (RPC types) trong gRPC, định nghĩa 4 kiểu giao tiếp chính giữa client và server:

2.1. Unary RPC

  • Đặc điểm: 1 request → 1 response
  • Luồng: Client gửi 1 message, server xử lý và trả về 1 message

Proto syntax:

rpc GetUser (UserRequest) returns (UserResponse);

2.2. Streaming RPC

2.2.1 Server Streaming RPC

  • Đặc điểm: 1 request → nhiều response (stream từ server)
  • Luồng: Client gửi 1 message, server trả về nhiều message theo stream

Proto syntax:

rpc GetOrders (OrderRequest) returns (stream OrderResponse);

2.2.2. Client Streaming RPC

  • Đặc điểm: nhiều request → 1 response
  • Luồng: Client gửi nhiều message (stream), server trả về 1 message

Proto syntax:

rpc UploadLogs (stream LogRequest) returns (LogResponse);

2.2.3. Bidirectional Streaming RPC

  • Đặc điểm: nhiều request ↔ nhiều response (2 chiều, độc lập)
  • Luồng: Client và server đều gửi stream song song

Proto syntax:

rpc Chat (stream ChatMessage) returns (stream ChatMessage);

III. gRPC Error Handling (Status codes)

Trong gRPC, việc xử lý lỗi không sử dụng HTTP status như REST, mà dựa trên một hệ thống status code chuẩn hoá được định nghĩa sẵn. Điều này giúp đảm bảo tính nhất quán giữa các service, bất kể ngôn ngữ hay nền tảng.

3.1. Status Code là gì?

Status code trong gRPC là mã trạng thái được server trả về để mô tả kết quả của một RPC call.

Mỗi response trong gRPC đều đi kèm:

  • Status code (bắt buộc)
  • Message mô tả lỗi (tuỳ chọn)
  • Metadata bổ sung (tuỳ chọn)

Cấu trúc logic:

status = {
  code: StatusCode,
  description: String,
  metadata: Metadata (optional)
}

3.2. Các nhóm Status Code phổ biến

gRPC định nghĩa một tập hợp status code chuẩn (theo io.grpc.Status trong Java). Các code quan trọng:

Nhóm thành công

  • OK (0)

    Request xử lý thành công.

Nhóm lỗi phía client (4xx tương đương)

  • INVALID_ARGUMENT

    Dữ liệu đầu vào không hợp lệ (validation fail)

  • NOT_FOUND

    Không tìm thấy resource

  • ALREADY_EXISTS

    Resource đã tồn tại

  • FAILED_PRECONDITION

    Trạng thái hệ thống không phù hợp để thực hiện request

  • OUT_OF_RANGE

    Giá trị nằm ngoài phạm vi cho phép

  • UNAUTHENTICATED

    Chưa xác thực (thiếu/invalid token)

  • PERMISSION_DENIED

    Không có quyền truy cập

Nhóm lỗi phía server (5xx tương đương)

  • INTERNAL

    Lỗi nội bộ server

  • UNAVAILABLE

    Service không sẵn sàng (down, timeout, network)

  • DEADLINE_EXCEEDED

    Request bị timeout

  • RESOURCE_EXHAUSTED

    Hết tài nguyên (rate limit, memory, quota)

  • ABORTED

    Operation bị huỷ do conflict

Nhóm đặc biệt

  • CANCELLED

    Client chủ động huỷ request

  • UNKNOWN

    Lỗi không xác định

  • UNIMPLEMENTED

    Method chưa được implement

3.3. Cách gRPC trả lỗi

Trong gRPC, lỗi không trả qua response message, mà được đẩy qua channel error.

Ví dụ (Server - Java)

public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
    if (request.getUserId().isEmpty()) {
        responseObserver.onError(
            Status.INVALID_ARGUMENT
                .withDescription("userId must not be empty")
                .asRuntimeException()
        );
        return;
    }

    User user = findUser(request.getUserId());

    if (user == null) {
        responseObserver.onError(
            Status.NOT_FOUND
                .withDescription("User not found")
                .asRuntimeException()
        );
        return;
    }

    responseObserver.onNext(toResponse(user));
    responseObserver.onCompleted();
}

3.4. Phía Client nhận lỗi như thế nào?

Client sẽ nhận lỗi thông qua exception:

try {
    UserResponse res = blockingStub.getUser(request);
} catch (StatusRuntimeException e) {
    Status.Code code = e.getStatus().getCode();

    if (code == Status.Code.NOT_FOUND) {
        // handle not found
    } else if (code == Status.Code.INVALID_ARGUMENT) {
        // handle validation error
    }
}

IV. gRPC Interceptors (Client & Server side)

4.1. gRPC Interceptor là gì

gRPC Interceptor là cơ chế cho phép chèn logic vào trước hoặc sau khi một RPC được thực thi ở cả phía client và server.

Đặc điểm:

  • Hoạt động như một lớp trung gian trong pipeline xử lý request/response
  • Không thay đổi business logic chính
  • Dùng để xử lý các cross-cutting concerns:
    • Authentication / Authorization
    • Logging
    • Tracing
    • Metrics
    • Retry / Timeout

4.2. Client-side Interceptor

Chạy ở phía client trước khi request được gửi đi.

Mục đích

  • Thêm metadata (authorization, user-id)
  • Logging request
  • Retry / cấu hình timeout

Luồng xử lý

Client → ClientInterceptor → gRPC call → Server

Ví dụ (Java)

public class AuthClientInterceptor implements ClientInterceptor {

    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
            MethodDescriptor<ReqT, RespT> method,
            CallOptions callOptions,
            Channel next) {

        return new ForwardingClientCall.SimpleForwardingClientCall<>(
                next.newCall(method, callOptions)) {

            @Override
            public void start(Listener<RespT> responseListener, Metadata headers) {
                headers.put(
                    Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER),
                    "Bearer token-value"
                );
                super.start(responseListener, headers);
            }
        };
    }
}

4.3. Server-side Interceptor

Chạy ở phía server trước khi request được xử lý bởi service.

Mục đích

  • Xác thực (authentication)
  • Kiểm tra quyền (authorization)
  • Logging
  • Extract metadata

Luồng xử lý

Client → ServerInterceptor → Service Handler

Ví dụ (Java)

public class AuthServerInterceptor implements ServerInterceptor {

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {

        String token = headers.get(
            Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER)
        );

        if (token == null) {
            call.close(Status.UNAUTHENTICATED, headers);
            return new ServerCall.Listener<ReqT>() {};
        }

        return next.startCall(call, headers);
    }
}

V. gRPC Metadata (Header handling)

5.1. gRPC Metadata là gì

gRPC Metadata là cơ chế truyền dữ liệu dạng key–value kèm theo request/response, tương tự như HTTP headers.

Đặc điểm:

  • Không nằm trong message protobuf (không thuộc payload chính)
  • Được gửi kèm trong quá trình gọi RPC
  • Dùng để truyền thông tin bổ sung

Các loại dữ liệu thường dùng:

  • authorization (JWT, token)
  • x-user-id
  • trace-id
  • request-id

5.2. Vai trò của Metadata

Metadata dùng để xử lý các thông tin ngoài business data:

  • Authentication / Authorization
    • Truyền token từ client → server
  • Tracing / Logging
    • Truyền trace-id xuyên suốt hệ thống
  • Context propagation
    • Truyền thông tin user, locale, timezone
  • Custom headers
    • Các thông tin nội bộ giữa các service

5.3. Phân loại Metadata

Trong gRPC, metadata được chia thành:

(1) Request Metadata (Headers)

  • Gửi từ client → server
  • Được xử lý trước khi service chạy

(2) Response Metadata (Headers & Trailers)

  • Server → client
  • Bao gồm:
    • Headers (gửi sớm)
    • Trailers (gửi khi kết thúc response)

5.4. Cách hoạt động trong luồng gRPC

Client → Metadata (headers) → Server
Server → Metadata (headers/trailers) → Client
  • Client attach metadata vào request
  • Server đọc metadata trong interceptor hoặc handler
  • Server có thể trả lại metadata cho client

5.5. Cách sử dụng Metadata (Java)

5.5.1. Tạo Metadata Key

Metadata.Key<String> AUTHORIZATION =
    Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
  • Key phải khai báo với kiểu dữ liệu
  • Thường dùng:
    • ASCII_STRING_MARSHALLER
    • BINARY_BYTE_MARSHALLER (cho dữ liệu nhị phân)

5.5.2. Gửi Metadata từ Client

Metadata metadata = new Metadata();
metadata.put(AUTHORIZATION, "Bearer token-value");

stub = MetadataUtils.attachHeaders(stub, metadata);

5.5.3. Nhận Metadata ở Server (Interceptor)

String token = headers.get(AUTHORIZATION);
  • Thường đọc trong ServerInterceptor
  • Sau đó:
    • Validate token
    • Gắn vào context

5.5.4. Gửi Metadata từ Server về Client

Metadata responseHeaders = new Metadata();
responseHeaders.put(
    Metadata.Key.of("custom-header", Metadata.ASCII_STRING_MARSHALLER),
    "value"
);

call.sendHeaders(responseHeaders);

5.6. Quy tắc và lưu ý

  • Key phải viết thường (lowercase)
  • Key kết thúc bằng bin nếu là binary
  • Metadata không dùng để thay thế payload chính, vì
  • Kích thước metadata có giới hạn (tùy implementation, thường vài KB)

5.7. Kết hợp với Interceptor

Metadata thường được xử lý thông qua interceptor:

Client Interceptor có nhiệm vụ gắn metadata vào request

Client → Interceptor → attach metadata → gửi request

Server Interceptor được dùng để đọc và xử lý metadata

Nhận request → Interceptor → đọc metadata → validate → handler

VI. Syntax cơ bản của gRPC (Protobuf IDL)

Một file .proto thường gồm các thành phần chính:

  • Syntax version
syntax = "proto3";
  • Package (namespace)
package user;
  • Option (tuỳ chọn cho code generation)

Ví dụ Java:

option java_package = "com.example.user";
option java_multiple_files = true;
  • Message (định nghĩa data structure)
message UserRequest {
  string user_id = 1;
}

message UserResponse {
  string name = 1;
  int32 age = 2;
}
  • Mỗi field có:
    • kiểu dữ liệu (string, int32, …)
    • field number (1, 2, 3, …) → dùng cho serialization
  • Service (định nghĩa API)
service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}
  • Các kiểu dữ liệu thường dùng

    • Scalar types:

      • string, int32, int64, bool, bytes, …
    • Repeated (array):

      repeated string tags = 3;
      
    • Map:

      map<string, string> metadata = 4;
      
  • Enum

enum Status {
  UNKNOWN = 0;
  ACTIVE = 1;
  INACTIVE = 2;
}
  • Nested message (lồng nhau)
message Order {
  string id = 1;

  message Item {
    string name = 1;
    int32 quantity = 2;
  }

  repeated Item items = 2;
}
  • Tổng kết cấu trúc một file .proto
syntax = "proto3";

package example;

option java_package = "com.example";

message Request {
  string id = 1;
}

message Response {
  string result = 1;
}

service ExampleService {
  rpc ExampleMethod (Request) returns (Response);
}
9 views