Lad os undersøge, hvordan man implementerer gRPC i Java.
gRPC (Google Remote Procedure Call): gRPC er en open source RPC-arkitektur udviklet af Google for at muliggøre højhastighedskommunikation mellem mikrotjenester. gRPC giver udviklere mulighed for at integrere tjenester skrevet på forskellige sprog. gRPC bruger Protobuf-meddelelsesformatet (Protocol Buffers), et yderst effektivt, meget pakket meddelelsesformat til serialisering af strukturerede data.
I nogle tilfælde kan gRPC API være mere effektiv end REST API.
Lad os prøve at skrive en server på gRPC. Først skal vi skrive flere .proto-filer, der beskriver tjenester og modeller (DTO). For en simpel server bruger vi ProfileService og ProfileDescriptor.
Profilservice ser sådan ud:
syntax = "proto3"; package com.deft.grpc; import "google/protobuf/empty.proto"; import "profile_descriptor.proto"; service ProfileService { rpc GetCurrentProfile (google.protobuf.Empty) returns (ProfileDescriptor) {} rpc clientStream (stream ProfileDescriptor) returns (google.protobuf.Empty) {} rpc serverStream (google.protobuf.Empty) returns (stream ProfileDescriptor) {} rpc biDirectionalStream (stream ProfileDescriptor) returns (stream ProfileDescriptor) {} }
gRPC understøtter en række klient-server kommunikationsmuligheder. Vi opdeler dem alle sammen:
- Normalt serverkald – anmodning/svar.
- Streaming fra klient til server.
- Streaming fra server til klient.
- Og selvfølgelig den tovejsstrøm.
ProfileService-tjenesten bruger ProfileDescriptor, som er specificeret i importafsnittet:
syntax = "proto3"; package com.deft.grpc; message ProfileDescriptor { int64 profile_id = 1; string name = 2; }
- int64 er længe for Java. Lad profil-id’et høre til.
- String – ligesom i Java er dette en strengvariabel.
Du kan bruge Gradle eller Maven til at bygge projektet. Det er mere bekvemt for mig at bruge maven. Og yderligere vil være koden ved hjælp af maven. Dette er vigtigt nok at sige, fordi for Gradle vil den fremtidige generation af .proto være lidt anderledes, og build-filen skal konfigureres anderledes. For at skrive en simpel gRPC-server behøver vi kun én afhængighed:
<dependency> <groupId>io.github.lognet</groupId> <artifactId>grpc-spring-boot-starter</artifactId> <version>4.5.4</version> </dependency>
Det er bare utroligt. Denne starter gør et enormt arbejde for os.
Projektet, som vi vil skabe, vil se nogenlunde således ud:
Vi har brug for GrpcServerApplication for at starte Spring Boot-applikationen. Og GrpcProfileService, som skal implementere metoder fra .proto-tjenesten. For at bruge protoc og generere klasser fra skrevne .proto-filer, skal du tilføje protobuf-maven-plugin til pom.xml. Byggeafsnittet vil se sådan ud:
<build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.6.2</version> </extension> </extensions> <plugins> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.6.1</version> <configuration> <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot> <outputDirectory>${basedir}/target/generated-sources/grpc-java</outputDirectory> <protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact> <clearOutputDirectory>false</clearOutputDirectory> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
- protoSourceRoot – angiver den mappe, hvor .proto-filerne er placeret.
- outputDirectory – vælg den mappe, hvor filerne skal genereres.
- clearOutputDirectory – et flag, der angiver, at du ikke må rydde genererede filer.
På dette stadium kan du bygge et projekt. Dernæst skal du gå til den mappe, som vi har angivet i outputmappen. De genererede filer vil være der. Nu kan du gradvist implementere GrpcProfileService.
Klasseerklæringen ser således ud:
@GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
GRpcService annotation – Markerer klassen som en grpc-service bønne.
Da vi arver vores service fra ProfileServiceGrpc, ProfileServiceImplBase, kan vi tilsidesætte metoderne for den overordnede klasse. Den første metode, vi tilsidesætter, er getCurrentProfile:
@Override public void getCurrentProfile(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { System.out.println("getCurrentProfile"); responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(1) .setName("test") .build()); responseObserver.onCompleted(); }
For at svare på klienten skal du kalde onNext-metoden på den beståede StreamObserver. Når du har sendt svaret, skal du sende et signal til klienten om, at serveren er færdig med at arbejde på Completed. Når du sender en anmodning til getCurrentProfile-serveren, vil svaret være:
{ "profile_id": "1", "name": "test" }
Lad os derefter tage et kig på serverstrømmen. Med denne meddelelsestilgang sender klienten en anmodning til serveren, serveren svarer klienten med en strøm af meddelelser. For eksempel sender den fem anmodninger i en løkke. Når afsendelsen er fuldført, sender serveren en besked til klienten om den vellykkede afslutning af streamen.
Den tilsidesatte serverstream-metode vil se sådan ud:
@Override public void serverStream(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { for (int i = 0; i < 5; i++) { responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(i) .build()); } responseObserver.onCompleted(); }
Klienten vil således modtage fem beskeder med et ProfileId svarende til svarnummeret.
{ "profile_id": "0", "name": "" } { "profile_id": "1", "name": "" } … { "profile_id": "4", "name": "" }
Klientstrøm minder meget om serverstrøm. Først nu sender klienten en strøm af meddelelser, og serveren behandler dem. Serveren kan behandle beskeder med det samme eller vente på alle anmodninger fra klienten og derefter behandle dem.
@Override public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> clientStream(StreamObserver<Empty> responseObserver) { return new StreamObserver<>() { @Override public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) { log.info("ProfileDescriptor from client. Profile id: {}", profileDescriptor.getProfileId()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { responseObserver.onCompleted(); } }; }
I klientstrømmen skal du returnere StreamObserveren til klienten, hvortil serveren vil modtage beskeder. OnError-metoden vil blive kaldt, hvis der opstod en fejl i streamen. For eksempel afsluttede det forkert.
For at implementere en tovejsstrøm er det nødvendigt at kombinere oprettelse af en strøm fra serveren og klienten.
@Override public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> biDirectionalStream( StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { return new StreamObserver<>() { int pointCount = 0; @Override public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) { log.info("biDirectionalStream, pointCount {}", pointCount); responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(pointCount++) .build()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { responseObserver.onCompleted(); } }; }
I dette eksempel vil serveren som svar på klientens besked returnere en profil med et øget pointCount.
Konklusion
Vi har dækket de grundlæggende muligheder for meddelelser mellem en klient og en server ved hjælp af gRPC: implementeret serverstrøm, klientstrøm, tovejsstrøm.
Artiklen er skrevet af Sergey Golitsyn