فلاتر و GRPC، بررسی عملی روی پلتفرم اندروید

Flutter + gRPC
Flutter + gRPC


به نام خدا

در این مطلب، قصد داریم روش استفاده از GRPC در فلاتر را به شکل مختصر و با نمونه کد توضیح دهیم. کدها روی اندروید تست شدند ولی احتمالا روی پلتفرم‌های غیر از وب باید کار کنند. این مطلب را برای تحقیق درس برنامه‌نویسی موبایل دانشگاه صنعتی شریف آماده کرده‌ایم.



جی‌آر‌پی‌سی چیست؟

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

از مزیت‌های gRPC می‌توان به performance بالای آن در مقابل راه حل‌هایی مثل استفاده از داده‌های JSON‌ای و REST، اجبار در تعریف شدن پیام‌ها، ارتباط دو طرفه و به صورت stream در کنار فراخوانی‌های معمولی نام برد.

برای اطلاعات بیشتر لینک روبرو را بخوانید: https://grpc.io/docs/what-is-grpc/introduction/

فرایند کار

در gRPC، ابتدا نوع و فرمت پیام‌ها را در قالب فایل‌های proto تعریف می‌کنیم، این فایل‌ها شامل اطلاعات جزئیات کل پیام‌ها، اطلاعات درخواست‌ها و پاسخ آن و سرویس‌ها که هر سرویس مجموعه‌ای از rpcهاست می‌شود.

بعد از تعریف proto، باید با کمک کامپایلر پروتوباف، فایل‌های پروتو‌ای که ساختیم را به کدهایی که برای زبانی که می‌خواهیم در آن برنامه بنویسیم تبدیل کنیم، در اینجا چون flutter را انتخاب کردیم، زبانمان dart می‌شود و خوش‌بختانه grpc، زبان dart را هم پشتیبانی می‌کند.

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

انواع RPCها در gRPC

بر اساس اینکه request و response از نوع یک پیام یا یک stream باشند، ما ۴ نوع RPC در gRPC داریم:

  • Unary: که یک درخواست و پاسخ ساده است.
  • Server streaming: کلاینت یک درخواست می‌فرستد، اما سرور یک جریان از پاسخ‌ها بر می‌گرداند.
  • Client streaming: کلاینت یک جریان از درخواست‌ها می‌فرستند و در پایان سرور یک پاسخ بر می‌گرداند.
  • Bidirectional streaming: ارتباط دو طرفه که هر کدام یک جریان می‌فرستند و می‌تواند پاسخ و دریافتی و ... باشد.

برای اطلاعات بیشتر لینک روبرو را بخوانید: https://grpc.io/docs/what-is-grpc/core-concepts/

شروع با ساختن یک پروژه‌ی فلاتر

ابتدا با کمک android studio یک پروژه بسازید، برای اینکار ابتدا مطمئن شوید که sdk مربوط به flutter و پلاگین‌های dart و flutter روی اندروید استودیو نصب باشد، سپس از منوی File، گزینه‌ی New Flutter project را انتخاب کنید، نوع پروژه را Flutter application انتخاب کنید و فرایند را ادامه دهید.

بعد از ساخته شدن پروژه، کدهای فلاترمان در پوشه‌ی lib قرار دارند.

تعریف proto

در این مقاله، می‌خواهیم از هر کدام از این انواع یک تست انجام دهیم. چون هدفمان نوشتن یک بیزینس واقعی نیست، فرض کنید سرور ما، سروری برای تولید عدد تصادفی بین بازه‌ی minimum تا maximum است. ساده‌ترین درخواست به این سرور این است که یک بازه به آن بدهیم و سرور یک خروجی به ما بدهد که به نوع Unary می‌شود. درخواست دوم به این صورت است که یک بازه به سرور بدهیم، سپس سرور تا زمانی که درخواست باز است، هر ثانیه یک عدد تصادفی به کلاینت بفرستد. روش سوم، به این صورت است که کلاینت، یک جریان از بازه‌ها به سرور بفرستد، سپس سرور یک عدد تصادفی تولید کند که در همه‌ی بازه‌ها صدق کند.

مدل آخر مثلا فرض کنید کلاینت هر یک ثانیه یک‌بار، درخواست جدید می‌دهد و سرور برای هر درخواست، ۳ عدد تصادفی از آن مدل تولید می‌کند.

