آموزش‌های کلادفلر به‌زودی در این بخش قرار داده می‌شود.

gRPC-Web چیست؟ ارتباط وب با سرویس‌های gRPC

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

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

یک نکته مهم که باید بدونید اینه که gRPC-Web الان به مرحله «عرضه عمومی» یا همون «Generally Available» رسیده. این یعنی چی؟ یعنی دیگه یک پروژه آزمایشی و نصفه و نیمه نیست. کاملا پایداره و شرکت‌ها و برنامه‌نویس‌ها میتونن با خیال راحت در محصولات نهایی و تجاری خودشون ازش استفاده کنن.

نکته کلیدی اینه که کلاینت‌های gRPC-Web نمیتونن مستقیم با سرویس‌های gRPC صحبت کنن. این وسط به یک واسطه یا یک جور مترجم نیاز دارن. این مترجم یک «پراکسی» یا «gateway» مخصوصه. به طور پیش‌فرض، کتابخانه gRPC-Web از پراکسی به اسم Envoy استفاده میکنه که پشتیبانی از gRPC-Web به صورت داخلی در اون تعبیه شده. پس روند کار اینطوری میشه: اپلیکیشن توی مرورگر شما درخواستش رو به زبان gRPC-Web به Envoy میفرسته، بعد Envoy اون درخواست رو ترجمه میکنه و به زبان gRPC اصلی به سرویس پشتیبان یا همون بک‌اند میفرسته. جواب رو هم به همین صورت برعکس ترجمه میکنه و به مرورگر برمیگردونه.

نگران نباشید، در آینده قراره این وضعیت بهتر هم بشه. تیم توسعه‌دهنده gRPC-Web انتظار داره که پشتیبانی از این تکنولوژی مستقیما به فریمورک‌های تحت وب در زبان‌های مختلف مثل پایتون، جاوا و نود جی‌اس اضافه بشه. این یعنی شاید در آینده دیگه نیازی به اون پراکسی جداگانه نباشه. اگه دوست دارید بیشتر در مورد برنامه‌های آینده بدونید، میتونید سند نقشه راه یا «roadmap» این پروژه رو مطالعه کنید.

چرا اصلا باید از gRPC و gRPC-Web استفاده کنیم؟

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

تمام پیچیدگی‌های ارتباط بین این زبان‌ها و محیط‌های مختلف توسط خود gRPC مدیریت میشه و شما دیگه درگیر جزئیات نمیشید. علاوه بر این، شما از تمام مزایای کار با «پروتکل بافر» یا همون «Protocol Buffers» هم بهره‌مند میشید. این مزایا شامل سریال‌سازی بهینه (یعنی تبدیل داده‌ها به فرمتی فشرده و سریع برای انتقال)، یک زبان تعریف رابط کاربری ساده (IDL) و قابلیت به‌روزرسانی آسان رابط‌ها میشه.

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

محدودیت‌های مرورگر و راه حل‌های موجود

یک حقیقت مهم وجود داره که باید بدونیم: امکان فراخوانی مستقیم یک سرویس gRPC از مرورگر وجود نداره. چرا؟ چون gRPC از ویژگی‌های پروتکل HTTP/2 استفاده میکنه و هیچ مرورگری اون سطح از کنترل دقیق روی درخواست‌های وب رو که برای پشتیبانی از یک کلاینت gRPC لازمه، در اختیار ما قرار نمیده. مثلا ما نمیتونیم مرورگر رو مجبور کنیم حتما از HTTP/2 استفاده کنه و حتی اگه میتونستیم، به فریم‌های خام HTTP/2 در مرورگرها دسترسی نداریم.

اینجا gRPC روی ASP.NET Core دو تا راه حل سازگار با مرورگر ارائه میده:

  • gRPC-Web
  • gRPC JSON transcoding

آشنایی با gRPC-Web

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

  • این روش خیلی شبیه به gRPC معمولیه، اما یک پروتکل انتقال داده کمی متفاوت داره که باعث میشه با HTTP/1.1 و مرورگرها سازگار باشه.
  • برای استفاده از این روش، اپلیکیشن مرورگر باید یک کلاینت gRPC رو از روی همون فایل .proto که تعریف کردیم، تولید کنه.
  • این روش به اپلیکیشن‌های مرورگر اجازه میده از مزایای پیام‌های باینری که عملکرد بالا و مصرف شبکه کمی دارن، بهره‌مند بشن.

خبر خوب اینه که دات نت (.NET) به صورت داخلی از gRPC-Web پشتیبانی میکنه.

آشنایی با gRPC JSON transcoding

