1. Giới thiệu
Trong lớp học lập trình này, bạn sẽ sử dụng gRPC-Java để tạo một máy khách và máy chủ tạo thành nền tảng của một ứng dụng lập bản đồ tuyến đường được viết bằng Java.
Khi kết thúc hướng dẫn này, bạn sẽ có một ứng dụng kết nối với một máy chủ từ xa bằng gRPC để lấy tên hoặc địa chỉ bưu chính của nội dung nằm tại một toạ độ cụ thể trên bản đồ. Một ứng dụng hoàn chỉnh có thể sử dụng thiết kế máy chủ-máy khách này để liệt kê hoặc tóm tắt các địa điểm yêu thích dọc theo một tuyến đường.
API của máy chủ được xác định trong một tệp Protocol Buffers. Tệp này sẽ được dùng để tạo mã chuẩn cho ứng dụng và máy chủ để chúng có thể giao tiếp với nhau, giúp bạn tiết kiệm thời gian và công sức khi triển khai chức năng đó.
Mã được tạo này không chỉ xử lý sự phức tạp của việc giao tiếp giữa máy chủ và ứng dụng mà còn xử lý quá trình chuyển đổi dữ liệu thành chuỗi và chuyển đổi chuỗi thành dữ liệu.
Kiến thức bạn sẽ học được
- Cách sử dụng Protocol Buffers để xác định một API dịch vụ.
- Cách tạo máy khách và máy chủ dựa trên gRPC từ một định nghĩa Protocol Buffers bằng cách sử dụng tính năng tạo mã tự động.
- Hiểu rõ về giao tiếp máy khách-máy chủ bằng gRPC.
Lớp học lập trình này dành cho các nhà phát triển Java mới làm quen với gRPC hoặc muốn tìm hiểu lại về gRPC, hoặc bất kỳ ai khác quan tâm đến việc xây dựng hệ thống phân tán. Bạn không cần có kinh nghiệm sử dụng gRPC.
2. Trước khi bắt đầu
Điều kiện tiên quyết
- JDK phiên bản 8 trở lên
Lấy mã
Để bạn không phải bắt đầu hoàn toàn từ đầu, lớp học lập trình này cung cấp một khung mã nguồn ứng dụng để bạn hoàn tất. Các bước sau đây sẽ hướng dẫn bạn cách hoàn tất ứng dụng, bao gồm cả việc sử dụng trình bổ trợ trình biên dịch vùng đệm giao thức để tạo mã gRPC chung.
Trước tiên, hãy tạo thư mục làm việc cho lớp học lập trình rồi chuyển đến thư mục đó:
mkdir grpc-java-getting-started && cd grpc-java-getting-started
Tải và giải nén lớp học lập trình:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-1/codelabs/grpc-java-getting-started/start_here
Ngoài ra, bạn có thể tải tệp .zip chỉ chứa thư mục codelab xuống rồi giải nén theo cách thủ công.
Bạn có thể xem mã nguồn hoàn chỉnh trên GitHub nếu muốn bỏ qua bước nhập nội dung triển khai.
3. Xác định dịch vụ
Bước đầu tiên là xác định dịch vụ gRPC của ứng dụng, phương thức RPC và các loại thông báo yêu cầu và phản hồi bằng cách sử dụng Protocol Buffers. Dịch vụ của bạn sẽ cung cấp:
- Một phương thức RPC có tên là
GetFeature
mà máy chủ triển khai và máy khách gọi. - Các loại thông báo
Point
vàFeature
là các cấu trúc dữ liệu được trao đổi giữa ứng dụng và máy chủ khi sử dụng phương thứcGetFeature
. Ứng dụng cung cấp toạ độ trên bản đồ dưới dạngPoint
trong yêu cầuGetFeature
gửi đến máy chủ, còn máy chủ sẽ trả lời bằng mộtFeature
tương ứng mô tả mọi thứ nằm ở toạ độ đó.
Phương thức RPC này và các loại thông báo của phương thức này sẽ được xác định trong tệp src/main/proto/routeguide/route_guide.proto
của mã nguồn được cung cấp.
Vùng đệm giao thức thường được gọi là protobuf. Để biết thêm thông tin về thuật ngữ gRPC, hãy xem phần Các khái niệm, cấu trúc và vòng đời cốt lõi của gRPC.
Vì đang tạo mã Java trong ví dụ này, nên chúng ta đã chỉ định một lựa chọn tệp java_package
và tên cho lớp Java trong .proto
:
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
Loại thông báo
Trong tệp routeguide/route_guide.proto
của mã nguồn, trước tiên hãy xác định kiểu thông báo Point
. Point
biểu thị một cặp toạ độ vĩ độ và kinh độ trên bản đồ. Trong lớp học lập trình này, hãy sử dụng số nguyên cho toạ độ:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
Các số 1
và 2
là số nhận dạng duy nhất cho từng trường trong cấu trúc message
.
Tiếp theo, hãy xác định loại thông báo Feature
. Feature
sử dụng trường string
cho tên hoặc địa chỉ bưu chính của một thứ gì đó tại một vị trí do Point
chỉ định:
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
Phương thức dịch vụ
Tệp route_guide.proto
có cấu trúc service
tên là RouteGuide
, xác định một hoặc nhiều phương thức do dịch vụ của ứng dụng cung cấp.
Thêm phương thức rpc
GetFeature
vào bên trong định nghĩa RouteGuide
. Như đã giải thích trước đó, phương thức này sẽ tra cứu tên hoặc địa chỉ của một vị trí từ một tập hợp toạ độ nhất định, vì vậy, hãy để GetFeature
trả về một Feature
cho một Point
nhất định:
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
Đây là một phương thức RPC đơn phương: một RPC đơn giản, trong đó ứng dụng gửi một yêu cầu đến máy chủ và đợi phản hồi quay lại, giống như một lệnh gọi hàm cục bộ.
4. Tạo mã máy khách và mã máy chủ
Tiếp theo, chúng ta cần tạo giao diện máy chủ và máy khách gRPC từ định nghĩa dịch vụ .proto
. Chúng tôi thực hiện việc này bằng cách sử dụng trình biên dịch vùng đệm giao thức protoc
với một trình bổ trợ Java gRPC đặc biệt. Bạn cần sử dụng trình biên dịch proto3 (hỗ trợ cả cú pháp proto2 và proto3) để tạo các dịch vụ gRPC.
Khi sử dụng Gradle hoặc Maven, trình bổ trợ bản dựng protoc
có thể tạo mã cần thiết trong quá trình tạo. Bạn có thể tham khảo README của grpc-java để biết cách tạo mã từ các tệp .proto
của riêng bạn.
Chúng tôi đã cung cấp một môi trường và cấu hình Gradle trong mã nguồn của lớp học lập trình để tạo dự án này.
Trong thư mục grpc-java-getting-started
, hãy chạy lệnh sau:
$ chmod +x gradlew $ ./gradlew generateProto
Các lớp sau đây được tạo từ định nghĩa dịch vụ của chúng tôi:
Feature.java
,Point.java
và những lớp khác chứa tất cả mã bộ đệm giao thức để điền, chuyển đổi tuần tự và truy xuất các loại thông báo yêu cầu và phản hồi của chúng ta.RouteGuideGrpc.java
chứa (cùng với một số mã hữu ích khác) một lớp cơ sở để các máy chủRouteGuide
triển khai,RouteGuideGrpc.RouteGuideImplBase
, với tất cả các phương thức được xác định trong dịch vụRouteGuide
và các lớp mã giả lập để máy khách sử dụng.
5. Triển khai máy chủ
Trước tiên, hãy xem cách chúng ta tạo một máy chủ RouteGuide
. Có hai phần để dịch vụ RouteGuide
thực hiện công việc của mình:
- Triển khai giao diện dịch vụ được tạo từ định nghĩa dịch vụ của chúng tôi, thực hiện "công việc" thực tế của dịch vụ.
- Chạy một máy chủ gRPC để lắng nghe các yêu cầu từ ứng dụng và gửi các yêu cầu đó đến đúng quy trình triển khai dịch vụ.
Triển khai RouteGuide
Như bạn có thể thấy, máy chủ của chúng ta có một lớp RouteGuideService
mở rộng lớp trừu tượng RouteGuideGrpc.RouteGuideImplBase
đã tạo:
private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}
Chúng tôi đã cung cấp 2 tệp sau đây để khởi động máy chủ của bạn bằng các tính năng:
./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java
./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json
Hãy xem xét chi tiết một cách triển khai RPC đơn giản.
RPC một chiều
RouteGuideService
triển khai tất cả các phương thức dịch vụ của chúng tôi. Trong trường hợp này, đó chỉ là GetFeature()
, lấy thông báo Point
từ máy khách và trả về thông tin vị trí tương ứng từ danh sách các địa điểm đã biết trong thông báo Feature
.
@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
responseObserver.onNext(checkFeature(request));
responseObserver.onCompleted();
}
Phương thức getFeature()
có 2 tham số:
Point
: yêu cầu.StreamObserver<Feature>
: một trình theo dõi phản hồi, là một giao diện đặc biệt để máy chủ gọi bằng phản hồi của mình.
Để trả về phản hồi của chúng tôi cho ứng dụng và hoàn tất lệnh gọi:
- Chúng tôi tạo và điền sẵn một đối tượng phản hồi
Feature
để trả về cho ứng dụng khách, như được chỉ định trong định nghĩa dịch vụ của chúng tôi. Trong ví dụ này, chúng ta thực hiện việc này trong một phương thứccheckFeature()
riêng tư. - Chúng ta sử dụng phương thức
onNext()
của đối tượng theo dõi phản hồi để trả vềFeature
. - Chúng ta sử dụng phương thức
onCompleted()
của trình theo dõi phản hồi để chỉ định rằng chúng ta đã hoàn tất việc xử lý RPC.
Khởi động máy chủ
Sau khi triển khai tất cả các phương thức dịch vụ, chúng ta cần khởi động một máy chủ gRPC để máy khách có thể thực sự sử dụng dịch vụ của chúng ta. Chúng tôi đưa vào mẫu tạo sẵn việc tạo đối tượng ServerBuilder:
ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)
Chúng ta tạo dịch vụ trong hàm khởi tạo:
- Chỉ định cổng mà chúng ta muốn dùng để theo dõi các yêu cầu của máy khách bằng phương thức
forPort()
của trình tạo (phương thức này sẽ dùng địa chỉ ký tự đại diện). - Tạo một thực thể của lớp triển khai dịch vụ
RouteGuideService
và truyền thực thể đó đến phương thứcaddService()
của trình tạo. - Gọi
build()
trên trình tạo để tạo một máy chủ RPC cho dịch vụ của chúng ta.
Đoạn mã sau đây cho biết cách chúng ta tạo một đối tượng ServerBuilder
.
/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
port, RouteGuideUtil.parseFeatures(featureFile));
}
Đoạn mã sau đây cho biết cách chúng ta tạo một đối tượng máy chủ cho dịch vụ RouteGuide
.
/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
this.port = port;
server = serverBuilder.addService(new RouteGuideService(features))
.build();
}
Triển khai một phương thức bắt đầu gọi start
trên máy chủ mà chúng ta đã tạo ở trên.
public void start() throws IOException {
server.start();
logger.info("Server started, listening on " + port);
}
Triển khai một phương thức để chờ máy chủ hoàn tất để máy chủ không thoát ngay lập tức.
/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
Như bạn có thể thấy, chúng ta tạo và khởi động máy chủ bằng cách sử dụng ServerBuilder
.
Trong phương thức chính, chúng ta sẽ:
- Tạo một thực thể
RouteGuideServer
. - Gọi
start()
để kích hoạt một máy chủ RPC cho dịch vụ của chúng tôi. - Chờ dịch vụ dừng bằng cách gọi
blockUntilShutdown()
.
public static void main(String[] args) throws Exception {
RouteGuideServer server = new RouteGuideServer(8980);
server.start();
server.blockUntilShutdown();
}
6. Tạo ứng dụng
Trong phần này, chúng ta sẽ xem xét việc tạo một ứng dụng cho dịch vụ RouteGuide
.
Tạo một phần phụ
Để gọi các phương thức dịch vụ, trước tiên, chúng ta cần tạo một stub. Có 2 loại stub, nhưng chúng ta chỉ cần dùng loại chặn cho lớp học lập trình này. Có 2 loại:
- một phần chặn/đồng bộ tạo lệnh gọi RPC và đợi máy chủ phản hồi, đồng thời sẽ trả về phản hồi hoặc đưa ra một ngoại lệ.
- một phần không chặn/không đồng bộ tạo các lệnh gọi không chặn đến máy chủ, trong đó phản hồi được trả về không đồng bộ. Bạn chỉ có thể thực hiện một số loại cuộc gọi truyền trực tuyến nhất định bằng cách sử dụng phần giữ chỗ không đồng bộ.
Trước tiên, chúng ta cần tạo một kênh gRPC, sau đó dùng kênh này để tạo phần giữ chỗ.
Chúng ta có thể dùng ManagedChannelBuilder
trực tiếp để tạo kênh.
ManagedChannelBuilder.forAddress(host, port).usePlaintext().build
Nhưng hãy sử dụng một phương thức tiện ích lấy một chuỗi có hostname:port
.
Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();
Giờ đây, chúng ta có thể dùng kênh này để tạo phần giữ chỗ chặn. Đối với lớp học lập trình này, chúng ta chỉ có các RPC chặn, vì vậy, chúng ta sẽ sử dụng phương thức newBlockingStub
được cung cấp trong lớp RouteGuideGrpc
mà chúng ta đã tạo từ .proto
.
blockingStub = RouteGuideGrpc.newBlockingStub(channel);
Gọi các phương thức dịch vụ
Bây giờ, hãy xem cách chúng ta gọi các phương thức dịch vụ.
Simple RPC
Việc gọi RPC GetFeature
đơn giản gần như dễ dàng như gọi một phương thức cục bộ.
Chúng ta tạo và điền vào một đối tượng vùng đệm giao thức yêu cầu (trong trường hợp của chúng ta là Point
), chuyển đối tượng đó đến phương thức getFeature()
trên phần giữ chỗ chặn và nhận lại Feature
.
Nếu xảy ra lỗi, lỗi đó sẽ được mã hoá dưới dạng Status
mà chúng ta có thể lấy từ StatusRuntimeException
.
Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();
Feature feature;
try {
feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
Boilerplate sẽ ghi lại một thông báo chứa nội dung dựa trên việc có hay không có một đối tượng tại điểm đã chỉ định.
7. Hãy dùng thử ngay!
- Trong thư mục
start_here
, hãy chạy lệnh sau:
$ ./gradlew installDist
Thao tác này sẽ biên dịch mã của bạn, đóng gói mã đó trong một tệp jar và tạo các tập lệnh chạy ví dụ. Các tệp này sẽ được tạo trong thư mục build/install/start_here/bin/
. Các tập lệnh là: route-guide-server
và route-guide-client
.
Máy chủ cần phải đang chạy trước khi khởi động máy khách.
- Chạy máy chủ:
$ ./build/install/start_here/bin/route-guide-server
- Chạy ứng dụng khách:
$ ./build/install/start_here/bin/route-guide-client
Bạn sẽ thấy kết quả như sau, trong đó dấu thời gian đã bị bỏ qua để đảm bảo sự rõ ràng:
INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906 INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619 INFO: *** GetFeature: lat=0 lon=0 INFO: Found no feature at 0, 0
8. Bước tiếp theo
- Tìm hiểu cách gRPC hoạt động trong phần Giới thiệu về gRPC và Các khái niệm cốt lõi
- Xem Hướng dẫn cơ bản
- Khám phá tài liệu tham khảo về API.