بدء استخدام gRPC-Go

1. مقدمة

في هذا الدرس العملي، ستستخدم gRPC-Go لإنشاء عميل وخادم يشكّلان أساس تطبيق لربط المسارات مكتوب بلغة Go.

في نهاية هذا البرنامج التعليمي، سيكون لديك تطبيق عميل يتصل بخادم بعيد باستخدام gRPC للحصول على اسم أو عنوان بريدي للموقع الجغرافي الذي يقع عند إحداثيات معيّنة على الخريطة. قد يستخدم تطبيق متكامل تصميم العميل والخادم هذا لتعداد نقاط الاهتمام أو تلخيصها على طول مسار معيّن.

يتم تحديد الخدمة في ملف Protocol Buffers، وسيتم استخدام هذا الملف لإنشاء رمز نموذجي للبرنامج العميل والخادم حتى يتمكّنا من التواصل مع بعضهما البعض، ما يوفّر عليك الوقت والجهد في تنفيذ هذه الوظيفة.

لا يهتم هذا الرمز الذي تم إنشاؤه بتعقيدات الاتصال بين الخادم والعميل فحسب، بل أيضًا بتسلسل البيانات وإلغاء تسلسلها.

أهداف الدورة التعليمية

  • كيفية استخدام مخزن البروتوكولات المؤقت لتحديد واجهة برمجة تطبيقات الخدمة
  • كيفية إنشاء برنامج عميل وخادم يستندان إلى gRPC من تعريف Protocol Buffers باستخدام إنشاء الرموز البرمجية المبرمَج
  • فهم عملية التواصل بين العميل والخادم باستخدام gRPC

هذا الدرس التطبيقي حول الترميز موجّه لمطوّري Go الجدد على gRPC أو الذين يريدون مراجعة gRPC، أو أي شخص آخر مهتم بإنشاء أنظمة موزّعة. لا يُشترط توفّر خبرة سابقة في gRPC.

2. قبل البدء

المتطلبات الأساسية

تأكَّد من تثبيت ما يلي:

  • الإصدار 1.24.5 أو الإصدارات الأحدث من سلسلة أدوات Go للحصول على تعليمات التثبيت، يُرجى الاطّلاع على دليل البدء في Go.
  • برنامج تجميع البيانات المنظّمة protoc، الإصدار 3.27.1 أو إصدار أحدث للحصول على تعليمات التثبيت، يُرجى الاطّلاع على دليل التثبيت الخاص بالمترجم.
  • مكوّنات إضافية لبرنامج تحويل مخزن البروتوكولات المؤقت إلى Go وgRPC لتثبيت هذه المكوّنات الإضافية، شغِّل الأوامر التالية:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

عدِّل المتغيّر PATH ليتمكّن برنامج تجميع بروتوكول المخزن المؤقت من العثور على المكوّنات الإضافية:

export PATH="$PATH:$(go env GOPATH)/bin"

الحصول على الشفرة‏

ولكي لا تضطر إلى البدء من الصفر تمامًا، يوفّر لك هذا الدرس التطبيقي حول الترميز بنية أساسية لرمز المصدر الخاص بالتطبيق لتتمكّن من إكماله. ستوضّح لك الخطوات التالية كيفية إكمال التطبيق، بما في ذلك استخدام مكوّنات برنامج تجميع بروتوكول المخزن المؤقت لإنشاء رمز gRPC النموذجي.

نزِّل الرمز المصدري هذا كأرشيف ZIP من GitHub وفكّ ضغط محتواه.

بدلاً من ذلك، يتوفّر رمز المصدر المكتمل على GitHub إذا كنت تريد تخطّي كتابة عملية التنفيذ.

3- تحديد الخدمة

تتمثّل خطوتك الأولى في تحديد خدمة gRPC للتطبيق وطريقة RPC وأنواع رسائل الطلبات والاستجابات باستخدام بروتوكول المخازن المؤقتة. ستوفّر خدمتك ما يلي:

  • طريقة استدعاء إجراء عن بُعد (RPC) تُسمّى GetFeature ينفّذها الخادم ويستدعيها العميل.
  • نوعا الرسائل Point وFeature اللذان يمثّلان بنى البيانات المتبادلة بين العميل والخادم عند استخدام طريقة GetFeature يقدّم العميل إحداثيات الخريطة كـ Point في طلب GetFeature إلى الخادم، ويردّ الخادم بـ Feature مطابق يصف أي شيء يقع في تلك الإحداثيات.

سيتم تحديد طريقة RPC هذه وأنواع الرسائل الخاصة بها في ملف routeguide/route_guide.proto الخاص برمز المصدر المقدَّم.

