شروع کار با gRPC-Java

۱. مقدمه

در این آزمایشگاه کد، شما از gRPC-Java برای ایجاد یک کلاینت و سرور استفاده خواهید کرد که پایه و اساس یک برنامه مسیریابی نوشته شده در جاوا را تشکیل می‌دهند.

در پایان این آموزش، شما یک کلاینت خواهید داشت که با استفاده از gRPC به یک سرور راه دور متصل می‌شود تا نام یا آدرس پستی آنچه را که در مختصات خاص روی نقشه قرار دارد، دریافت کند. یک برنامه کامل ممکن است از این طراحی کلاینت-سرور برای شمارش یا خلاصه کردن نقاط مورد علاقه در طول یک مسیر استفاده کند.

API سرور در یک فایل Protocol Buffers تعریف شده است که برای تولید کد قالبی (boilerplate code) برای کلاینت و سرور استفاده می‌شود تا بتوانند با یکدیگر ارتباط برقرار کنند و در زمان و تلاش شما برای پیاده‌سازی آن قابلیت صرفه‌جویی شود.

این کد تولید شده نه تنها پیچیدگی‌های ارتباط بین سرور و کلاینت، بلکه سریال‌سازی و از سریال‌زدایی داده‌ها را نیز برطرف می‌کند.

آنچه یاد خواهید گرفت

  • نحوه استفاده از بافرهای پروتکل برای تعریف یک API سرویس.
  • نحوه ساخت یک کلاینت و سرور مبتنی بر gRPC از تعریف Protocol Buffers با استفاده از تولید خودکار کد.
  • آشنایی با ارتباطات کلاینت-سرور با gRPC

این آزمایشگاه کد برای توسعه‌دهندگان جاوا که تازه با gRPC آشنا شده‌اند یا به دنبال مرور gRPC هستند، یا هر کسی که به ساخت سیستم‌های توزیع‌شده علاقه‌مند است، مناسب است. هیچ تجربه قبلی gRPC لازم نیست.

۲. قبل از شروع

پیش‌نیازها

  • JDK نسخه ۸ یا بالاتر

کد را دریافت کنید

برای اینکه مجبور نباشید کاملاً از ابتدا شروع کنید، این codelab چارچوبی از کد منبع برنامه را برای تکمیل شما فراهم می‌کند. مراحل زیر نحوه تکمیل برنامه، از جمله استفاده از افزونه‌های کامپایلر بافر پروتکل برای تولید کد gRPC قالب‌بندی شده را به شما نشان می‌دهد.

ابتدا، دایرکتوری کاری codelab را ایجاد کنید و با دستور cd به آن وارد شوید:

mkdir grpc-java-getting-started && cd grpc-java-getting-started

کدلب را دانلود و استخراج کنید:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-java-getting-started/start_here

روش دیگر این است که فایل .zip که فقط شامل دایرکتوری codelab است را دانلود کرده و به صورت دستی آن را از حالت فشرده خارج کنید.

اگر می‌خواهید از تایپ کردن پیاده‌سازی صرف‌نظر کنید، کد منبع تکمیل‌شده در گیت‌هاب موجود است.

۳. تعریف سرویس

اولین قدم شما تعریف سرویس gRPC برنامه، متد RPC آن و انواع پیام‌های درخواست و پاسخ آن با استفاده از Protocol Buffers است. سرویس شما موارد زیر را ارائه خواهد داد:

  • یک متد RPC به نام GetFeature که سرور پیاده‌سازی می‌کند و کلاینت آن را فراخوانی می‌کند.
  • انواع پیام Point و Feature هستند که ساختارهای داده‌ای هستند که هنگام استفاده از متد GetFeature بین کلاینت و سرور رد و بدل می‌شوند. کلاینت مختصات نقشه را به عنوان یک Point در درخواست GetFeature خود به سرور ارائه می‌دهد و سرور با یک Feature مربوطه که هر آنچه را که در آن مختصات قرار دارد توصیف می‌کند، پاسخ می‌دهد.