فایل پروتوی چنین کاری، کد زیر می‌شود:

syntax = &quotproto3&quot

package random_exchange;

service RandomExchangerService {
    rpc GetSingleRandom (RandomRequest) returns (RandomResponse);
    rpc GetRandomForEver (RandomRequest) returns (stream RandomResponse);
    rpc GetSingleRandomWithMultipleConditions (stream RandomRequest) returns (RandomResponse);
    rpc GetBidiRandom(stream RandomRequest) returns (stream RandomResponse);
}

message RandomRequest {
    int32 minimum = 1;
    int32 maximum = 2;
}

message RandomResponse {
    int32 value = 1;
}

همان‌طور که می‌بینید، دو نوع پیام داریم، یک پیام RandomRequest که بازه‌ی درخواست عدد تصادفی را مشخص کرده و RandomResponse که فقط یک عدد که آن عدد تصادفی است در آن است.

سرویس RandomExchangerService شامل چهار rpc است که همان چهار فراخوانی‌ای است که در بالا توضیح دادیم.

این فایل را در پوشه‌ی protos به نام random_exchange.proto قرار دهید.

کامپایل پروتو‌ها به زبان Dart

قبل از هر چیز، کامپایلر پروتوباف را نصب کنیم، برای این کار مثلا در ubuntu با کامند زیر می‌توانیم نصب را انجام دهیم:

sudo apt install protobuf-compiler

این کامپایلر، به شکل پیش‌فرض کامپایل به زبان c را پشتیبانی می‌کند، اما برای اینکه بتوانیم به زبان dart کامپایل کنیم، باید یک پلاگین روی آن نصب کنیم.

برای این کار، می‌توانیم از کامند زیر استفاده کنیم: (از تحریم شکن مناسب استفاده کنیم، چراکه به لطف گوگل تحریمیم! :| )

dart pub global activate protoc_plugin

این کامند، پکیج protoc_plugin را به شکل گلوبال روی سیستم فعال می‌کند، اما فایل‌های bin آن به صورت پیش‌فرض در مسیر environmentـِ PATH قرار ندارد و سیستم آن را نمی‌شناسد. در warningهای دستور بالا، پوشه‌ای که باید به PATH اضافه شود ذکر می‌شود، در کل باید دستور زیر را در محیط لینوکسی اجرا کنید:

export PATH=&quot$PATH:$HOME/.pub-cache/bin&quot

پلاگین نصب شد! فرض کنید فایل protoی بخش قبل را در پوشه‌ی protos، به نام random_exchange.proto ذخیره کردیم و کدهای flutterـمان را در پوشه‌ی lib می‌نویسیم، در این پوشه یک پوشه‌ی دیگر به نام generated می‌سازیم که کدهای کامپایل شده در آن قرار گیرند و سپس فرایند کامپایل را انجام می‌دهیم:

mkdir -p lib/generated
protoc --dart_out=grpc:lib/generated -Iprotos protos/random_exchange.proto

بعد از اجرای دستور بالا، باید تعدادی فایل در پوشه‌ی generated ساخته شده باشند.

اضافه کردن پیش‌نیاز‌ها

قبل از ادامه، نیاز به اضافه کردن تعدادی پیش‌نیاز داریم که grpc به درستی کار کند، برای اینکار فایل pubspec.yaml را باز کنید و نیازمندی‌های زیر را به dependencies اضافه کنید:

archive: ^3.0.0
async: ^2.5.0
crypto: ^3.0.0
fixnum: ^1.0.0
googleapis_auth: ^1.1.0
meta: ^1.3.0
http: ^0.13.0
http2: ^2.0.0
protobuf: ^2.0.0
grpc: ^3.0.0

و همچنین نیازمندی‌های زیر را به dev_dependencies اضافه کنید:

mockito: ^5.0.0
path: ^1.8.0
test: ^1.16.0
stream_channel: ^2.1.0
stream_transform: ^2.0.0

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

حال با اجرای `flutter pub get` یا `dart pub get` نیازمندی‌ها را دانلود کنید. (تحریمیم، از تحریم شکن مناسب استفاده کنید :) )

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

حال به سراغ پیاده‌سازی سرور برویم. سرور را در این مثال با dart پیاده‌سازی می‌کنیم، ولی می‌تواند با هر زبانی پیاده‌سازی شود.