این یک راه حل جایگزینه. این روش به اپلیکیشن‌های مرورگر اجازه میده تا سرویس‌های gRPC رو طوری فراخوانی کنن که انگار دارن با یک RESTful API معمولی با فرمت JSON کار میکنن.

  • در این حالت، اپلیکیشن مرورگر نیازی به تولید کلاینت gRPC نداره و اصلا لازم نیست چیزی در مورد gRPC بدونه.
  • RESTful API‌ها به صورت خودکار از روی سرویس‌های gRPC ساخته میشن. این کار با اضافه کردن یک سری حاشیه‌نویسی یا «metadata» مربوط به HTTP به فایل .proto انجام میشه.
  • این روش به یک اپلیکیشن اجازه میده که هم از gRPC و هم از JSON web API‌ها پشتیبانی کنه، بدون اینکه لازم باشه برای هر کدوم سرویس‌های جداگانه‌ای بسازه و کار تکراری انجام بده.

دات نت همچنین از ساخت JSON web API از روی سرویس‌های gRPC هم پشتیبانی داخلی داره. فقط یادتون باشه که برای استفاده از gRPC JSON transcoding به دات نت ۷ یا بالاتر نیاز دارید.

یک آموزش پایه‌ای: بیایید دست به کار بشیم

خب، تئوری کافیه. بیایید با یک مثال عملی ببینیم چطور میشه از gRPC-Web استفاده کرد. در این آموزش یاد میگیریم که:

  • چطور یک سرویس رو در یک فایل .proto تعریف کنیم.
  • چطور با استفاده از کامپایلر پروتکل بافر، کد کلاینت رو تولید کنیم.
  • چطور از API ی gRPC-Web برای نوشتن یک کلاینت ساده برای سرویسمون استفاده کنیم.

فرض ما اینه که شما یک آشنایی اولیه با پروتکل بافرها دارید.

مرحله ۱: تعریف سرویس در فایل echo.proto

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

message EchoRequest {
  string message = 1;
}

message EchoResponse {
  string message = 1;
}

service EchoService {
  rpc Echo(EchoRequest) returns (EchoResponse);
}

این تعریف خیلی ساده است. ما یک درخواست به نام EchoRequest داریم که یک پیام متنی میگیره. یک پاسخ به نام EchoResponse داریم که اون هم یک پیام متنی برمیگردونه. و یک سرویس به نام EchoService داریم که یک متد به نام Echo داره. این متد یک EchoRequest میگیره و یک EchoResponse برمیگردونه. کارش اینه که هر پیامی بهش بدی، همون رو بهت برگردونه. مثل یک اکو یا پژواک صدا.

مرحله ۲: پیاده‌سازی سرور بک‌اند gRPC

حالا که نقشه رو داریم، باید سرور رو بسازیم. در مرحله بعد، ما رابط EchoService رو با استفاده از Node.js در بک‌اند پیاده‌سازی میکنیم. این سرور که اسمش رو EchoServer میذاریم، درخواست‌های کلاینت‌ها رو مدیریت میکنه. برای دیدن جزئیات کامل کد، میتونید به فایل node-server/server.js در مثال‌های رسمی gRPC-Web مراجعه کنید.

یک نکته مهم اینه که شما میتونید سرور رو با هر زبانی که gRPC ازش پشتیبانی میکنه پیاده‌سازی کنید. این یکی از قدرت‌های gRPC هست.

یک تیکه کد از سرور نود جی‌اس این شکلیه:

function doEcho(call, callback) {
  callback(null, {message: call.request.message});
}

همونطور که میبینید، این تابع خیلی ساده است. یک درخواست (call) میگیره، پیام داخلش رو برمیداره و در یک پیام جدید به عنوان پاسخ (callback) برمیگردونه.

مرحله ۳: پیکربندی پراکسی Envoy

همونطور که قبلا گفتیم، برای اینکه درخواست مرورگر به سرور بک‌اند ما برسه، به یک مترجم یا پراکسی نیاز داریم. در این مثال، ما از پراکسی Envoy استفاده میکنیم. فایل پیکربندی کامل Envoy رو میتونید در envoy.yaml ببینید.

برای اینکه Envoy درخواست‌های gRPC رو به سرور بک‌اند ما فوروارد کنه، به یک بلوک تنظیمات شبیه به این نیاز داریم:

admin:
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8080 }
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route: { cluster: echo_service }
          http_filters:
          - name: envoy.grpc_web
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
  - name: echo_service
    connect_timeout: 0.25s
    type: LOGICAL_DNS
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options: {}
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: echo_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: node-server
                port_value: 9090

