Java gRPC från Scratch

Låt oss utforska hur man implementerar gRPC i Java.

gRPC (Google Remote Procedure Call): gRPC är en RPC-arkitektur med öppen källkod utvecklad av Google för att möjliggöra höghastighetskommunikation mellan mikrotjänster. gRPC tillåter utvecklare att integrera tjänster skrivna på olika språk. gRPC använder meddelandeformatet Protobuf (Protocol Buffers), ett mycket effektivt, mycket packat meddelandeformat för att serialisera strukturerad data.

För vissa användningsfall kan gRPC API vara mer effektivt än REST API.

Låt oss försöka skriva en server på gRPC. Först måste vi skriva flera .proto-filer som beskriver tjänster och modeller (DTO). För en enkel server använder vi ProfileService och ProfileDescriptor.

ProfileService ser ut så här:

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 stöder en mängd olika klient-serverkommunikationsalternativ. Vi delar upp dem alla:

  • Normalt serveranrop – begäran/svar.
  • Streamar från klient till server.
  • Streamar från server till klient.
  • Och, naturligtvis, den dubbelriktade strömmen.

Tjänsten ProfileService använder ProfileDescriptor, som anges i importsektionen:

syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • int64 är Long for Java. Låt profil-id tillhöra.
  • String – precis som i Java är detta en strängvariabel.

Du kan använda Gradle eller Maven för att bygga projektet. Det är bekvämare för mig att använda maven. Och ytterligare kommer att vara koden med hjälp av maven. Detta är tillräckligt viktigt att säga eftersom för Gradle kommer den framtida generationen av .proto att vara något annorlunda, och byggfilen kommer att behöva konfigureras annorlunda. För att skriva en enkel gRPC-server behöver vi bara ett beroende:

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

Det är bara otroligt. Denna starter gör ett enormt arbete för oss.

Projektet som vi kommer att skapa kommer att se ut ungefär så här:

Vi behöver GrpcServerApplication för att starta Spring Boot-applikationen. Och GrpcProfileService, som kommer att implementera metoder från .proto-tjänsten. För att använda protoc och generera klasser från skrivna .proto-filer, lägg till protobuf-maven-plugin till pom.xml. Byggsektionen kommer att se ut så här:

<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 – anger katalogen där .proto-filerna finns.
  • outputDirectory – välj katalogen där filerna ska genereras.
  • clearOutputDirectory – en flagga som indikerar att man inte ska rensa genererade filer.

I detta skede kan du bygga ett projekt. Därefter måste du gå till mappen som vi angav i utdatakatalogen. De genererade filerna kommer att finnas där. Nu kan du gradvis implementera GrpcProfileService.

Klassdeklarationen kommer att se ut så här:

@GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

GRpcService-anteckning – Markerar klassen som en grpc-serviceböna.

Eftersom vi ärver vår tjänst från ProfileServiceGrpc, ProfileServiceImplBase, kan vi åsidosätta metoderna för den överordnade klassen. Den första metoden vi kommer att åsidosätta är 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();
    }

För att svara på klienten måste du anropa onNext-metoden på den skickade StreamObservern. Efter att ha skickat svaret skickar du en signal till klienten att servern har slutat arbeta på Completed. När du skickar en förfrågan till getCurrentProfile-servern kommer svaret att vara:

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

Låt oss sedan ta en titt på serverströmmen. Med detta meddelandehanteringssätt skickar klienten en begäran till servern, servern svarar klienten med en ström av meddelanden. Till exempel skickar den fem förfrågningar i en loop. När sändningen är klar skickar servern ett meddelande till klienten om att strömmen har slutförts.

Den åsidosatta serverströmningsmetoden kommer att se ut så här:

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

Således kommer klienten att få fem meddelanden med ett ProfileId, lika med svarsnumret.

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

Klientström är mycket lik serverström. Först nu sänder klienten en ström av meddelanden och servern bearbetar dem. Servern kan behandla meddelanden omedelbart eller vänta på alla förfrågningar från klienten och sedan bearbeta 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 måste du returnera StreamObserver till klienten, till vilken servern kommer att ta emot meddelanden. Metoden onError kommer att anropas om ett fel inträffade i strömmen. Till exempel avslutades det felaktigt.

För att implementera en dubbelriktad ström är det nödvändigt att kombinera att skapa en ström från servern och 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 det här exemplet, som svar på klientens meddelande, kommer servern att returnera en profil med ett ökat antal poäng.

Slutsats

Vi har täckt de grundläggande alternativen för meddelanden mellan en klient och en server med hjälp av gRPC: implementerad serverström, klientström, dubbelriktad ström.

Artikeln skrevs av Sergey Golitsyn