قبل از پیاده‌سازی سرور، باید سرویس‌هایی که میخواهیم این سرور بتواند آن‌ها را هندل کند پیاده‌سازی کنیم، از کدهای تولید شده، کلاس RandomExchangerServiceBase به وسیله‌ی کامپایلر ساخته شده، آن‌را extend کرده و توابع آن‌را پیاده‌سازی می‌کنیم.

یک نمونه از این پیاده‌سازی در زیر آمده است. در دارت، gRPC از ساختارهای async/await پیش‌فرض دارت استفاده می‌کند، به همین خاطر به سادگی می‌توان کدها را با کمک آن‌ها ساده‌تر پیاده کرد.

تابع getSingleRandom با توجه به درخواست فقط یک خروجی بر می‌گرداند.

تابع getRandomForEver، یک ثانیه یک بار خروجی بر می‌گرداند و ...

import 'dart:math';

import 'package:grpc/grpc.dart';
import 'package:flutter_app/generated/random_exchange.pb.dart';
import 'package:flutter_app/generated/random_exchange.pbgrpc.dart';

class RandomExchangerService extends RandomExchangerServiceBase {
  var randomGenerator = new Random();

  @override
  Future<RandomResponse> getSingleRandom(
      ServiceCall call, RandomRequest request) async {
    return RandomResponse()
      ..value = randomGenerator.nextInt(request.maximum - request.minimum + 1) +
          request.minimum;
  }

  @override
  Stream<RandomResponse> getBidiRandom(
      ServiceCall call, Stream<RandomRequest> requests) async* {
    await for(var request in requests) {
      yield RandomResponse()
        ..value =
            randomGenerator.nextInt(request.maximum - request.minimum + 1) +
                request.minimum;
      await Future.delayed(Duration(milliseconds: 250));
      yield RandomResponse()
        ..value =
            randomGenerator.nextInt(request.maximum - request.minimum + 1) +
                request.minimum;
      await Future.delayed(Duration(milliseconds: 250));
      yield RandomResponse()
        ..value =
            randomGenerator.nextInt(request.maximum - request.minimum + 1) +
                request.minimum;
    }
  }

  @override
  Future<RandomResponse> getSingleRandomWithMultipleConditions(
      ServiceCall call, Stream<RandomRequest> requests) async {
    var minimum = -1000000000;
    var maximum = 1000000000;
    await for (var request in requests) {
      minimum = max(request.minimum, minimum);
      maximum = min(request.maximum, maximum);
    }
    maximum = max(minimum, maximum);
    return RandomResponse()
      ..value = randomGenerator.nextInt(maximum - minimum + 1) + minimum;
  }

  @override
  Stream<RandomResponse> getRandomForEver(
      ServiceCall call, RandomRequest request) async* {
    while (!call.isCanceled && !call.isTimedOut) {
      yield RandomResponse()
        ..value =
            randomGenerator.nextInt(request.maximum - request.minimum + 1) +
                request.minimum;
      await Future.delayed(Duration(seconds: 1));
    }
  }
}

حال نوبت این است که یک سرور روی این Service بالا بیاوریم، grpc خودش امکانات زیادی، از جمله gzip، استفاده از tls و ... را به ما می‌دهد که بهتر است آن‌ها را در محیط پروداکشن فعال کنیم، اما اکنون یک سرور با کانکشن insecure می‌سازیم تا درگیر پیچیدگی‌های ساختن certificate و ... نشویم.

کلاس Server از پکیج grpc برای اینکار به ما کمک می‌کند، نمونه کد زیر، روی پورت ۵۰۰۵۱ یک سرور خام که سرویس بالا از سرویس‌هایش هستند بالا می‌آورد:

final server = Server(
  [RandomExchangerService()],
);
await server.serve(address: '0.0.0.0', port: 50051);

این کد را داخل main قرار می‌دهیم:

Future<void> main(List<String> args) async {
  final server = Server(
    [RandomExchangerService()],
  );
  await server.serve(address: '0.0.0.0', port: 50051);
  print('Server listening on port ${server.port}...');
}


کد کامل سرور را در لینک روبرو می‌توانید ببینید: https://github.com/ATofighi/FlutterGRPC-Example/blob/master/lib/server.dart

تست کردن سرور با کمک Kreya