این متد RPC و انواع پیام‌های آن، همگی در فایل src/main/proto/routeguide/route_guide.proto از کد منبع ارائه شده تعریف خواهند شد.

بافرهای پروتکل معمولاً به عنوان protobufs شناخته می‌شوند. برای اطلاعات بیشتر در مورد اصطلاحات gRPC، به مفاهیم اصلی، معماری و چرخه حیات gRPC مراجعه کنید.

از آنجایی که ما در این مثال کد جاوا تولید می‌کنیم، یک گزینه فایل java_package و یک نام برای کلاس جاوا در .proto خود مشخص کرده‌ایم:

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

انواع پیام

در فایل routeguide/route_guide.proto از کد منبع، ابتدا نوع پیام Point را تعریف کنید. یک Point نشان دهنده یک جفت مختصات طول و عرض جغرافیایی روی نقشه است. برای این codelab، از اعداد صحیح برای مختصات استفاده کنید:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

اعداد 1 و 2 شماره‌های شناسایی منحصر به فرد برای هر یک از فیلدهای موجود در ساختار message هستند.

سپس، نوع پیام Feature را تعریف کنید. یک Feature از یک فیلد string برای نام یا آدرس پستی چیزی در مکانی که توسط Point مشخص شده است، استفاده می‌کند:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

روش خدمات

فایل route_guide.proto دارای یک ساختار service به نام RouteGuide است که یک یا چند متد ارائه شده توسط سرویس برنامه را تعریف می‌کند.

متد GetFeature rpc را به تعریف RouteGuide اضافه کنید. همانطور که قبلاً توضیح داده شد، این متد نام یا آدرس یک مکان را از مجموعه‌ای از مختصات مشخص جستجو می‌کند، بنابراین GetFeature یک Feature برای یک Point مشخص برمی‌گرداند:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

این یک روش RPC تکی است: یک RPC ساده که در آن کلاینت یک درخواست به سرور ارسال می‌کند و منتظر پاسخ می‌ماند، درست مانند یک فراخوانی تابع محلی.

۴. تولید کد کلاینت و سرور

در مرحله بعد باید رابط‌های کلاینت و سرور gRPC را از تعریف سرویس .proto خود تولید کنیم. ما این کار را با استفاده از کامپایلر بافر پروتکل protoc با یک افزونه ویژه gRPC Java انجام می‌دهیم. برای تولید سرویس‌های gRPC باید از کامپایلر proto3 (که از هر دو سینتکس proto2 و proto3 پشتیبانی می‌کند) استفاده کنید.

هنگام استفاده از Gradle یا Maven، افزونه protoc build می‌تواند کد لازم را به عنوان بخشی از ساخت تولید کند. برای نحوه تولید کد از فایل‌های .proto خودتان، می‌توانید به grpc-java README مراجعه کنید.

ما یک محیط Gradle و پیکربندی آن را در کد منبع codelab برای ساخت این پروژه ارائه داده‌ایم.

داخل دایرکتوری grpc-java-getting-started ، دستور زیر را اجرا کنید:

$ chmod +x gradlew
$ ./gradlew generateProto

کلاس‌های زیر از تعریف سرویس ما تولید می‌شوند:

  • Feature.java ، Point.java و موارد دیگر که شامل تمام کد بافر پروتکل برای پر کردن، سریال‌سازی و بازیابی انواع پیام‌های درخواست و پاسخ ما هستند.
  • RouteGuideGrpc.java که شامل (همراه با برخی کدهای مفید دیگر) یک کلاس پایه برای پیاده‌سازی سرورهای RouteGuide ، RouteGuideGrpc.RouteGuideImplBase ، به همراه تمام متدهای تعریف شده در سرویس RouteGuide و کلاس‌های stub برای استفاده کلاینت‌ها است.

۵. پیاده‌سازی سرور

ابتدا بیایید نگاهی به نحوه ایجاد یک سرور RouteGuide بیندازیم. برای اینکه سرویس RouteGuide ما کارش را انجام دهد، دو بخش وجود دارد:

  • پیاده‌سازی رابط سرویس تولید شده از تعریف سرویس ما، که «کار» واقعی سرویس ما را انجام می‌دهد.
  • اجرای یک سرور gRPC برای گوش دادن به درخواست‌های کلاینت‌ها و ارسال آنها به پیاده‌سازی صحیح سرویس.

