Java gRPC de la zero

Să explorăm cum să implementăm gRPC în Java.

gRPC (Google Remote Procedure Call): gRPC este o arhitectură RPC open-source dezvoltată de Google pentru a permite comunicarea de mare viteză între microservicii. gRPC permite dezvoltatorilor să integreze servicii scrise în diferite limbi. gRPC folosește formatul de mesagerie Protobuf (Protocol Buffers), un format de mesagerie foarte eficient, foarte împachetat pentru serializarea datelor structurate.

Pentru unele cazuri de utilizare, API-ul gRPC poate fi mai eficient decât API-ul REST.

Să încercăm să scriem un server pe gRPC. În primul rând, trebuie să scriem mai multe fișiere .proto care descriu serviciile și modelele (DTO). Pentru un server simplu, vom folosi ProfileService și ProfileDescriptor.

ProfileService arată astfel:

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 acceptă o varietate de opțiuni de comunicare client-server. Le vom descompune pe toate:

  • Apel normal de server – cerere/răspuns.
  • Streaming de la client la server.
  • Streaming de la server la client.
  • Și, desigur, fluxul bidirecțional.

Serviciul ProfileService utilizează ProfileDescriptor, care este specificat în secțiunea de import:

syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • int64 este Long pentru Java. Lăsați ID-ul profilului să aparțină.
  • String – la fel ca în Java, aceasta este o variabilă șir.

Puteți folosi Gradle sau Maven pentru a construi proiectul. Este mai convenabil pentru mine să folosesc Maven. Și mai departe va fi codul folosind Maven. Acest lucru este suficient de important pentru a spune, deoarece pentru Gradle, viitoarea generație a .proto va fi ușor diferită, iar fișierul de compilare va trebui configurat diferit. Pentru a scrie un server gRPC simplu, avem nevoie de o singură dependență:

<dependency>
    <groupId>io.github.lognet</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>4.5.4</version>
</dependency>

Este pur și simplu incredibil. Acest starter face o cantitate enormă de muncă pentru noi.

  8 cei mai buni producători de diagrame ER pentru a vizualiza și proiecta baze de date

Proiectul pe care îl vom crea va arăta cam așa:

Avem nevoie de GrpcServerApplication pentru a porni aplicația Spring Boot. Și GrpcProfileService, care va implementa metode din serviciul .proto. Pentru a utiliza protoc și a genera clase din fișierele .proto scrise, adăugați protobuf-maven-plugin la pom.xml. Secțiunea de construire va arăta astfel:

<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 – specificând directorul în care se află fișierele .proto.
  • outputDirectory – selectați directorul în care vor fi generate fișierele.
  • clearOutputDirectory – un indicator care indică faptul că nu trebuie să ștergeți fișierele generate.

În această etapă, puteți construi un proiect. Apoi, trebuie să mergeți la folderul pe care l-am specificat în directorul de ieșire. Fișierele generate vor fi acolo. Acum puteți implementa treptat GrpcProfileService.

Declarația de clasă va arăta astfel:

@GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

Adnotare GRpcService – marchează clasa ca bean grpc-service.

Deoarece moștenim serviciul nostru de la ProfileServiceGrpc, ProfileServiceImplBase, putem suprascrie metodele clasei părinte. Prima metodă pe care o vom înlocui este 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();
    }

Pentru a răspunde clientului, trebuie să apelați metoda onNext pe StreamObserver transmis. După trimiterea răspunsului, trimiteți un semnal către client că serverul a terminat de lucrat la Terminat. Când trimiteți o solicitare către serverul getCurrentProfile, răspunsul va fi:

{
  "profile_id": "1",
  "name": "test"
}

În continuare, să aruncăm o privire la fluxul serverului. Cu această abordare de mesagerie, clientul trimite o cerere către server, serverul îi răspunde clientului cu un flux de mesaje. De exemplu, trimite cinci cereri într-o buclă. Când trimiterea este completă, serverul trimite un mesaj clientului despre finalizarea cu succes a fluxului.

  9 instrumente de corectare pentru a face postarea de pe blog să poată fi partajată

Metoda de flux de server suprascris va arăta astfel:

@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();
    }

Astfel, clientul va primi cinci mesaje cu un ProfileId, egal cu numărul de răspuns.

{
  "profile_id": "0",
  "name": ""
}
{
  "profile_id": "1",
  "name": ""
}
…
{
  "profile_id": "4",
  "name": ""
}

Fluxul client este foarte asemănător cu fluxul server. Abia acum clientul transmite un flux de mesaje, iar serverul le procesează. Serverul poate procesa mesajele imediat sau poate aștepta toate solicitările de la client și apoi le poate procesa.

    @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();
            }
        };
    }

În fluxul Client, trebuie să returnați StreamObserver clientului, la care serverul va primi mesaje. Metoda onError va fi apelată dacă a apărut o eroare în flux. De exemplu, s-a terminat incorect.

Pentru a implementa un flux bidirecțional, este necesar să combinați crearea unui flux de la server și client.

@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();
            }
        };
    } 

În acest exemplu, ca răspuns la mesajul clientului, serverul va returna un profil cu un pointCount crescut.

Concluzie

Am acoperit opțiunile de bază pentru mesageria între un client și un server folosind gRPC: flux server implementat, flux client, flux bidirecțional.

Articolul a fost scris de Serghei Golitsyn

  Raspberry Pi vs. Arduino: care este mai bun?