خب 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() رو چاپ کنیم
});
بیایید این کد رو با هم تحلیل کنیم:
- در دو خط اول، ما کلاسهایی که در مرحله قبل تولید کردیم (
EchoRequest
,EchoResponse
,EchoServiceClient
) رو با استفاده ازrequire
وارد برنامهمون میکنیم. - بعد یک نمونه جدید از
EchoServiceClient
میسازیم و آدرس پراکسی Envoy خودمون (http://localhost:8080
) رو بهش میدیم. - یک نمونه جدید از
EchoRequest
میسازیم و پیام «Hello World!» رو داخلش قرار میدیم. - در نهایت، متد
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 رو پشتیبانی میکنه:
- Unary RPCs: یک درخواست میفرستی و یک پاسخ میگیری. مثل همین مثال
Echo
ما. - Server-side Streaming RPCs: یک درخواست میفرستی و یک جریان از پاسخها رو به صورت پشت سر هم دریافت میکنی.
- Client-side Streaming RPCs: یک جریان از درخواستها رو میفرستی و در نهایت یک پاسخ میگیری.
- 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 | استریم سمت سرور | استریم سمت کلاینت و دوطرفه |
---|---|---|---|---|
Improbable | Fetch/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، در سطح بالا یک هدف مشترک دارن: تولید خودکار کلاینت از روی یک تعریف مشخص.
پرسش و پاسخ
خب بچهها، بحث ما تقریبا تموم شد. برای اینکه مطمئن بشیم همه چیز خوب جا افتاده، بیایید چند تا سوال مهم رو با هم مرور کنیم.
- سوال ۱: gRPC دقیقا چیه؟
جواب: gRPC یک چارچوب RPC (Remote Procedure Call) مدرن، متنباز و با کارایی بالاست که توسط گوگل ساخته شده. به زبان ساده، یک روش خیلی سریع و بهینه برای اینه که سرویسهای مختلف در یک سیستم کامپیوتری بتونن با هم صحبت کنن و از هم سرویس بگیرن، حتی اگه در کامپیوترهای مختلف و با زبانهای برنامهنویسی متفاوتی نوشته شده باشن. این چارچوب از پروتکل HTTP/2 برای انتقال داده استفاده میکنه.
- سوال ۲: چرا نمیتونیم مستقیم از gRPC توی مرورگر استفاده کنیم و به gRPC-Web نیاز داریم؟
جواب: چون gRPC روی ویژگیهای سطح پایین پروتکل HTTP/2 ساخته شده. مرورگرهای وب امروزی به برنامهنویسها اجازه دسترسی و کنترل مستقیم روی این ویژگیها رو نمیدن. برای همین، یک «پل» یا «مترجم» به اسم gRPC-Web ساخته شده که یک پروتکل کمی متفاوت و سازگار با محدودیتهای مرورگر داره. این پروتکل بعدا توسط یک پراکسی (مثل Envoy) به gRPC استاندارد ترجمه میشه.
- سوال ۳: فایل
.proto
چیه و چه نقشی داره؟
جواب: فایل .proto
که بهش فایل پروتکل بافر هم میگن، یک فایل متنیه که شما در اون ساختار دادهها و سرویسهای خودتون رو تعریف میکنید. این فایل مثل یک «قرارداد» یا «نقشه» بین کلاینت و سرور عمل میکنه. شما پیامهای درخواست، پیامهای پاسخ و متدهای سرویس رو در این فایل تعریف میکنید. بعد با استفاده از کامپایلر پروتکل بافر (protoc
)، میتونید از روی این فایل، کد مربوط به کلاینت و سرور رو به زبانهای مختلف تولید کنید.
- سوال ۴: پراکسی Envoy چیه و چرا در معماری gRPC-Web مهمه؟
جواب: Envoy یک پراکسی سرویس مدرن و با کارایی بالاست. در دنیای gRPC-Web، نقش یک مترجم رو بازی میکنه. درخواستهایی که از مرورگر با پروتکل gRPC-Web میان رو دریافت میکنه، اونها رو به پروتکل استاندارد gRPC (که روی HTTP/2 کار میکنه) تبدیل میکنه و به سرور بکاند میفرسته. پاسخ سرور رو هم به همین ترتیب برعکس ترجمه میکنه و برای مرورگر میفرسته. بدون وجود یک پراکسی مثل Envoy، ارتباط مستقیم بین مرورگر و سرور gRPC ممکن نیست. البته پراکسیهای دیگهای مثل پراکسی Go، Apache APISIX و Nginx هم این قابلیت رو دارن.
- سوال ۵: تفاوت اصلی بین دو حالت
mode=grpcwebtext
وmode=grpcweb
چیه؟
جواب: تفاوت اصلی در نحوه کدگذاری و انتقال دادههاست.
grpcwebtext
دادهها رو به صورت متنی (Base64) کدگذاری میکنه و هم از درخواستهای تکی (Unary) و هم از استریمینگ سمت سرور پشتیبانی میکنه.grpcweb
دادهها رو به صورت باینری و خام پروتوباف منتقل میکنه که بهینهتره، ولی فقط از درخواستهای تکی (Unary) پشتیبانی میکنه.
- سوال ۶: آیا میتونم از 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
دیدگاهتان را بنویسید