يُشار إلى Protocol Buffers عادةً باسم protobufs. لمزيد من المعلومات عن مصطلحات gRPC، يُرجى الاطّلاع على المفاهيم الأساسية والبنية ودورة الحياة في gRPC.

أنواع الرسائل

في ملف routeguide/route_guide.proto الخاص بالرمز المصدر، حدِّد أولاً نوع الرسالة Point. يمثّل Point زوج إحداثيات خط العرض وخط الطول على الخريطة. في هذا الدرس العملي، استخدِم أعدادًا صحيحة للإحداثيات:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

الرقمان 1 و2 هما رقما تعريف فريدان لكل حقل من الحقول في بنية message.

بعد ذلك، حدِّد Feature نوع الرسالة. يستخدم Feature الحقل string للاسم أو العنوان البريدي الخاص بمكان محدّد من خلال Point:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

طريقة الخدمة

يحتوي ملف route_guide.proto على بنية service باسم RouteGuide تحدّد طريقة واحدة أو أكثر توفّرها خدمة التطبيق.

أضِف الطريقة rpc داخل تعريف RouteGuide.GetFeature كما هو موضّح سابقًا، ستبحث هذه الطريقة عن اسم أو عنوان موقع جغرافي من مجموعة إحداثيات معيّنة، لذا اطلب من GetFeature عرض Feature لـ Point معيّن:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

هذه طريقة أحادية لاستدعاء الإجراء عن بُعد: استدعاء إجراء بسيط عن بُعد يرسل فيه العميل طلبًا إلى الخادم وينتظر تلقّي ردّ، تمامًا مثل استدعاء دالة محلية.

4. إنشاء رمز العميل والخادم

بعد ذلك، أنشئ رمز gRPC النموذجي لكل من البرنامج الخادم والبرنامج العميل من ملف .proto باستخدام برنامج تجميع مخزن البروتوكولات المؤقت. في الدليل routeguide، نفِّذ ما يلي:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

ينشئ هذا الأمر الملفات التالية:

  • route_guide.pb.go، الذي يحتوي على دوال لإنشاء أنواع رسائل التطبيق والوصول إلى بياناتها.
  • route_guide_grpc.pb.go، الذي يحتوي على دوال يستخدمها العميل لاستدعاء طريقة gRPC البعيدة للخدمة، ودوال يستخدمها الخادم لتقديم تلك الخدمة البعيدة

بعد ذلك، سننفّذ طريقة GetFeature من جهة الخادم، حتى يتمكّن الخادم من الردّ على الطلب الذي يرسله العميل.

5- تنفيذ الخدمة

يتم تنفيذ العمل الرئيسي من خلال الدالة GetFeature على جانب الخادم: تأخذ هذه الدالة رسالة Point من العميل وتعرض في رسالة Feature معلومات الموقع الجغرافي المطابقة من قائمة بالأماكن المعروفة. في ما يلي طريقة تنفيذ الدالة في server/server.go:

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

عند استدعاء هذه الطريقة بعد تلقّي طلب من برنامج عميل بعيد، يتم تمرير عنصر Context يصف عملية استدعاء الإجراء عن بُعد، وعنصر Point مخزن بروتوكولات مؤقت من طلب العميل هذا. تعرض الدالة عنصرًا من بروتوكول Feature للموقع الجغرافي الذي تم البحث عنه وerror حسب الحاجة.

في الطريقة، املأ عنصر Feature بالمعلومات المناسبة لـ Point المحدّدة، ثم return مع خطأ nil لإعلام gRPC بأنّك انتهيت من التعامل مع طلب الإجراء عن بُعد (RPC) وأنّه يمكن إرجاع عنصر Feature إلى العميل.

تتطلّب الطريقة GetFeature إنشاء كائن routeGuideServer وتسجيله حتى يمكن توجيه الطلبات من العملاء لعمليات البحث عن الموقع الجغرافي إلى هذه الدالة. يتم ذلك في main():

func main() {
  flag.Parse()
  lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

في ما يلي الخطوات التي تحدث في main():

  1. حدِّد منفذ TCP الذي سيتم استخدامه للاستماع إلى طلبات العميل البعيد باستخدام lis, err := net.Listen(...). تستخدم التطبيقات تلقائيًا منفذ TCP رقم 50051 كما هو محدّد في المتغيّر port أو من خلال تمرير الخيار --port في سطر الأوامر عند تشغيل الخادم. إذا تعذّر فتح منفذ TCP، ينتهي التطبيق بحدوث خطأ فادح.
  2. أنشئ مثيلاً لخادم gRPC باستخدام grpc.NewServer(...)، وسمِّ هذا المثيل grpcServer.
  3. أنشئ مؤشرًا إلى routeGuideServer، وهو بنية تمثّل خدمة واجهة برمجة التطبيقات للتطبيق، وسمِّ المؤشر s.
  4. استخدِم s.loadFeatures() لملء المصفوفة s.savedFeatures بالمواقع الجغرافية التي يمكن البحث عنها من خلال GetFeature.
  5. سجِّل خدمة واجهة برمجة التطبيقات مع خادم gRPC ليتم توجيه طلبات استدعاء الإجراء عن بُعد (RPC) إلى GetFeature إلى الدالة المناسبة.
  6. اتّصِل بـ Serve() على الخادم مع تفاصيل المنفذ لإجراء انتظار حظر لطلبات العميل، ويستمر ذلك إلى أن يتم إيقاف العملية أو يتم استدعاء Stop().

تستمدّ الدالة loadFeatures() عمليات الربط بين الإحداثيات والمواقع الجغرافية من server/testdata.go.

6. إنشاء العميل

عدِّل الآن client/client.go، وهو المكان الذي ستنفّذ فيه رمز العميل.

لاستدعاء طرق الخدمة البعيدة، علينا أولاً إنشاء قناة gRPC للتواصل مع الخادم. ننشئ ذلك من خلال تمرير سلسلة URI المستهدَفة للخادم (وهي في هذه الحالة ببساطة العنوان ورقم المنفذ) إلى grpc.NewClient() في دالة main() الخاصة بالعميل على النحو التالي:

conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
        log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

عنوان الخادم، الذي يحدّده المتغيّر serverAddr، هو localhost:50051 تلقائيًا، ويمكن تجاوزه باستخدام الخيار --addr في سطر الأوامر عند تشغيل العميل.

إذا كان العميل بحاجة إلى الاتصال بخدمة تتطلّب بيانات اعتماد المصادقة، مثل بيانات اعتماد TLS أو JWT، يمكن للعميل تمرير كائن DialOptions كمعلَمة إلى grpc.NewClient يحتوي على بيانات الاعتماد المطلوبة. لا تتطلّب خدمة RouteGuide أي بيانات اعتماد.

بعد إعداد القناة في gRPC، نحتاج إلى رمز بديل للعميل من أجل تنفيذ استدعاءات الإجراء عن بُعد (RPC) من خلال استدعاءات دوال Go. نحصل على الرمز الصوري هذا باستخدام طريقة NewRouteGuideClient التي يوفّرها ملف route_guide_grpc.pb.go الذي تم إنشاؤه من ملف .proto الخاص بالتطبيق.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

طُرق خدمة المكالمات

في gRPC-Go، تعمل طلبات RPC في وضع الحظر/التزامن، ما يعني أنّ طلب RPC ينتظر استجابة الخادم، وسيعرض إما استجابة أو خطأ.

Simple RPC

إنّ استدعاء إجراء RPC بسيط GetFeature هو أمر مباشر تقريبًا مثل استدعاء إجراء محلي، وهو client.GetFeature في هذه الحالة:

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

يستدعي العميل الطريقة على الرمز التجريبي الذي تم إنشاؤه سابقًا. بالنسبة إلى مَعلمات الطريقة، ينشئ العميل عنصرًا من بروتوكول Point لطلب المخزن المؤقت ويملأه. يمكنك أيضًا تمرير عنصر context.Context يتيح لنا تغيير سلوك RPC إذا لزم الأمر، مثل تحديد حد زمني للمكالمة أو إلغاء RPC قيد التنفيذ. إذا لم تعرض المكالمة خطأً، يمكن للعميل قراءة معلومات الاستجابة من الخادم من القيمة المرجَعة الأولى:

log.Println(feature)

في المجمل، يجب أن تبدو الدالة main() الخاصة بالعميل على النحو التالي:

func main() {
        flag.Parse()

        // Set up a connection to the gRPC server.
        conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("fail to dial: %v", err)
        }
        defer conn.Close()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. جرّبه الآن

تأكَّد من أنّ الخادم والعميل يعملان معًا بشكل صحيح من خلال تنفيذ الأوامر التالية في دليل العمل الخاص بالتطبيق:

  1. شغِّل الخادم في إحدى الوحدات الطرفية:
cd server
go run .
  1. شغِّل العميل من وحدة طرفية أخرى:
cd client
go run .

ستظهر لك نتيجة مشابهة لما يلي، مع حذف الطوابع الزمنية لتوضيح الصورة:

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

8. الخطوات التالية