Јава гРПЦ од нуле

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