پیاده‌سازی RouteGuide

همانطور که می‌بینید، سرور ما یک کلاس RouteGuideService دارد که کلاس انتزاعی تولید شده‌ی RouteGuideGrpc.RouteGuideImplBase را ارث‌بری می‌کند:

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}

ما دو فایل زیر را برای مقداردهی اولیه سرور شما با ویژگی‌ها ارائه کرده‌ایم:

./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java

./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json

بیایید یک پیاده‌سازی ساده RPC را با جزئیات بررسی کنیم.

RPC یگانه

RouteGuideService تمام متدهای سرویس ما را پیاده‌سازی می‌کند. در این مورد، فقط GetFeature() است، یک پیام Point از کلاینت می‌گیرد و در یک پیام Feature اطلاعات مکان مربوطه را از لیستی از مکان‌های شناخته شده برمی‌گرداند.

@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
  responseObserver.onNext(checkFeature(request));
  responseObserver.onCompleted();
}

متد getFeature() دو پارامتر می‌گیرد:

  • Point : درخواست.
  • StreamObserver<Feature> : یک ناظر پاسخ، که یک رابط ویژه برای سرور است تا پاسخ خود را فراخوانی کند.

برای ارسال پاسخ به مشتری و تکمیل تماس:

  1. ما یک شیء پاسخ Feature را برای بازگرداندن به کلاینت، همانطور که در تعریف سرویس ما مشخص شده است، ساخته و پر می‌کنیم. در این مثال، ما این کار را در یک متد خصوصی جداگانه checkFeature() انجام می‌دهیم.
  2. ما از متد onNext() در ناظر پاسخ برای بازگرداندن Feature استفاده می‌کنیم.
  3. ما از متد onCompleted() در ناظر پاسخ استفاده می‌کنیم تا مشخص کنیم که کارمان با RPC تمام شده است.

سرور را شروع کنید

پس از پیاده‌سازی تمام متدهای سرویس، باید یک سرور gRPC راه‌اندازی کنیم تا کلاینت‌ها بتوانند از سرویس ما استفاده کنند. ما در قالب کد خود، ایجاد شیء ServerBuilder را نیز قرار می‌دهیم:

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

ما سرویس را در سازنده (constructor) می‌سازیم:

  1. پورتی را که می‌خواهیم برای گوش دادن به درخواست‌های کلاینت استفاده کنیم، با استفاده از متد forPort() سازنده مشخص کنید (از آدرس wildcard استفاده خواهد کرد).
  2. یک نمونه از کلاس پیاده‌سازی سرویس RouteGuideService ایجاد کنید و آن را به متد addService() سازنده منتقل کنید.
  3. برای ایجاد یک سرور RPC برای سرویس ما، تابع build() را روی سازنده فراخوانی کنید.

قطعه کد زیر نحوه ایجاد یک شیء ServerBuilder را نشان می‌دهد.

/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
        port, RouteGuideUtil.parseFeatures(featureFile));
  }

قطعه کد زیر نحوه ایجاد یک شیء سرور برای سرویس RouteGuide را نشان می‌دهد.

/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
  this.port = port;
  server = serverBuilder.addService(new RouteGuideService(features))
      .build();
}

یک متد start پیاده‌سازی کنید که start روی سروری که در بالا ایجاد کردیم، فراخوانی کند.

public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

روشی پیاده‌سازی کنید که منتظر بماند تا سرور کارش تمام شود و بلافاصله از آن خارج نشود.

/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
  if (server != null) {
    server.awaitTermination();
  }
}

همانطور که می‌بینید، ما سرور خود را با استفاده از ServerBuilder می‌سازیم و راه‌اندازی می‌کنیم.

در روش اصلی ما:

  1. یک نمونه RouteGuideServer ایجاد کنید.
  2. برای فعال کردن یک سرور RPC برای سرویس ما، تابع start() را فراخوانی کنید.
  3. با فراخوانی تابع blockUntilShutdown() منتظر بمانید تا سرویس متوقف شود.
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

۶. مشتری را ایجاد کنید

در این بخش، به ایجاد یک کلاینت برای سرویس RouteGuide خود خواهیم پرداخت.

نمونه‌سازی یک مقاله خرد

برای فراخوانی متدهای سرویس، ابتدا باید یک stub ایجاد کنیم. دو نوع stub وجود دارد، اما ما فقط باید از stub مسدودکننده برای این codelab استفاده کنیم. این دو نوع عبارتند از:

  • یک stub مسدودکننده/همزمان که یک فراخوانی RPC انجام می‌دهد و منتظر پاسخ سرور می‌ماند، و یا پاسخی را برمی‌گرداند یا یک استثنا ایجاد می‌کند.
  • یک stub غیر مسدودکننده/ناهمزمان که فراخوانی‌های غیر مسدودکننده را به سرور انجام می‌دهد، و در آنجا پاسخ به صورت ناهمزمان بازگردانده می‌شود. شما می‌توانید انواع خاصی از فراخوانی‌های جریانی را فقط با استفاده از stub ناهمزمان انجام دهید.

ابتدا باید یک کانال gRPC ایجاد کنیم و سپس از آن برای ایجاد stub خود استفاده کنیم.

ما می‌توانستیم مستقیماً از یک ManagedChannelBuilder برای ایجاد کانال استفاده کنیم.

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

اما بیایید از یک متد کاربردی استفاده کنیم که رشته‌ای با hostname:port دریافت می‌کند.

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

حالا می‌توانیم از کانال برای ایجاد blocking stub خود استفاده کنیم. برای این آزمایشگاه کد، ما فقط blocking RPC داریم، بنابراین از متد newBlockingStub ارائه شده در کلاس RouteGuideGrpc که از .proto خود تولید کرده‌ایم، استفاده می‌کنیم.

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

روش‌های سرویس تماس

حالا بیایید نگاهی به نحوه فراخوانی متدهای سرویس خود بیندازیم.

RPC ساده

فراخوانی متد ساده‌ی RPC GetFeature تقریباً به سادگی فراخوانی یک متد محلی است.

ما یک شیء بافر پروتکل درخواست (در مورد ما Point ) ایجاد و پر می‌کنیم، آن را به متد getFeature() در stub مسدودکننده خود منتقل می‌کنیم و یک Feature برمی‌گردانیم.

اگر خطایی رخ دهد، به صورت Status کدگذاری می‌شود که می‌توانیم آن را از StatusRuntimeException به دست آوریم.

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();

Feature feature;
try {
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

این الگوی کد، پیامی را ثبت می‌کند که حاوی محتویاتی است که بر اساس وجود یا عدم وجود یک ویژگی در نقطه مشخص شده، تنظیم شده‌اند.

۷. امتحانش کن!

  1. داخل دایرکتوری start_here ، دستور زیر را اجرا کنید:
$ ./gradlew installDist

این دستور کد شما را کامپایل می‌کند، آن را در یک jar بسته‌بندی می‌کند و اسکریپت‌هایی را ایجاد می‌کند که مثال را اجرا می‌کنند. این اسکریپت‌ها در دایرکتوری build/install/start_here/bin/ ایجاد می‌شوند. این اسکریپت‌ها عبارتند از: route-guide-server و route-guide-client .

قبل از شروع کلاینت، سرور باید در حال اجرا باشد.

  1. سرور را اجرا کنید:
$ ./build/install/start_here/bin/route-guide-server
  1. کلاینت را اجرا کنید:
$ ./build/install/start_here/bin/route-guide-client

خروجی مانند این را خواهید دید، که برای وضوح بیشتر، مهرهای زمانی حذف شده‌اند:

INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906
INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619
INFO: *** GetFeature: lat=0 lon=0
INFO: Found no feature at 0, 0

۸. قدم بعدی چیست؟