Implementacija gRPC u Javi: Detaljan Vodič
Zaronimo u svet gRPC i istražimo kako ga implementirati u Javi.
gRPC (Google Remote Procedure Call) je open-source RPC arhitektura koju je razvio Google s ciljem omogućavanja brze komunikacije među mikroservisima. gRPC omogućava programerima da integrišu usluge napisane na različitim programskim jezicima. Za razmenu poruka, gRPC koristi Protocol Buffers (Protobuf), visokoučinkovit i kompaktni format serijalizacije strukturiranih podataka.
U određenim scenarijima, gRPC API se može pokazati efikasnijim od REST API-ja.
Uputimo se u pisanje gRPC servera. Kao prvi korak, potrebno je definisati nekoliko .proto fajlova koji opisuju servise i modele (DTO). Za jednostavan server, koristićemo `ProfileService` i `ProfileDescriptor`.
`ProfileService` izgleda ovako:
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 podržava različite šablone komunikacije klijent-server. Razmotrićemo ih sve detaljno:
- Uobičajeni poziv servera – zahtev/odgovor.
- Strimovanje od klijenta ka serveru.
- Strimovanje od servera ka klijentu.
- Dvosmerni tok komunikacije.
Servis `ProfileService` koristi `ProfileDescriptor`, čija je definicija navedena u odeljku za uvoz:
syntax = "proto3"; package com.deft.grpc; message ProfileDescriptor { int64 profile_id = 1; string name = 2; }
- `int64` u Javi odgovara tipu `long`, što predstavlja ID profila.
- `string` je varijabla tipa string, identično kao u Javi.
Za izradu projekta možete koristiti Gradle ili Maven. U ovom primeru koristimo Maven. Važno je napomenuti da konfiguracija .proto fajlova i datoteka za izradu projekta može varirati u zavisnosti od izabranog alata. Za jednostavan gRPC server, potrebna je samo jedna zavisnost:
<dependency> <groupId>io.github.lognet</groupId> <artifactId>grpc-spring-boot-starter</artifactId> <version>4.5.4</version> </dependency>
Ovaj starter preuzima značajan deo posla.
Struktura projekta će izgledati otprilike ovako:
Potrebna nam je `GrpcServerApplication` za pokretanje Spring Boot aplikacije i `GrpcProfileService` koji implementira metode iz .proto servisa. Da bi se koristio protokol i generisale klase iz .proto datoteka, dodajte `protobuf-maven-plugin` u `pom.xml`. Konfiguracija sekcije za izgradnju projekta će izgledati ovako:
<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` – definisanje direktorijuma gde se nalaze .proto fajlovi.
- `outputDirectory` – specificiranje direktorijuma za generisanje fajlova.
- `clearOutputDirectory` – oznaka koja određuje da generisane datoteke ne budu brisane.
Nakon konfigurisanja, možete kreirati projekat. Generisani fajlovi će se pojaviti u direktorijumu koji ste definisali u `outputDirectory`. Sada možemo postepeno implementirati `GrpcProfileService`.
Deklaracija klase će izgledati ovako:
@GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
`@GRpcService` anotacija označava klasu kao gRPC servis.
Nasleđivanjem klase od `ProfileServiceGrpc.ProfileServiceImplBase`, možemo prebrisati metode nadređene klase. Prva metoda koju ćemo implementirati je `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(); }
Da biste odgovorili klijentu, potrebno je pozvati metodu `onNext` na prosleđenom `StreamObserver`. Nakon slanja odgovora, obavestite klijenta da je server završio sa radom pozivom `onCompleted`. Kada pošaljete zahtev serveru na `getCurrentProfile`, odgovor će biti:
{ "profile_id": "1", "name": "test" }
Sada se prebacujemo na tok servera. U ovom pristupu razmeni poruka, klijent šalje zahtev serveru, a server odgovara klijentu nizom poruka. Na primer, server šalje pet zahteva u petlji. Po završetku slanja, server obaveštava klijenta o uspešnom završetku toka.
Implementacija `serverStream` metode će izgledati ovako:
@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(); }
Klijent će primiti pet poruka, svaka sa `ProfileId` jednakim broju odgovora.
{ "profile_id": "0", "name": "" } { "profile_id": "1", "name": "" } … { "profile_id": "4", "name": "" }
Klijentski tok je vrlo sličan toku servera. Međutim, u ovom slučaju, klijent šalje tok poruka, a server ih obrađuje. Server može obraditi poruke odmah ili sačekati sve zahteve klijenta pre obrade.
@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(); } }; }
U metodi `clientStream`, potrebno je vratiti `StreamObserver` klijentu, preko kojeg server prima poruke. Metoda `onError` se poziva ukoliko dođe do greške u toku, na primer, u slučaju prekinute veze.
Za implementaciju dvosmernog toka, neophodno je kombinovati kreiranje tokova sa servera i klijenta.
@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(); } }; }
U ovom primeru, kao odgovor na poruku klijenta, server će vratiti profil sa inkrementiranim brojem poena.
Zaključak
Obradjene su osnovne opcije razmene poruka između klijenta i servera koristeći gRPC: implementirani su server stream, client stream i bidirectional stream.
Autor članka: Sergej Golitsin