چون فرمت grpc به صورت باینری است، بدون یک کلاینتی که grpc بفهمد، نمی‌توان به سادگی آن را دیباگ و تست کرد، برای همین خاطر، تعداد debugger که gui به ما بدهند به وجود آمده که با کمک آن‌ها می‌توان ریکوئست از نوع grpc زد و خروجی را به شکل human readable که برای انسان قابل فهم باشد دید. یکی از این ابزار‌های Kreya است. از طریق لینک روبرو اقدام به نصب آن کنید: https://kreya.app/downloads/

پس از نصب و باز کردن آن، با صحنه‌ی زیر روبرو می‌شوید:

شروع کار با Kreya
شروع کار با Kreya

سپس روی Create project بزنید و یک پروژه‌ی جدید بسازید، سپس در Project، در Importers، یک ایمپورتر جدید ساخته، نوع آن‌ را grpc proto files بگذارید و از طریق بخش انتخاب پوشه، پوشه‌ی protos را انتخاب کنید و save کنید و از بالا راست Back بزنید.

در صفحه‌ی پیش رو، اطلاعات سرور grpc شامل endpoint و ... را می‌خواهد، certificate آن را disable کنید و endpoint را http://localhost:50051 وارد کنید.

سپس از منوی سمت چپ، می‌توانید Service و RPCهای درون آن را پیدا کنید، مثلا getSingleRandom را انتخاب می‌کنیم و یک درخواست به آن می‌فرستیم:

ارسال یک درخواست ساده با کمک Kreya
ارسال یک درخواست ساده با کمک Kreya

می‌توانید بقیه‌ی RPCها را هم تست کنید تا مطمئن شویم سرور کار می‌کند.

نوشتن کلاینت با فلاتر

تا اینجا مطمئن شدیم که سرور کار می‌کند، حال لازم است که اپ فلاتر را بنویسیم، در این اپ از سه استیت مربوط به minimum، maximum و value که مقدار تصادفی خروجی آمده از سرور است استفاده می‌کنیم، در بالای صفحه دو فیلد برای وارد کردن کمینه و بیشینه، در وسط صفحه عدد تصادفی تولید شده و در پایین صفحه دکمه‌های اجرای RPCها را قرار می‌دهیم. یک کد ابتدایی که Widgetها را شامل شود کد زیر است:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter GRPC Test'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _value = 0;
  int? _minimum = 0;
  int? _maximum = 10000;
  TextEditingController _minimumFieldController = TextEditingController();
  TextEditingController _maximumFieldController = TextEditingController();

  void _getSingleRandom() async {
    // TODO
  }

  void _getRandomsForEver() async {
    // TODO
  }

  void _getMultiConditionRandom() async {
    // TODO

  }

  void _getBidiRandom() async {
    // TODO
  }

  void _cancelRequest() async {
    // TODO
  }

  @override
  void initState() {
    super.initState();
    // TODO
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(&quotEnter bounds of range&quot),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Spacer(),
                    Expanded(
                        child: TextField(
                      d: (value) {
                        setState(() {
                          _minimum = int.tryParse(value);
                        });
                      },
                      controller: _minimumFieldController,
                      decoration: InputDecoration(hintText: &quotmin&quot),
                    )),
                    Spacer(),
                    Expanded(
                        child: TextField(
                      d: (value) {
                        setState(() {
                          _maximum = int.tryParse(value);
                        });
                      },
                      controller: _maximumFieldController,
                      decoration: InputDecoration(hintText: &quotmax&quot),
                    )),
                    Spacer(),
                  ],
                ),
                Spacer(),
                Text(
                  'You random number is:',
                ),
                Text(
                  '$_value',
                  style: Theme.of(context).textTheme.headline4,
                ),
                Spacer(),
              ]),
        ),
        floatingActionButton: SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Wrap(
              spacing: 10,
              alignment: WrapAlignment.spaceEvenly,
              children: [
                  OutlinedButton(
                      onPressed: _getSingleRandom,
                      child: Text(&quotGet Single Random&quot)),
                  OutlinedButton(
                      onPressed: _getMultiConditionRandom,
                      child: Text(&quotGet MultiConditional Random&quot)),
                  OutlinedButton(
                      onPressed: _getRandomsForEver,
                      child: Text(&quotGet Multiple Randoms&quot)),
                  OutlinedButton(
                      onPressed: _getBidiRandom,
                      child: Text(&quotGet Bidi Randoms&quot)),
                  OutlinedButton(
                      onPressed: _cancelRequest, child: Text(&quotCancel Request&quot))
              ],
            )));
  }
}