این تنظیمات شاید در نگاه اول پیچیده به نظر بیاد، ولی بیایید با هم بررسیش کنیم:

  • listeners: این بخش تعریف میکنه که Envoy روی چه آدرس و پورتی منتظر درخواست‌های ورودی باشه. اینجا روی پورت 8080 منتظر میمونه.
  • http_filters: اینجاست که جادو اتفاق میفته. ما دو تا فیلتر مهم داریم. یکی envoy.grpc_web که درخواست‌های gRPC-Web رو میشناسه و پردازش میکنه. دومی envoy.filters.http.router هست که درخواست رو به مقصد مناسبش هدایت میکنه.
  • clusters: این بخش تعریف میکنه که سرور بک‌اند ما کجاست. ما یک کلاستر به اسم echo_service تعریف کردیم که به آدرس node-server و پورت 9090 اشاره میکنه.

پس سناریو اینطوریه: مرورگر یک درخواست gRPC-Web به پورت 8080 میفرستسه. Envoy این درخواست رو دریافت میکنه، با استفاده از فیلتر grpc_web اون رو ترجمه میکنه و به سرور gRPC بک‌اند ما که روی پورت 9090 قرار داره، ارسال میکنه. پاسخ سرور رو هم به همین ترتیب برعکس ترجمه میکنه و برای مرورگر میفرسته.

گاهی اوقات لازمه یک سری تنظیمات مربوط به CORS هم اضافه کنید تا مرورگر اجازه داشته باشه محتوا رو از یک مبدا دیگه درخواست بده.

مرحله ۴: تولید پیام‌های پروتوباف و کلاینت سرویس

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

protoc -I=$DIR echo.proto \
--js_out=import_style=commonjs:$OUT_DIR
  • protoc: این همون کامپایلر پروتکل بافر هست.
  • -I=$DIR: به کامپایلر میگه فایل‌های .proto رو کجا پیدا کنه.
  • --js_out=import_style=commonjs:$OUT_DIR: این قسمت میگه که خروجی جاوااسکریپت بساز. import_style=commonjs باعث میشه که فایل‌های تولید شده از سیستم ماژول require() ی CommonJS استفاده کنن که در دنیای Node.js خیلی رایجه. $OUT_DIR هم پوشه خروجی رو مشخص میکنه.

خب، این فقط کلاس‌های مربوط به پیام‌ها رو ساخت. ما به کدهای مربوط به کلاینت سرویس هم نیاز داریم. برای این کار، به یک پلاگین مخصوص برای protoc به اسم protoc-gen-grpc-web نیاز داریم.

اول باید این پلاگین رو دانلود و نصب کنید. میتونید فایل باینری protoc رو از صفحه رسمی پروتکل بافر و پلاگین protoc-gen-grpc-web رو از صفحه گیت‌هاب gRPC-Web دانلود کنید. بعد باید مطمئن بشید که هر دوی این فایل‌ها اجرایی هستن و در PATH سیستم شما قرار دارن تا از هر جایی قابل دسترس باشن.

یک راه دیگه برای نصب پلاگین اینه که از سورس کد کامپایلش کنید. برای این کار باید از ریشه مخزن gRPC-Web دستور زیر رو اجرا کنید:

cd grpc-web
sudo make install-plugin

بعد از اینکه پلاگین آماده شد، با دستور زیر میتونیم کد کلاینت سرویس رو تولید کنیم:

protoc -I=$DIR echo.proto \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:$OUT_DIR

این دستور خیلی شبیه دستور قبلیه، ولی از --grpc-web_out استفاده میکنه که مخصوص پلاگین ماست. بیایید پارامترهاش رو بررسی کنیم:

  • mode: این پارامتر میتونه grpcwebtext (که پیش‌فرض هست) یا grpcweb باشه. این دو حالت در نحوه کدگذاری داده‌ها تفاوت دارن که بعدا بیشتر در موردش صحبت میکنیم.
  • import_style: این پارامتر هم میتونه closure (که پیش‌فرض هست) یا commonjs باشه. ما از commonjs استفاده کردیم تا با خروجی قبلیمون هماهنگ باشه.

این دستور به طور پیش‌فرض یک فایل به نام echo_grpc_web_pb.js تولید میکنه که شامل کد کلاینت ماست.

مرحله ۵: نوشتن کد کلاینت جاوااسکریپت

بالاخره رسیدیم به قسمت شیرین ماجرا. حالا آماده‌ایم که کد جاوااسکریپت سمت کلاینت رو بنویسیم. این کد رو در یک فایلی به نام client.js قرار میدیم.

