Google remote procedure call - Giao thức giao tiếp client-server
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ạyprotocprotoc-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,LoginResponseclass đại diện cho dữ liệu (immutable, builder pattern)AuthServiceGrpcchứa:AuthServiceImplBase(server implement)AuthServiceBlockingStubAuthServiceStubAuthServiceFutureStub
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 messageonError(Throwable t)Báo lỗi và kết thúc streamonCompleted()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
- Client gửi request
- Không chờ response (non-blocking)
- Khi server trả dữ liệu gRPC runtime sẽ gọi
onNext() - Nếu có lỗi thì gọi
onError() - 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
- block (
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-idtrace-idrequest-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-idxuyên suốt hệ thống
- Truyền
- 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_MARSHALLERBINARY_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
binnế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
- kiểu dữ liệu (
- 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);
}
