gRPC in Action

g

سنتعرف من خلال هذا المقال على تقنية Grpc وكيفية استخدامها مع مثال لها.

أولا: ماهو gRPC:
دعونا أولا نقوم بتعريف RPC فقط :
RPC هو إختصار ل Remote Procedure Call وهو تقنية تَستخدم نظام client server model لنقل البيانات بين الclient و الserver وتكون عملية تزامنية “Synchronous” وتسمى Unary بحيث انك عندما ترسل request تنتظر حتى تحصل على الresponse وتكون من خلال نفس الشبكة “Shared Netword” ، كما يدعم ايضا الStreaming من الclient للServer او العكس او كلاهما معا “bidirectional” .
يستخدم RPC عادة مع الـdistributed systems لانه يسهل التواصل بين الsystems او الservices المختلفة ويجعلها تعمل وتتواصل كانها من مصدر واحد، كما انه يمكنك من كتابة الservices الخاصة بك بتقنيات ولغات مختلفة وجعلها تتواصل مع بعضها بطريقة فعالة وسريعة وبسيطة.
نأتي الآن لتعريف gRPC:
هو إطار عمل مفتوح المصدر يتمتع بكفاءة عالية مبني على تقنية RPC ويستخدم HTTP/2 تم إطلاقه من قبل شركة Google.
يستخدم gRPC الـProtocol Buffer Implementaiton ليقوم بعمل Serialization للبيانات.

تعلم بالتطبيق

مصدر الصورة grpc.io

سنتعلم كيف يعمل gRPC من خلال عمل تطبيق بسيط يتكون من جزئين بلغتين مختلفتين
سنستخدم لغة Go و C# مع .NET Core.
من الممكن ان تكون C++ او Java او Python او اي لغة اخرى لكن انا فضلت استخدام هاتين اللغتين في هذا المثال ، يمكنك التحقق من اللغات المدعومة من خلال الموقع الرسمي .

سنقوم بعمل تطبيق بسيط وسيكون عبارة عن service مكتوبة بلغة go مسؤولة عن ادارة الaccounts وسنستخدمها من خلال C# Client.

أولا: تعريف الServices والMessage Types من خلال Protobuf

كما ذكرنا سابقا gRPC يستخدم Protobuf كـdefault serialization format لتضمين البيانات بين الclient والserver ، يضمن هذا الformat سرعة وكفاءة في نقل البيانات أكثر من XML و JSON.

تعريف ملف الProtobuf:

دعنا نقوم بإنشاء ملف ولنسمه account.proto

syntax = "proto3";

package accountproto;

message CreateAccountRequest{
    string username = 1;
    string email = 2;
}

message CreateAccountResponse{
    string id = 1;
}

message GetAccountInformationRequest{
    string id = 1;
}

message GetAccountInformationResponse{
    string id = 1;
    string username = 2;
    string email = 3;
}

service AccountService{
    rpc CreateAccount(CreateAccountRequest) returns (CreateAccountResponse){}
    rpc GetAccountInformation(GetAccountInformationRequest) returns (GetAccountInformationResponse){}
}

في البداية قمنا بتعريف اننا نستخدم proto3 من الـprotocol buffers language وإذا لم نذكرها سيفترض اننا نستخدم proto2 ونحن لا نريد ذلك.
بعدها package وهي ستكون الpackage name لملف go الذي سيتكون من هذا الproto, في حالة c# حقل package سيكون هو الnamespace لمزيد من المعلومات يمكنك التأكد من الموقع الرسمي.
يتكون باقي الملف من نوعين من البيانات وهي service و message.
الـmessage هي ماسيدخل او ماسيخرج من method معينة في الservice الخاصة بك, بداخلها properties والانواع المدعومة لهذه الحقول هي string, bytes, int32,float,double ويمكنك معرفة باقي الانواع المدعومة من هنا.
الرقم بعد تعريف الproperty لابد ان يكون رقم فريد “unique” لانه سيمثل الproperty name عند عمل serialization لها وتحويلها binary.

نقوم بتعريف rpc methods المتعلقة بالـservice ونحدد ماهي الinputs والoutputs التي عرفناها سابقاً.
كما نرى لدينا method تدعى CreateAccount تاخذ CreateAccountRequest كمدخل وترجع CreateAccountResponse.
الimplementaion الخاص بهذا بهذه الmethod سنعرفه في المرحلة التالية بعد توليد go file الذي يحتوي على الinterface الخاص بالaccount.proto الذي قمنا بتعريفه.
ناتي الان لخطوات عمل compilation لهذا الملف
أولا: قم بتنزيل الcompliler من هنا.
ثانيا: قم بالذهاب الى المجلد وتنفيذ هذا الامر من خلال الTerminal :

protoc -I ../account-proto --go_out=plugins=grpc:./proto-go ../account-proto/account.proto

بعد تنفيذ هذا الأمر وإذا كان كل شئ على مايرام “Hopefully” سنجد انه تم توليد ملف جديد باسم account.pb.go يحتوي على هذا الملف على الاوامر اللازمة لتسجيل وتشغيل الservice الخاصة بنا كما يحتوي أيضاً على structs تم توليدها تلقائيا للmessages التي قمنا بتعريفها في الملف و أيضاً interface للservice الخاصة بنا لكي نتمكن من عمل implementation للmethods التي عرفناها.

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"errors"

	pb "github.com/Ahmad-Magdy/grpc-by-example/proto-go"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	"github.com/satori/go.uuid"
)

type Account struct {
	ID       string
	Username string
	Email    string
}

var (
	accounts []*Account
)

type server struct{}

func (s *server) CreateAccount(ctx context.Context, accountRequest *pb.CreateAccountRequest) (*pb.CreateAccountResponse, error) {
	fmt.Printf("Server: Recived, %s\n", accountRequest.GetUsername())

	newAccount := &Account{
		ID: uuid.NewV4().String()  ,
		Username: accountRequest.GetUsername(),
		Email:    accountRequest.GetEmail(),
	}

	accounts = append(accounts, newAccount)
	return &pb.CreateAccountResponse{Id: newAccount.ID}, nil
}

func (s *server) GetAccountInformation(ctx context.Context, m *pb.GetAccountInformationRequest) (*pb.GetAccountInformationResponse,error){
	 for _,accountItem:= range accounts{
		 if accountItem.ID == m.GetId(){
			 return &pb.GetAccountInformationResponse{
											Id:accountItem.ID, 
											Username : accountItem.Username, 
											Email:accountItem.Email, }, nil
		 }

	 }
	 return nil, errors.New("account not found")
}

func main() {
	lis, err := net.Listen("tcp", ":3000")
	if err != nil {
		log.Fatal("Failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterAccountServiceServer(s, &server{})
	reflection.Register(s)
	if err := s.Serve(lis); err != nil {
		log.Fatal("Failed to serve: %v", err)
	}
}

دعوني اقوم بتوضيح ماقمت بكتابته في هذا الملف خطوة بخطوة:

  • قمت باستيراد pb “github.com/Ahmad-Magdy/grpc-by-example/proto-go” والذي يمثل الـfolder الذي يحتوي على ملف المتولد من الproto file.
  • قمت بتعريف struct يمثل الserver والذي سيحتوي على الimplementation للmethods الذي قمنا بتعريفها في الproto file.
  • كما ذكرنا يوجد لدينا 2 methods واحدة تدعى CreateAccount والاخرى GetAccountInformation.
  • قمت بعمل implementation بسيط يوضح كيف تعمل هذه الmethods وهو ليس efficient ولكن يكفي لتوضيح وتبسيط الفكرة.
  • في func main قمت بإنشاء server جديد للgrpc وتسجيل الservice الخاصة بي, الكود الخاص بتسجيل الservice وهو بداخل “RegisterAccountServiceServer” تم توليده تلقائيا عند عمل compile لملف الproto الذي قمنا بتعريفه سابقاً.

كفى حديثا ودعونا نقوم بتشغيل البرنامج لنرى النتيجة:

لا مشاكل وكل شئ على مايرام الى حد الان ?? دعونا ننتقل الى جزء الC# ,

dotnet new sln --name xyz
dotnet new classlib --name AccountServiceBase
dotnet sln account-service-client.sln add AccountServiceBase/AccountServiceBase.csproj
cd AccountServiceBase
dotnet add package Grpc
dotnet add package Grpc.Tools
dotnet add package Google.Protobuf

نقوم بعمل solution جديد وعمل project كbase لنا يحتوي على الproto generated files والclient في project منفصل يستخدمه.

يلزم تضمين <Protobuf> ك item group ليقوم بعمل compile للproto files في كل مرة نقوم بها بعمل build للproject.
في النهاية ينبغي ان يكون لدينا csproj يشبه هذا الشكل.

<Project Sdk=”Microsoft.NET.Sdk”>

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include=”Google.Protobuf” Version=”3.6.1″ />
<PackageReference Include=”Grpc” Version=”1.18.0″ />
<PackageReference Include=”Grpc.Tools” Version=”1.18.0″>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<Protobuf Include=”../../**/*.proto” OutputDir=”%(RelativePath)” CompileOutputs=”false” />
</ItemGroup>

</Project>

إذا قمت بتجربة dotnet build فستجد انه تم توليد ملفين وهما Account و AccountGrpc الذين سيتم استخدامهما لاحقا.
الأن نقوم بانشاء الclient لهذه الservice في نفس المجلد الخاص بالsolution نقوم بتنفيذ الاتي:

dotnet new console --name AccountServiceClient
dotnet sln account-service-client.sln add AccountServiceClient/AccountServiceClient.csproj
dotnet add AccountServiceClient/AccountServiceClient.csproj reference AccountServiceBase/AccountServiceBase.csproj

static void Main(string[] args)
{
    var channel = new Channel("localhost:3000", ChannelCredentials.Insecure);

    var client = new AccountService.AccountServiceClient(channel);

    var reply = client.CreateAccount(new CreateAccountRequest() {Username = "Adam",Email = "a@amagdy.me"});
   
    Console.WriteLine($"Reply from the server with message {reply.Id}");

    channel.ShutdownAsync().Wait();
}
  • في البداية قمنا بتعريف channel لتتصل على نفس الserver الذي قمنا بتعريفه في go بنفس الport والذي كان 3000 `, واخترنا ان تكون الCredentials Insecure وبذلك لن تكون مع SSL ويمكنك قراءة المزيد من التفاصيل من هنا.
  • بعد ذلك قمنا بتعريف client للservice الخاصة بنا وتنفيذ CreateAccount وانتظار الرد من السيرفر من الجانب الاخر.

الخلاصة:

في هذه المقالة أخذنا نبذة سريعة عن الgRPC وكيف تبدأ باستخدامه في مشروعك, تعرفنا ايضا انه يدعم الكثير من اللغات مثل C#, Go, PHP , Python, Ruby, Node.js and much more.
حاولنا في هذا المقال تطبيق مثال بسيط لservice وطريقة استدعائها في لغة اخرى واستخدامها كان من الممكن ايضا انت تكون مجموعة من الservices تتواصل فيما بينها ولكن راعيت ان يكون المقال قصيرا فيسهل قراءته ويمكنك تطبيق الباقي بنفس الطريقة.
سأقوم لاحقاً بمشاركة هذا المثال على Github.

مصادر مفيدة:
https://github.com/vladimirvivien/go-grpc
https://github.com/grpc/grpc-go
https://github.com/grpc/grpc/blob/master/src/csharp/BUILD-INTEGRATION.md
https://github.com/grpc/grpc/tree/master/examples/csharp/Helloworld
https://grpc.io/docs/tutorials/basic/csharp.html

عن الكاتب

أحمد مجدي

Full Stack Software Developer , Geek

أضف تعليقاً

هذا الموقع يستخدم Akismet للحدّ من التعليقات المزعجة والغير مرغوبة. تعرّف على كيفية معالجة بيانات تعليقك.