const {EchoRequest, EchoResponse} = require('./echo_pb.js');
const {EchoServiceClient} = require('./echo_grpc_web_pb.js');

var echoService = new EchoServiceClient('http://localhost:8080');

var request = new EchoRequest();
request.setMessage('Hello World!');

echoService.echo(request, {}, function(err, response) {
  // اینجا میتونیم با پاسخ سرور کار کنیم
  // مثلا response.getMessage() رو چاپ کنیم
});

بیایید این کد رو با هم تحلیل کنیم:

  1. در دو خط اول، ما کلاس‌هایی که در مرحله قبل تولید کردیم (EchoRequest, EchoResponse, EchoServiceClient) رو با استفاده از require وارد برنامه‌مون میکنیم.
  2. بعد یک نمونه جدید از EchoServiceClient میسازیم و آدرس پراکسی Envoy خودمون (http://localhost:8080) رو بهش میدیم.
  3. یک نمونه جدید از EchoRequest میسازیم و پیام «Hello World!» رو داخلش قرار میدیم.
  4. در نهایت، متد echo رو روی سرویسمون فراخوانی میکنیم. به عنوان ورودی، درخواست (request)، یک آبجکت خالی برای متادیتا ({}) و یک تابع callback بهش میدیم. این تابع زمانی اجرا میشه که پاسخی از سرور دریافت بشه. این پاسخ میتونه خطا (err) یا جواب موفقیت‌آمیز (response) باشه.

برای اینکه این کد کار کنه، به یک سری پکیج دیگه هم نیاز داریم. باید یک فایل package.json در پروژه‌مون داشته باشیم:

{
  "name": "grpc-web-commonjs-example",
  "dependencies": {
    "google-protobuf": "^3.6.1",
    "grpc-web": "^0.4.0"
  },
  "devDependencies": {
    "browserify": "^16.2.2",
    "webpack": "^4.16.5",
    "webpack-cli": "^3.1.0"
  }
}

این فایل مشخص میکنه که پروژه ما به پکیج‌های google-protobuf و grpc-web نیاز داره. همچنین برای ساختن فایل نهایی برای مرورگر، از ابزارهایی مثل browserify یا webpack استفاده میکنیم.

مرحله ۶: کامپایل کتابخانه جاوااسکریپت

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

اول با دستور npm install پکیج‌های مورد نیاز رو نصب میکنیم.

بعد با دستوری مثل این، فایل نهایی رو میسازیم:

npx webpack client.js

این دستور فایل client.js ما و تمام وابستگی‌هاش رو در یک فایل به نام main.js در پوشه dist قرار میده. حالا کافیه این فایل dist/main.js رو در صفحه HTML خودمون قرار بدیم و نتیجه رو ببینیم!

یک راه سریع برای شروع

اگر حوصله انجام تمام این مراحل رو به صورت دستی ندارید، یک راه سریع‌تر هم وجود داره. میتونید از مثال‌های آماده خود gRPC-Web استفاده کنید:

$ git clone https://github.com/grpc/grpc-web
$ cd grpc-web
$ docker-compose up node-server envoy commonjs-client

این دستورات با استفاده از داکر، همه چیز رو براتون آماده میکنه: سرور نود جی‌اس، پراکسی Envoy و یک کلاینت نمونه. بعد کافیه مرورگر خودتون رو باز کنید و به آدرس http://localhost:8081/echotest.html برید تا همه چیز رو در عمل ببینید.

نگاهی عمیق‌تر به ویژگی‌ها و گزینه‌ها

حالا که با اصول اولیه آشنا شدیم، بیایید کمی جزئی‌تر به گزینه‌هایی که در اختیار داریم نگاه کنیم.

انواع RPC و پشتیبانی در gRPC-Web

gRPC در حالت کلی چهار نوع متد یا RPC رو پشتیبانی میکنه:

  1. Unary RPCs: یک درخواست میفرستی و یک پاسخ میگیری. مثل همین مثال Echo ما.
  2. Server-side Streaming RPCs: یک درخواست میفرستی و یک جریان از پاسخ‌ها رو به صورت پشت سر هم دریافت میکنی.
  3. Client-side Streaming RPCs: یک جریان از درخواست‌ها رو میفرستی و در نهایت یک پاسخ میگیری.
  4. Bi-directional Streaming RPCs: یک جریان دوطرفه از درخواست‌ها و پاسخ‌ها به صورت همزمان داری.

gRPC-Web در حال حاضر فقط از دو نوع اول پشتیبانی میکنه. یعنی Unary RPC و Server-side Streaming RPC. استریمینگ سمت کلاینت و دوطرفه فعلا پشتیبانی نمیشن. البته برای استریمینگ سمت سرور هم یک شرط وجود داره: باید حتما از حالت grpcwebtext استفاده کنید.

حالت‌های مختلف انتقال داده (mode)

وقتی داشتیم کد کلاینت رو تولید میکردیم، یک پارامتر به اسم mode دیدیم. این پارامتر دو تا مقدار میتونه داشته باشه:

  • mode=grpcwebtext: این حالت پیش‌فرض هست.
    • Content-type درخواست میشه application/grpc-web-text.
    • داده‌ها یا همون payload به صورت base64 کدگذاری میشن. این یعنی داده‌های باینری به فرمت متنی تبدیل میشن.
    • هم از Unary و هم از Server-side streaming پشتیبانی میکنه.
  • mode=grpcweb: این حالت از فرمت باینری پروتوباف استفاده میکنه.
    • Content-type درخواست میشه application/grpc-web+proto.
    • داده‌ها به صورت باینری و خام پروتوباف فرستاده میشن. این حالت معمولا بهینه‌تر و کم‌حجم‌تره.
    • فقط از Unary call پشتیبانی میکنه. استریمینگ سمت سرور در این حالت کار نمیکنه.

استایل‌های مختلف ایمپورت (import_style)

یک پارامتر دیگه هم به اسم import_style داشتیم. این پارامتر نحوه وارد کردن ماژول‌ها در کد تولید شده رو مشخص میکنه:

  • import_style=closure: این حالت پیش‌فرض هست و از سیستم ماژول goog.require() کتابخانه Closure گوگل استفاده میکنه.
  • import_style=commonjs: این حالت از require() ی CommonJS استفاده میکنه که برای کار با ابزارهایی مثل Webpack و Browserify خیلی مناسبه.
  • import_style=commonjs+dts: (آزمایشی) این حالت علاوه بر کد CommonJS، یک فایل تایپینگ .d.ts هم برای پیام‌های پروتوباف و کلاینت سرویس تولید میکنه. این برای کار با TypeScript خیلی مفیده.
  • import_style=typescript: (آزمایشی) این حالت مستقیما کد کلاینت سرویس رو به زبان TypeScript تولید میکنه.

پشتیبانی از TypeScript

خبر خوب برای طرفدارهای TypeScript اینه که gRPC-Web به صورت آزمایشی از این زبان پشتیبانی میکنه. حالا ماژول grpc-web رو میشه به عنوان یک ماژول TypeScript وارد کرد.

برای استفاده از این قابلیت، باید موقع تولید کد از پلاگین protoc-gen-grpc-web، یکی از این دو گزینه رو بهش بدید:

  • import_style=commonjs+dts: کد CommonJS به همراه فایل‌های تایپینگ .d.ts تولید میکنه.
  • import_style=typescript: کد کاملا TypeScript تولید میکنه.

یک نکته خیلی مهم: شما نباید import_style=typescript رو برای فلگ --js_out استفاده کنید. این فلگ فقط باید روی import_style=commonjs تنظیم بشه. در واقع --js_out کد جاوااسکریپت پیام‌ها (echo_pb.js) رو تولید میکنه و --grpc-web_out فایل‌های TypeScript مربوط به سرویس (EchoServiceClientPb.ts) و تایپینگ پیام‌ها (echo_pb.d.ts) رو میسازه. این یک راه حل موقتیه تا زمانی که خود --js_out به صورت کامل از TypeScript پشتیبانی کنه.

پس دستور کامل برای تولید کد TypeScript با فرمت باینری این شکلی میشه:

protoc -I=$DIR echo.proto \
--js_out=import_style=commonjs,binary:$OUT_DIR \
--grpc-web_out=import_style=typescript,mode=grpcweb:$OUT_DIR

و کد سمت کلاینت در TypeScript میتونه این شکلی باشه:

import * as grpcWeb from 'grpc-web';
import {EchoServiceClient} from './EchoServiceClientPb';
import {EchoRequest, EchoResponse} from './echo_pb';

const echoService = new EchoServiceClient('http://localhost:8080', null, null);

const request = new EchoRequest();
request.setMessage('Hello World!');

const call = echoService.echo(request, {'custom-header-1': 'value1'},
  (err: grpcWeb.RpcError, response: EchoResponse) => {
    if (err) {
      console.log(`Received error: ${err.code}, ${err.message}`);
    } else {
      console.log(response.getMessage());
    }
  });

call.on('status', (status: grpcWeb.Status) => {
  // ...
});

همونطور که میبینید، همه چیز تایپ-سیف و مشخصه.

کلاینت مبتنی بر Promise

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

// Create a Promise client instead
const echoService = new EchoServicePromiseClient('http://localhost:8080', null, null);

// ... (request is the same as above)

echoService.echo(request, {'custom--header-1': 'value1'})
  .then((response: EchoResponse) => {
    console.log(`Received response: ${response.getMessage()}`);
  }).catch((err: grpcWeb.RpcError) => {
    console.log(`Received error: ${err.code}, ${err.message}`);
  });

فقط یک نکته مهم رو در نظر داشته باشید: وقتی از Promise استفاده میکنید، دیگه نمیتونید به callback‌های .on(...) (مثلا برای metadata و status) دسترسی داشته باشید.

تاریخچه و وضعیت فعلی gRPC در مرورگر

جالبه بدونید که داستان gRPC در مرورگر چطور شروع شد. در تابستان ۲۰۱۶، یک تیم در گوگل و یک شرکت به نام Improbable به صورت مستقل از هم شروع به کار روی چیزی کردن که میشد اسمش رو گذاشت «gRPC برای مرورگر». خیلی زود از وجود هم باخبر شدن و با هم همکاری کردن تا یک مشخصات فنی یا «spec» برای این پروتکل جدید تعریف کنن.

مشخصات فنی gRPC-Web

همونطور که گفتیم، پیاده‌سازی کامل مشخصات gRPC مبتنی بر HTTP/2 در مرورگرها غیرممکنه. به خاطر همین، مشخصات gRPC-Web با در نظر گرفتن این محدودیت‌ها و با تعریف تفاوت‌هاش با gRPC اصلی نوشته شد. این تفاوت‌های کلیدی عبارتند از:

  • پشتیبانی از هر دو پروتکل HTTP/1.1 و HTTP/2.
  • ارسال هدرهای تریلر gRPC در انتهای بدنه درخواست/پاسخ.
  • الزامی بودن استفاده از یک پراکسی برای ترجمه بین درخواست‌های gRPC-Web و پاسخ‌های gRPC HTTP/2.

دو پیاده‌سازی اصلی

تیم‌های گوگل و Improbable هر کدوم پیاده‌سازی خودشون رو از این مشخصات ارائه دادن. این دو پیاده‌سازی در دو مخزن جداگانه قرار داشتن و برای مدتی با هم سازگار نبودن.

  • کلاینت Improbable:
    • با زبان TypeScript نوشته شده.
    • در npm با نام @improbable-eng/grpc-web موجوده.
    • یک پراکسی به زبان Go هم داره که هم به صورت پکیج و هم به صورت یک برنامه مستقل قابل استفاده است.
    • از هر دو Fetch و XHR برای ارسال درخواست‌ها استفاده میکنه و به صورت خودکار بر اساس قابلیت‌های مرورگر یکی رو انتخاب میکنه.
  • کلاینت Google:
    • با زبان جاوااسکریپت و با استفاده از کتابخانه Google Closure نوشته شده.
    • در npm با نام grpc-web موجوده.
    • در ابتدا با یک افزونه برای NGINX به عنوان پراکسی عرضه شد، ولی بعدا روی فیلتر HTTP پراکسی Envoy تمرکز کرد.
    • فقط از XHR برای ارسال درخواست‌ها استفاده میکنه.

بیایید این دو رو در یک جدول با هم مقایسه کنیم:

کلاینت / ویژگیروش انتقالUnaryاستریم سمت سروراستریم سمت کلاینت و دوطرفه
ImprobableFetch/XHR✔️✔️❌ (با یک روش آزمایشی وب‌سوکت)
Google (grpcwebtext)XHR✔️✔️
Google (grpcweb)XHR✔️❌ (فقط در انتها نتیجه رو برمیگردونه)

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

آینده و مسیر پیش رو

در اکتبر ۲۰۱۸، پیاده‌سازی گوگل به نسخه ۱.۰ رسید و به صورت عمومی عرضه شد. تیم گوگل یک نقشه راه برای اهداف آینده منتشر کرده که شامل این موارد میشه:

  • یک فرمت کدگذاری پیام‌ها شبیه JSON ولی بهینه‌تر.
  • پراکسی‌های درون-فرایندی برای Node، پایتون، جاوا و غیره (تا نیاز به پراکسی جدا مثل Envoy کمتر بشه).
  • ادغام با فریمورک‌های محبوب مثل React، Angular و Vue.
  • استفاده از Fetch API برای استریمینگ بهینه‌تر از نظر حافظه.
  • پشتیبانی از استریمینگ دوطرفه.

بعد از مذاکرات بین دو تیم، تصمیم بر این شد که کلاینت گوگل و پراکسی Envoy به عنوان راه حل‌های پیشنهادی برای کاربران جدید معرفی بشن. کلاینت و پراکسی Improbable به عنوان پیاده‌سازی‌های جایگزین و آزمایشی باقی میمونن.

پس اگه امروز میخواید با gRPC-Web شروع کنید، بهترین گزینه کلاینت گوگل هست. این کلاینت تضمین‌های سازگاری API قوی‌ای داره و توسط یک تیم اختصاصی پشتیبانی میشه.

مزایای استفاده از gRPC-Web

حالا که با جزئیات فنی آشنا شدیم، بیایید یک بار دیگه مزایای معماری gRPC-Web رو مرور کنیم.

  • gRPC سرتاسری (End-to-end gRPC): به شما اجازه میده کل خط لوله RPC خودتون رو با استفاده از پروتکل بافرها طراحی کنید. دیگه لازم نیست یک لایه اضافه برای تبدیل درخواست‌های HTTP/JSON به gRPC در بک‌اند داشته باشید.
  • هماهنگی بیشتر بین تیم‌های فرانت‌اند و بک‌اند: وقتی کل ارتباط با پروتکل بافر تعریف میشه، دیگه مرز بین تیم «کلاینت» و تیم‌های «میکروسرویس‌ها» کمرنگ‌تر میشه. ارتباط کلاینت-بک‌اند فقط یک لایه gRPC دیگه در کنار بقیه لایه‌هاست.
  • تولید آسان کتابخانه‌های کلاینت: وقتی سروری که با دنیای بیرون در ارتباطه، یک سرور gRPC باشه، تولید کتابخانه‌های کلاینت برای زبان‌های مختلف (روبی، پایتون، جاوا و …) خیلی راحت‌تر میشه و دیگه نیازی به نوشتن کلاینت‌های HTTP جداگانه برای هر کدوم نیست.

دیدگاه دنیای واقعی: gRPC-Web در مقابل REST

جالبه بدونید که همیشه همه با یک تکنولوژی موافق نیستن. در یک بحث در وبسایت ردیت، یک مهندس ارشد پیشنهاد کرده بود که تیمشون از gRPC-Web به سمت استفاده از REST Handler‌های معمولی حرکت کنه. دلیل این پیشنهاد این بود که در یک استارتاپ که سرعت توسعه و رسیدن به بازار اولویت داره، این کار میتونه مفید باشه.

نگرانی‌هایی که در مورد این تغییر مطرح شده بود اینها بودن:

  • از دست دادن یک تعریف نوع (type definition) یکپارچه که پروتوباف فراهم میکنه.
  • نیاز به ابداع مجدد چرخ و نوشتن کدهای تکراری.
  • نیاز به ارتباط و مستندسازی بیشتر بین تیم‌ها.

در مقابل، تنها مزیت مسیر HTTP/REST این عنوان شده بود که درک و فهم اون برای بعضی‌ها ساده‌تره. این بحث نشون میده که انتخاب بین این تکنولوژی‌ها همیشه یک سری بده‌بستان (trade-off) داره و به شرایط پروژه بستگی داره. هر دو روش، چه استفاده از پروتوباف برای تولید کلاینت gRPC-Web و چه استفاده از OpenAPI برای تولید کلاینت REST، در سطح بالا یک هدف مشترک دارن: تولید خودکار کلاینت از روی یک تعریف مشخص.

پرسش و پاسخ

خب بچه‌ها، بحث ما تقریبا تموم شد. برای اینکه مطمئن بشیم همه چیز خوب جا افتاده، بیایید چند تا سوال مهم رو با هم مرور کنیم.

  1. سوال ۱: gRPC دقیقا چیه؟

جواب: gRPC یک چارچوب RPC (Remote Procedure Call) مدرن، متن‌باز و با کارایی بالاست که توسط گوگل ساخته شده. به زبان ساده، یک روش خیلی سریع و بهینه برای اینه که سرویس‌های مختلف در یک سیستم کامپیوتری بتونن با هم صحبت کنن و از هم سرویس بگیرن، حتی اگه در کامپیوترهای مختلف و با زبان‌های برنامه‌نویسی متفاوتی نوشته شده باشن. این چارچوب از پروتکل HTTP/2 برای انتقال داده استفاده میکنه.

  1. سوال ۲: چرا نمیتونیم مستقیم از gRPC توی مرورگر استفاده کنیم و به gRPC-Web نیاز داریم؟

جواب: چون gRPC روی ویژگی‌های سطح پایین پروتکل HTTP/2 ساخته شده. مرورگرهای وب امروزی به برنامه‌نویس‌ها اجازه دسترسی و کنترل مستقیم روی این ویژگی‌ها رو نمیدن. برای همین، یک «پل» یا «مترجم» به اسم gRPC-Web ساخته شده که یک پروتکل کمی متفاوت و سازگار با محدودیت‌های مرورگر داره. این پروتکل بعدا توسط یک پراکسی (مثل Envoy) به gRPC استاندارد ترجمه میشه.

  1. سوال ۳: فایل .proto چیه و چه نقشی داره؟

جواب: فایل .proto که بهش فایل پروتکل بافر هم میگن، یک فایل متنیه که شما در اون ساختار داده‌ها و سرویس‌های خودتون رو تعریف میکنید. این فایل مثل یک «قرارداد» یا «نقشه» بین کلاینت و سرور عمل میکنه. شما پیام‌های درخواست، پیام‌های پاسخ و متدهای سرویس رو در این فایل تعریف میکنید. بعد با استفاده از کامپایلر پروتکل بافر (protoc)، میتونید از روی این فایل، کد مربوط به کلاینت و سرور رو به زبان‌های مختلف تولید کنید.

  1. سوال ۴: پراکسی Envoy چیه و چرا در معماری gRPC-Web مهمه؟

جواب: Envoy یک پراکسی سرویس مدرن و با کارایی بالاست. در دنیای gRPC-Web، نقش یک مترجم رو بازی میکنه. درخواست‌هایی که از مرورگر با پروتکل gRPC-Web میان رو دریافت میکنه، اونها رو به پروتکل استاندارد gRPC (که روی HTTP/2 کار میکنه) تبدیل میکنه و به سرور بک‌اند میفرسته. پاسخ سرور رو هم به همین ترتیب برعکس ترجمه میکنه و برای مرورگر میفرسته. بدون وجود یک پراکسی مثل Envoy، ارتباط مستقیم بین مرورگر و سرور gRPC ممکن نیست. البته پراکسی‌های دیگه‌ای مثل پراکسی Go، Apache APISIX و Nginx هم این قابلیت رو دارن.

  1. سوال ۵: تفاوت اصلی بین دو حالت mode=grpcwebtext و mode=grpcweb چیه؟

جواب: تفاوت اصلی در نحوه کدگذاری و انتقال داده‌هاست.

  • grpcwebtext داده‌ها رو به صورت متنی (Base64) کدگذاری میکنه و هم از درخواست‌های تکی (Unary) و هم از استریمینگ سمت سرور پشتیبانی میکنه.
  • grpcweb داده‌ها رو به صورت باینری و خام پروتوباف منتقل میکنه که بهینه‌تره، ولی فقط از درخواست‌های تکی (Unary) پشتیبانی میکنه.
  1. سوال ۶: آیا میتونم از gRPC-Web برای آپلود فایل یا چت دوطرفه استفاده کنم؟

جواب: در حال حاضر، خیر. gRPC-Web از استریمینگ سمت کلاینت و استریمینگ دوطرفه پشتیبانی نمیکنه. این قابلیت‌ها در نقشه راه پروژه قرار دارن ولی هنوز پیاده‌سازی نشدن. پس برای کارهایی مثل آپلود فایل (که نیاز به استریم سمت کلاینت داره) یا یک اپلیکیشن چت زنده (که نیاز به استریم دوطرفه داره)، gRPC-Web فعلا راه حل کاملی نیست.

منابع

  • [2] What Are gRPC & gRPC Web. Basics of Hacking Into gRPC-Web | by Amin Nasiri | Medium
  • [4] The state of gRPC in the browser | gRPC
  • [6] GitHub – grpc/grpc-web: gRPC for Web Clients
  • [8] My backfill Principal Engineer wants to move off of GRPC web and start using REST Handlers. Will this be a shit show? : r/golang
  • [10] gRPC vs. gRPC-Web: Key Differences Explained
  • [1] grpc-web – npm
  • [3] Use gRPC in browser apps | Microsoft Learn
  • [5] Go and gRPC is just so intuitive. Here’s a detailed full-stack flow with gRPC-Web, Go and React. Also, there is a medium story focused on explaining how such a setup might boost efficiency and the step-by-step implementation. : r/golang
  • [7] gRPC-Web is Generally Available | gRPC
  • [9] Basics tutorial | Web | gRPC

دیدگاه‌ها

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *