سلام دوستان، امیدوارم حالتون خوب باشه، گرچه با این شرایط اقتصادی، اجتماعی و ... سخته :)
بگذریم؛ طبق قولی که داده بودم، میخوایم راجع به API و ترکیبش با gRPC صحبت کنیم و یکم کد بزنیم :)
اول از همه اگر با مفهوم API آشنا نیستید، پیشنهاد میکنم مطلب قبلی خودم که اینجاست رو بخونید یا حداقل گوگل کنید.
خب RPC یا Remote Procedure Call یه پروتکل ارتباطی نرم افزار هست، اجازه میده یک برنامه، به صورت ریموت و اصطلاحا از راه دور، یک سرویسی رو که برای یک برنامه دیگه س، ران و اجرا کنه، بدون اینکه از جزئیات شبکه و کارکردش و ... چیزی بدونه یا اصلا نیاز باشه که بدونه.
الان شاید حس کنید پیچیده شد، ولی ساده میشه :)
اینطوری فکر کنید که یه Process رو که اصلا رو سیستم فیزیکی ما نیست، Run میکنیم از راه دور، انگار که لوکال هاست خودمونه :))
به Procedure Call، چیز های دیگه هم میگن، مثل Subroutine Call یا Function Call.
این پروتکل (RPC) از مدل Clinet-Server استفاده میکنه.
برنامه ای که درخواست (Request) رو میده، Client هست، و برنامه ای که Service-Providing میکنه یعنی سرویس ارائه میده، سرور هستش.
آر پی سی کلا بصورت دیفالت، بصورت Synchronous عمل میکنه، یعنی صبر میکنه تمام Response ها بیاد، بعدش یکجا جواب رو بده. گرچه میتونیم از Thread ها استفاده کنیم برای Asynchronous کردنش.
همون RPC هستش فقط گوگل اینو نوشته :)
جی آر پی سی language independent هست؛ یعنی میتونیم از چند زبان استفاده کنیم و مشکلی پیش نمیاد
فرضا ما این بخش رو با پایتون مینویسیم که جلوتر میبینید، بعدا کسی میتونه از همین میکرو سرویس، تو پروژه لاراولی (laravel) خودش استفاده کنه.
قضیه اینطوریه که ما یک stub داریم، که کلا client میتونه از طریق اون، درخواست های خودش رو به server منتقل کنه. این درخواست ها میتونه شامل: درخواست اتصال، ارسال و دریافت steaming و Call کردن یه فانکشن و کنسل کردنش و ... باشه.
وقتی یک RPC اتفاق میوفته، environment ای که درخواست اولیه رو داده، suspend میشه (به حالت تعلیق در میاد)، پارامتر هایی که خواسته از طریق network به environment ای میره که باید درخواست اونجا اجرا و پردازش بشه و در نهایت response برمیگرده به محیطی که درخواست اولیه رو داده بود. انگار که اصلا یه فانکشن رو خودتون تو برنامه خودتون run کردید، درحالیکه الان میدونیم اینطور نبوده!
۱- کاربر (Client) از رابط کاربری ای که براش تعبیه شده استفاده میکنه که stub هست، ران میکنه و خب همه چیز لوکال داره اتفاق میوفته تا اینجا.
۲- کلاینت استاب (Client-Stub)، درخواست رو با پارامتر هایی که بهش دادیم، Pack میکنه در قالب یک مسیجی که به این عمل میگن Marshalling.
۳-حالا اطلاعات از سیستم Client به سرور ارسال میشه، جایی که باید پردازش انجام بشه و جوابی برگردونده بشه.
۴- سیستم ای که نقش سرور رو داره، مسیج رو میده به Server-Stub خودش.
۵- سرور استاب، اینجا مسیجی رو unpack میکنه که اصطلاحا بهش میگن unmarshalling
۶- وقتی کار سرور تموم شد، باز مسیج رو برمیگردونه به استاب خودش (Server-Stub) که بازم عمل Marshalling انجام میشه و اطلاعات رو از Server-Stub میده به Transport-Layer.
۷- نهایتا میرسه به Client-Stub که اونجا باز هم UnMarshalling اتفاق میوفته و پاسخ میرسه به دست کلاینی که درخواست اولیه رو داده بود.
همونطور که میدونید، اطلاعات تو بستر وب و اینترنت به طرق مختلفی باید Serialize بشه و بعد فرستاده بشه.
یه نمونه مرسوم هم JSON هست (Java Script Object Notation).
ولی gRPC یه فرمت خاصی داره که ترجیح بر اینه که از همون استفاده بشه، چرا که سرعت چندین برابر از JSON و خب از بقیه فرمت ها سازگار تر هستش.
اینجا میاد و چیزی به اسم Protobuf یا کامل ش Protocol Buffer رو معرفی میکنه.
کاری که تو این فایل میکنیم به این صورت هست که:
ساختار داده یا Data Structure ای رو مشخص میکنیم که قراره اطلاعات داشته باشند.
مثلا من میخوام یک (API-KEY) از کلاینت بگیرم و در جوابش، ۲۵۰ فیلم برتر IMDB رو بهش برگردونم.
اینجا که میرسیم، باید بریم و از وبسایتش چیزی که داریم روش کار میکنیم، اطلاعات جمع کنیم.
مثل اینکه وقتی میخوایم پست بذاریم یا لایک کنیم، چه Endpoint ای با چه پارامتر هایی Call میشه.
چه ریسپانسی برمیگرده؛ معمولا به صورت JSON برمیگرده، ولی باید ساختار JSON اش رو بشناسیم، و ببینیم چه اطلاعاتی میخوایم برگردونیم به مخاطب.
بیاید این مثال رو ببینیم:
الان میخوایم ۲۵۰ فیلم برتر IMDB رو بگیریم از سایت رسمی خودش.
قبل از هرچیزی، برید و API-KEY خودتون رو از سایت رسمی IMDB بگیرید.
ریسپانسی که برمیگرده به این شکل هست:
{"items":[{"id":"tt0111161","rank":"1","title":"The Shawshank Redemption","fullTitle":"The Shawshank Redemption (1994)","year":"1994","image":"https://m.media-amazon.com/images/M/MV5BMDFkYTc0MGEtZmNhMC00ZDIzLWFmNTEtODM1ZmRlYWMwMWFmXkEyXkFqcGdeQXVyMTMxODk2OTU@._V1_UX128_CR0,3,128,176_AL_.jpg","crew":"Frank Darabont (dir.), Tim Robbins, Morgan Freeman","imDbRating":"9.2","imDbRatingCount":"2604033"} .....
خب الان قابل حدس هستش که برای ریسپانسی که به مخاطب باید بدیم، چه فیلد هایی باید تعریف کنیم تو فایل Proto.
1- id --> string
2- rank --> string
3- title --> string
4- fullTitle --> string
5- year --> string
6- image --> string
7- crew --> string
8- imDbRating --> string
9- imDbRatingCount --> string
خب حالا نیاز داریم به کامپایلر بگیم که برای زبانی که استفاده میکنیم، یکسری کد generate کنه تا بتونیم ازش استفاده کنیم برای برنامه ای که میخوایم بنویسیم.
ما از زبان Python استفاده میکنیم.
اول ترجیحا یک Virtual environment بسازید، به این شکل:
python3 -m venv .venv
حالا اکتیوش کنید:
source .venv/bin/activate
یک فایل Proto بسازید:
touch movie_service.proto
حالا تو فایل پروتو اینارو بنویسید:
syntax = "proto3"
service TopMovies{
rpc GetTop250Movies(MovieRequest) returns (MovieArray) {}
}
message MovieRequest{
string api_key = 1;
}
message Movie{
string id = 1;
string rank = 2;
sting title = 3;
sting fullTtitle = 4;
string year = 5;
string image = 6;
string crew = 7;
string imDbRating = 8;
string imDbRatingCount = 9;
}
message MovieArray{
repeated Movie movies = 1;
}
یک فایل به اسم main.py و یک فایل هم به اسم api.py بسازید.
کل فایل ها به این شکل باید باشه الان:
ProgramFolder
├── api.py
├── main.py
├── movie_service.proto
کامندی که باید بزنیم تا کد مورد نیاز generate بشه، به این صورت هست:
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. movie_service.proto
خب الان اگه دقت کنید، دوتا فایل جدید پایتونی اصافه شده:
ProgramFolder
├── api.py
├── main.py
├── movie_service.proto
├── movie_service_pb2_grpc.py
├── movie_service_pb2.py
تو فایل main.py این کد هارو بنویسید:
main.py
import grpc
from api import get_top_250_movies
import movie_service_pb2
import movie_service_pb2_grpc
from concurrent import futures
class MovieService(movie_service_pb2_grpc.TopMoviesServicer):
def GetTop250Movies(self, request, context):
api_key = request.api_key
movies = get_top_250_movies(api_key)
return movie_service_pb2.MovieArray(movies=movies)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
movie_service_pb2_grpc.add_TopMoviesServicer_to_server( MovieService(), server)
server.add_insecure_port("127.0.0.1:50051")
server.start()
server.wait_for_termination()
def main():
if __name__ == "__main__":
print("server is up and running on port 50051:\n")
serve()
main()
و تو فایل api.py این کد هارو بنویسید:
api.py
import requests
URL = "https://imdb-api.com/en/API/Top250Movies/"
def _get(url, api_key):
response = requests.get(f'{url}{api_key}')
response.raise_for_status()
return response.json()
def get_top_250_movies(api_key):
for movie in _get(URL, api_key)['items']:
movie_info = {
'id' : movie.get('id'),
'rank': movie.get('rank'),
'title': movie.get('title'),
'fullTitle': movie.get('fullTitle'),
'year': movie.get('year'),
'image': movie.get('image'),
'crew': movie.get('crew'),
'imDbRating': movie.get('imDbRating'),
'imDbRatingCount': movie.get('imDbRatingCount'),
}
data.append(movie_info)
return data
خب حالا یه توضیحی بدم راجع به این کدهایی که داریم:
اول اینکه مستقیما کاری با دوتا فایلی که خود compiler برامون generate کرده، نداریم.
تو فایل api.py:
صرفا داریم با ماژول Requests پایتون، یه درخواست با متد GET به سرور IMDB میفرستیم و این درخواست در اصل دقیقا به ENDPOINT ای میره که وظیفه اش، ارسال پاسخی هست که شامل ۲۵۰ فیلم برتر IMDB میشه.
بعد یه فانکشن داریم به اسم get_top_250_movies:
که خب یه لوپ زدیم روی request ای که میفرستیم و فیلدهایی که میخوایم رو با key-value تو یک دیکشنری در هر iterate ذخیره میکنیم و آخر هر لوپ، به یک لیست append میکنیم :)
تو فایل main.py یک کلاس داریم به اسم MovieService که از کلاسی به اسم MovieServicer که همین کامپایلر برامون generate کرده، ارث بری میکنه.
اگه یادتون باشه، تو فایل proto یه سرویس تعریف کردیم به اسم TopMovies
که توش یک rpc داریم، که میاد میگه:
متد GetTop250Movies با این پارامتر ران بشه و فلان چیز رو return کنه:
service TopMovies{
rpc GetTop250Movies(MovieRequest) returns (MovieArray) {}
}
اینجا MovieRequest و MovieArray دوتا مسیجی هستن که تو فایل proto تعریف کرده بودیم
و GetTop250Movies یک متدی هست که به سرویس MovieService اضافه کردیم تو فایل main.py
فانکشن serve هم دقیقا حکم سرور رو داره که کلاینت به چه IP ای و رو چه PORT ای بتونه درخواست بده و جواب رو بگیره.
که برای ما رو لوکال هاست پورت ۵۰۰۵۱ هستش.
127.0.0.1:50051
خب حالا نیاز داریم که بتونیم درخواست رو بفرستیم به عنوان کلاینت، به سرور خودمون.
بهتره برنامه bloomRPC رو دانلود کنید و از اون استفاده کنید.
همینطور که میبینید، اول باید IP:port رو مشخص کنید.
بعدش علامت + سبز رنگ بالای صفحه سمت چپ رو بزنید و فایل --> movie_service.proto رو پیدا و انتخاب کنید.
در فیلد api_key: ای پی آی رو که از سایت IMDB گرفتین رو بذارید.
حالا میتونید bloomRPC رو run کنید.
سورس کد این بخش هم میتونید تو GitHub من پیدا کنید :)
امیدوارم مفید واقع بشه و اگر مشکلی، ایرادی تو کار و توضیحات دیدید، ممنون میشم باهام در میون بذارید.