با اجرای آن ظاهر زیر را می‌بینیم:

ظاهر ابتدایی اپ :)
ظاهر ابتدایی اپ :)

البته این ظاهر با اصول طراحی فاصله دارد، اما برای اینکه کد پایه‌ی این مقاله شلوغ نشود و بتوانیم به اصل مطلب بپردازیم، از آن اغماض می‌کنیم.

اتصال به سرور و ساختن یک stub

برای اتصال به سرور به دو آبجکت نیاز داریم:

1. channel

که وظیفه‌ی برقرار ارتباط با سرور با کمک هاست و پورت مربوطه را برعهده دارد، همچنین هندل‌کردن certificate و یا گزینه‌های دیگر چون فشرده سازی برعهده‌ی channel است، از پکیج grpc/grpc.dart، کلاس ClientChannel چنین کاری را برایمان انجام می‌دهد، اگر فرض کنیم سرور روی آی‌پی 192.168.43.8 بالا آمده است، کد زیر یک channel می‌سازد:

final channel = ClientChannel('192.168.43.8',
    port: 50051,
    options:
        const ChannelOptions(credentials: ChannelCredentials.insecure()));

2. stub

در کنار channel، نیاز به یک stub داریم، stub، یک پیاده‌سازی از service است که اجرای هر کدام از توابع آن (که توابع سرویس هستند)، با کمک channel، یک rpc به سرور را در پیش دارد. یکی از کلاس‌های کدهای autogenerated مربوط به این پیاده‌سازی است که برای سرویس RandomExchangerService، کلاس RandomExchangerServiceClient است. با کد زیر stub را تعریف می‌کنیم:

stub = RandomExchangerServiceClient(channel);

یک مکان برای قرار دادن این کد‌ها، تابع initState است، مثلا کد زیر حالت کامل آن است:

import 'package:grpc/grpc.dart';

import 'generated/random_exchange.pbgrpc.dart';

.....


late ClientChannel channel;
late RandomExchangerServiceClient stub;

@override
void initState() {
  super.initState();
  channel = ClientChannel('192.168.43.8',
      port: 50051,
      options:
          const ChannelOptions(credentials: ChannelCredentials.insecure()));
  stub = RandomExchangerServiceClient(channel);
}


@override
void dispose() {
  super.dispose();
  channel.terminate();
}

دقت کنید که در dispose، کانکشن را بستیم، اگر این کار را نکنیم، ممکن است موجب ایجاد memory leak و file descriptor leak در برنامه‌مان شویم.

اجرای RPC به صورت Unary

برای اولین مثال، به پیاده‌سازی تابع _getSingleRandom بپردازیم، این تابع باید با توجه به متغیرهای _minimum و _maximum از State برنامه، یک فراخوان getSingleRandom را اجرا کند و بعد از آنکه خروجی آن آمد، استیت value را بروزرسانی کند.

پس باید یک پیام از نوع RandomRequest بسازیم، از کدهای generated، کلاس RandomRequest را داریم که در کانسترکتورش، آرگمان‌های minimum و maximum را می‌گیرد، پس با کد زیر می‌توان این کار را انجام داد:

RandomRequest(minimum: _minimum, maximum: _maximum)

و می‌توان به stub گفت که درخواست را برایمان بفرستد:

stub
        .getSingleRandom(RandomRequest(minimum: _minimum, maximum: _maximum))

خروجی از نوع Future<RandomResponse> خواهد بود، برای اینکه پیاده‌سازی راحت شود، می‌توانیم از پترن async await استفاده کنیم، به این صورت که تابع _getSingleRandom را به شکل async پیاده کنیم و روی فراخوان getSingleRandom، بیاییم و await کنیم، در این صورت در ادامه RandomResponse را داریم و به سادگی می‌توانیم استیت را آپدیت کنیم.

  void _getSingleRandom() async {
    final response = await stub
        .getSingleRandom(RandomRequest(minimum: _minimum, maximum: _maximum));
    setState(() {
      _value = response.value;
    });
  }

به همین سادگی اولین RPCمان را در کلاینت زدیم. :) بقیه‌ی چیزها به لطف پیاده‌سازی grpc انجام می‌شود.

پیاده‌سازی RPC به صورت Server Streaming

می‌خواهیم بعد از کلیک روی دکمه‌ی Gen Multiple Random، کلاینت فراخوانی getRandomForEver را صدا کند، سپس تا زمانی که سرور عدد می‌دهد، کلاینت این عدد را نشان دهد، همچنین بعد از کلیک روی دکمه‌ی Cancel، درخواست از طرف کلاینت متوقف شود و دیگر سرور stream نکند.

برای اینکار یک state به نام _responseStream اضافه می‌کنیم که استریم را داشته باشیم و اگر خواستیم بتوانیم کنسلش کنیم. خروجی stub.getRandomForEver به صورت Stream<RandomResponse> است، در اینجا هم می‌توان از پترن async/await استفاده کرد و روی stream، فور زد که کد آن به شکل زیر می‌شود:

ResponseStream? _responseStream;
void _getRandomsForEver() async {
  final responseStream = stub
      .getRandomForEver(RandomRequest(minimum: _minimum, maximum: _maximum));
  setState(() {
    _responseStream = responseStream;
  });
  await for (var response in responseStream) {
    setState(() {
      _value = response.value;
    });
  }
}

void _cancelRequest() async {
  _responseStream?.cancel();
  setState(() {
    _responseStream = null;
  });
}

پیاده‌سازی RPC به صورت Client Streaming

در این حالت فرض کنید که بعد از شروع، یک استیت _streamValues داریم که برابر با true می‌شود، سپس در زمانی که true است، ۱۰ ثانیه یک‌بار، مقدار minimum و maximum را برای سرور می‌فرستیم، سپس وقتی روی cancel کلیک کردیم، _streamValues برابر با false شود و جریان ارسال تمام شود. باید stub.getSingleRandomWithMultipleConditions را صدا کنیم، این تابع به عنوان ورودی Stream<RandomRequest> می‌گیرد، یک راه برای ساختن Stream، استفاده از روش async/await است، به این صورت که یک تابع از نوع async* بسازیم که عناصر استریمی که باید ساخته شود را yield کند. مثلا تابع زیر یک نمونه از چنین پیاده‌سازی‌ای است:

Stream<RandomRequest> requestBounds() async* {
  setState(() {
    _streamValues = true;
  });
  while (_streamValues) {
    await showDialog(
        context: context,
        builder: (context) => AlertDialog(
                title: Text(&quotEnter bound in next 10 seconds&quot),
                actions: [
                  TextButton(
                      onPressed: () => {Navigator.pop(context)},
                      child: Text('OK')),
                ]));
    await Future.delayed(Duration(seconds: 10));
    yield RandomRequest(minimum: _minimum, maximum: _maximum);
  }
}

ابتدا یک دیالوگ نشان می‌دهد که کاربر متوجه شود باید ۱۰ ثانیه دیگر ورودی وارد کند، سپس ۱۰ ثانیه صبر می‌کند و یک RandomRequest را yield می‌کند. البته این مدل پیاده‌سازی خیلی با اصول فاصله دارد، اما برای سادگی و دیدن اصل مطلب، وارد جزئیات طراحی محصول نشدیم.

کد کامل آن به شکل زیر می‌شود:

  bool _streamValues = false;

  void _getMultiConditionRandom() async {
    Stream<RandomRequest> requestBounds() async* {
      setState(() {
        _streamValues = true;
      });
      while (_streamValues) {
        await showDialog(
            context: context,
            builder: (context) => AlertDialog(
                    title: Text(&quotEnter bound in next 10 seconds&quot),
                    actions: [
                      TextButton(
                          onPressed: () => {Navigator.pop(context)},
                          child: Text('OK')),
                    ]));
        await Future.delayed(Duration(seconds: 10));
        yield RandomRequest(minimum: _minimum, maximum: _maximum);
      }
    }

    final response =
        await stub.getSingleRandomWithMultipleConditions(requestBounds());
    setState(() {
      _value = response.value;
    });
  }

پیاده‌سازی RPC به صورت Bidirectional

این روش ترکیبی از دو روش قبلی می‌شود، باید ورودی یک Stream<RandomRequest> دهیم و خروجی هم یک Stream<RandomRequest> است، ترتیب ورودی دادن و خروجی دادن مهم نیست، می‌توانیم هر تعداد ورودی بدهیم و هر تعداد خروجی به هر ترتیبی براساس لاجیک برنامه داشته باشیم، مثلا در این کلاینت و سرور، یک درخواست از کلاینت و سپس سه درخواست از کلاینت و دوباره به همین ترتیب انجام می‌شود، توضیحات کلی انجام این کار داده شده و فقط به پیاده‌سازی آن اشاره می‌کنیم:

  void _getBidiRandom() async {
    Stream<RandomRequest> requestBounds() async* {
      while (_streamValues) {
        await Future.delayed(Duration(seconds: 1));
        yield RandomRequest(minimum: _minimum, maximum: _maximum);
      }
    }

    setState(() {
      _streamValues = true;
    });

    final responseStream = stub.getBidiRandom(requestBounds());
    setState(() {
      _responseStream = responseStream;
    });

    await for (var response in responseStream) {
      setState(() {
        _value = response.value;
      });
    }
  }

پس این روش هم انجام شد.

کد کامل این پیاده‌سازی را از اینجا می‌توانید ببینید: https://github.com/ATofighi/FlutterGRPC-Example/blob/master/lib/main.dart

در ادامه

تا اینجا یک پیاده‌سازی اولیه از چهار روش ارتباطی در gRPC را دیدیم، اما این هنوز ابتدای راه است، کارهای دیگری هم لازم است برای اینکه محصول ما ارزش‌آفرینی داشته باشد انجام دهیم که در این مطلب به جزئیات آن‌ها نمی‌پردازیم، از جمله این موارد، موارد زیر هستند:

هندل کردن خطا‌ها و اکسپشن‌ها

در لحظه‌های await، ممکن است به هر دلیل خطا بخوریم و RandomRequest بر نگردد، مثلا ارتباط با سرور قطع شود، درخواست timeout شود و یا خودمان با فشاردادن cancel درخواست را timeout کنیم. سرور خطا بخورد و ...، بهتر است انواع این خطاها را catch کنیم و به کاربر خطای مناسب را نشان دهیم.

استفاده از metadataها

یکی از موارد در gRPC که به آن نپرداختیم، metadataهاست، متادیتاها مشابه آن‌چه از Headerها در http داشتیم عمل می‌کنند، عموما داده‌هایی مثل authorization، timeoutها و دیتاهایی که در تمام درخواست‌ها باید ارسال شوند را در metadata می‌گذاریم. برای این‌کار، پارامتر دوم توابع stub، آرگمان نام‌دار options است که کلاس CallOptions را ورودی می‌گیرد و این کلاس، آرگمان metadata دارد، مثلا کد زیر یک نمونه از آن است:

stub.getSingleRandom(request, options: CallOptions(timeout: Duration(milliseconds: 100), metadata: {
  'authorization': 'xx',
  'a': 'b',
}));

جی‌آر‌پی‌سی بر روی مرورگر

با اینکه فلاتر cross platform است، اما پروتوکل gRPC با توجه به ماهیتش هم‌اکنون روی مروگر‌ها پشتیبانی نمی‌شود، پس منطقا کدهایی که زدیم هم روی مرورگرها پشتیبانی نمی‌شوند! برای حل این مشکل، پروتوکل دیگری به نام gRPC-web پیاده‌سازی شده که gRPC را روی پروتوکل‌های HTTP به صورتی که مروگرها آن را پشتیبانی کنند سوار شده است، برای استفاده از آن باید یا از سروری که grpc-web را پشتیبانی کند استفاده کنیم و یا اینکه یک proxy که grpc را به grpc-web تبدیل کند (مثل envoy proxy) در سمت سرور استفاده کنیم. در سمت کلاینت هم از کانال GrpcWebClientChannel استفاده کنیم، در stubها تغییر خاصی اتفاق نمی‌افتد، اما باید دقت کنیم در برخی از حالت‌ها حالت bidirectional دیگر پشتیبانی نمی‌شود.

منابع



نویسندگان:

  • ارشیا مقیمی
  • حامد علی‌محمدزاده
  • علیرضا توفیقی محمدی

تهیه شده برای درس برنامه‌نویسی موبایل ارائه شده در نیم‌سال دوم تحصیلی ۰۰-۹۹ دانشگاه صنعتی شریف