Java gRPC από το μηδέν

Ας εξερευνήσουμε πώς να εφαρμόσουμε το gRPC στην Java.

gRPC (Google Remote Procedure Call): Το gRPC είναι μια αρχιτεκτονική RPC ανοιχτού κώδικα που αναπτύχθηκε από την Google για να επιτρέπει την επικοινωνία υψηλής ταχύτητας μεταξύ των μικροϋπηρεσιών. Το gRPC επιτρέπει στους προγραμματιστές να ενσωματώνουν υπηρεσίες γραμμένες σε διαφορετικές γλώσσες. Το gRPC χρησιμοποιεί τη μορφή ανταλλαγής μηνυμάτων Protobuf (Protocol Buffers), μια εξαιρετικά αποτελεσματική, εξαιρετικά γεμάτη μορφή μηνυμάτων για τη σειριοποίηση δομημένων δεδομένων.

Για ορισμένες περιπτώσεις χρήσης, το gRPC API μπορεί να είναι πιο αποτελεσματικό από το REST API.

Ας προσπαθήσουμε να γράψουμε έναν διακομιστή στο gRPC. Αρχικά, πρέπει να γράψουμε πολλά αρχεία .proto που περιγράφουν υπηρεσίες και μοντέλα (DTO). Για έναν απλό διακομιστή, θα χρησιμοποιήσουμε το ProfileService και το ProfileDescriptor.

Το ProfileService μοιάζει με αυτό:

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 υποστηρίζει μια ποικιλία επιλογών επικοινωνίας πελάτη-διακομιστή. Θα τα αναλύσουμε όλα:

  • Κανονική κλήση διακομιστή – αίτημα/απόκριση.
  • Ροή από πελάτη σε διακομιστή.
  • Ροή από διακομιστή σε πελάτη.
  • Και, φυσικά, το αμφίδρομο ρεύμα.

Η υπηρεσία ProfileService χρησιμοποιεί το ProfileDescriptor, το οποίο καθορίζεται στην ενότητα εισαγωγής:

syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • Το int64 είναι Long για Java. Αφήστε το αναγνωριστικό προφίλ να ανήκει.
  • String – όπως και στην Java, αυτή είναι μια μεταβλητή συμβολοσειράς.
  Πώς να αλλάξετε το Family Manager στο PS4

Μπορείτε να χρησιμοποιήσετε το Gradle ή το Maven για να δημιουργήσετε το έργο. Είναι πιο βολικό για μένα να χρησιμοποιώ το maven. Και περαιτέρω θα είναι ο κωδικός που χρησιμοποιεί το maven. Αυτό είναι αρκετά σημαντικό να πούμε γιατί για το Gradle, η μελλοντική γενιά του .proto θα είναι ελαφρώς διαφορετική και το αρχείο build θα πρέπει να ρυθμιστεί διαφορετικά. Για να γράψουμε έναν απλό διακομιστή gRPC, χρειαζόμαστε μόνο μία εξάρτηση:

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

Είναι απλά απίστευτο. Αυτό το μίζα κάνει τεράστια δουλειά για εμάς.

Το έργο που θα δημιουργήσουμε θα μοιάζει κάπως έτσι:

Χρειαζόμαστε GrpcServerApplication για να ξεκινήσουμε την εφαρμογή Spring Boot. Και το GrpcProfileService, το οποίο θα εφαρμόσει μεθόδους από την υπηρεσία .proto. Για να χρησιμοποιήσετε το protoc και να δημιουργήσετε κλάσεις από γραπτά αρχεία .proto, προσθέστε το protobuf-maven-plugin στο pom.xml. Το τμήμα κατασκευής θα μοιάζει με αυτό:

<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 – καθορίζει τον κατάλογο όπου βρίσκονται τα αρχεία .proto.
  • outputDirectory – επιλέξτε τον κατάλογο όπου θα δημιουργηθούν τα αρχεία.
  • clearOutputDirectory – μια σημαία που υποδεικνύει ότι δεν πρέπει να διαγραφούν τα αρχεία που δημιουργούνται.

Σε αυτό το στάδιο, μπορείτε να δημιουργήσετε ένα έργο. Στη συνέχεια, πρέπει να μεταβείτε στον φάκελο που καθορίσαμε στον κατάλογο εξόδου. Τα αρχεία που δημιουργούνται θα είναι εκεί. Τώρα μπορείτε σταδιακά να εφαρμόσετε το GrpcProfileService.

  Ανακτήστε τα έσοδά σας με τη λύση εντοπισμού AdBlock για WordPress

Η δήλωση τάξης θα μοιάζει με αυτό:

@GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

Σχολιασμός GRpcService – Επισημαίνει την κλάση ως φασόλι grpc-service.

Εφόσον κληρονομούμε την υπηρεσία μας από το ProfileServiceGrpc, το ProfileServiceImplBase, μπορούμε να παρακάμψουμε τις μεθόδους της γονικής κλάσης. Η πρώτη μέθοδος που θα παρακάμψουμε είναι το 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();
    }

Για να απαντήσετε στον πελάτη, πρέπει να καλέσετε τη μέθοδο onNext στον περασμένο StreamObserver. Μετά την αποστολή της απάντησης, στείλτε ένα σήμα στον πελάτη ότι ο διακομιστής έχει ολοκληρώσει τη λειτουργία του Ολοκληρώθηκε. Όταν στέλνετε ένα αίτημα στον διακομιστή getCurrentProfile, η απάντηση θα είναι:

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

Στη συνέχεια, ας ρίξουμε μια ματιά στη ροή διακομιστή. Με αυτήν την προσέγγιση ανταλλαγής μηνυμάτων, ο πελάτης στέλνει ένα αίτημα στον διακομιστή, ο διακομιστής απαντά στον πελάτη με μια ροή μηνυμάτων. Για παράδειγμα, στέλνει πέντε αιτήματα σε έναν βρόχο. Όταν ολοκληρωθεί η αποστολή, ο διακομιστής στέλνει ένα μήνυμα στον πελάτη σχετικά με την επιτυχή ολοκλήρωση της ροής.

Η μέθοδος ροής διακομιστή που έχει παρακαμφθεί θα μοιάζει με αυτό:

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

Έτσι, ο πελάτης θα λάβει πέντε μηνύματα με ένα ProfileId, ίσο με τον αριθμό απάντησης.

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

Η ροή πελάτη είναι πολύ παρόμοια με τη ροή διακομιστή. Μόνο τώρα ο πελάτης μεταδίδει μια ροή μηνυμάτων και ο διακομιστής τα επεξεργάζεται. Ο διακομιστής μπορεί να επεξεργαστεί τα μηνύματα αμέσως ή να περιμένει όλα τα αιτήματα από τον πελάτη και στη συνέχεια να τα επεξεργαστεί.

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

Στη ροή Client, πρέπει να επιστρέψετε το StreamObserver στον πελάτη, στον οποίο ο διακομιστής θα λαμβάνει μηνύματα. Η μέθοδος oneError θα κληθεί εάν παρουσιαστεί σφάλμα στη ροή. Για παράδειγμα, τερματίστηκε λανθασμένα.

  Πώς να εγκαταστήσετε το πρόγραμμα περιήγησης Tor στο Chromebook σας

Για την υλοποίηση μιας αμφίδρομης ροής, είναι απαραίτητο να συνδυαστεί η δημιουργία μιας ροής από τον διακομιστή και τον πελάτη.

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

Σε αυτό το παράδειγμα, ως απόκριση στο μήνυμα του πελάτη, ο διακομιστής θα επιστρέψει ένα προφίλ με αυξημένο αριθμό σημείων.

συμπέρασμα

Καλύψαμε τις βασικές επιλογές για την ανταλλαγή μηνυμάτων μεταξύ ενός πελάτη και ενός διακομιστή χρησιμοποιώντας gRPC: υλοποιημένη ροή διακομιστή, ροή πελάτη, ροή αμφίδρομης κατεύθυνσης.

Το άρθρο γράφτηκε από τον Sergey Golitsyn