1. مقدمة
في هذا الدرس التطبيقي حول الترميز، ستستخدم gRPC-Go لإنشاء عميل وخادم يشكّلان الأساس لتطبيق يحدّد المسارات مكتوب بلغة Go.
في نهاية هذا الدليل التعليمي، سيكون لديك عميل يتصل بخادم بعيد باستخدام gRPC للحصول على معلومات حول الميزات على مسار العميل، وإنشاء ملخّص لمسار العميل، وتبادل معلومات المسار، مثل آخر الأخبار عن حركة المرور، مع الخادم والعملاء الآخرين.
يتم تحديد الخدمة في ملف بتنسيق Protocol Buffers، وسيتم استخدام هذا الملف لإنشاء رمز نص نموذجي للعميل والخادم حتى يتمكّنا من التواصل مع بعضهما البعض، ما يوفّر عليك الوقت والجهد في تنفيذ هذه الوظيفة.
لا يهتم هذا الرمز الذي تم إنشاؤه بتعقيدات الاتصال بين الخادم والعميل فحسب، بل أيضًا بتسلسل البيانات وإلغاء تسلسلها.
ماذا ستتعلّم؟
- كيفية استخدام "مخازن البروتوكولات المؤقتة" (Protocol Buffers) لتحديد واجهة برمجة تطبيقات الخدمة
- كيفية إنشاء عميل وخادم يستندان إلى gRPC استنادًا إلى "مخازن البروتوكولات المؤقتة" من خلال إنشاء الرموز البرمجية آليًا
- فهم عملية التواصل بين العميل والخادم باستخدام gRPC
هذا الدرس التطبيقي حول الترميز موجّه لمطوّري Go المبتدئين في 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 النموذجي.
أولاً، أنشئ دليل عمل الدرس التطبيقي وcd فيه:
mkdir streaming-grpc-go-getting-started && cd streaming-grpc-go-getting-started
نزِّل الدرس التطبيقي حول الترميز واستخرِجه:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \ | tar xvz --strip-components=4 \ grpc-codelabs-1/codelabs/grpc-go-streaming/start_here
بدلاً من ذلك، يمكنك تنزيل ملف .zip الذي يحتوي على دليل الدرس العملي فقط وفك ضغطه يدويًا.
يتوفّر الرمز المصدر المكتمل على GitHub إذا كنت تريد تخطّي كتابة عملية التنفيذ.
3- تحديد الرسائل والخدمات
تتمثّل خطوتك الأولى في تحديد خدمة gRPC للتطبيق وطُرق استدعاء الإجراء عن بُعد (RPC) وأنواع رسائل الطلبات والردود باستخدام مخازن البروتوكولات المؤقتة. ستوفّر خدمتك ما يلي:
- طُرق استدعاء الإجراء عن بُعد التي تحمل الأسماء
ListFeaturesوRecordRouteوRouteChatوالتي ينفّذها الخادم ويستدعيها العميل - أنواع الرسائل
PointوFeatureوRectangleوRouteNoteوRouteSummary، وهي عبارة عن بنى بيانات يتم تبادلها بين العميل والخادم عند استدعاء الطرق المذكورة أعلاه.
سيتم تحديد طرق "استدعاء الإجراء عن بُعد" هذه وأنواع الرسائل الخاصة بها في ملف routeguide/route_guide.proto الخاص بالرمز المصدر المقدَّم.
يُشار إلى "مخازن البروتوكولات المؤقتة" عادةً باسم 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;
}
بعد ذلك، تظهر رسالة Rectangle تمثّل مستطيلاً لخطوط الطول والعرض، ويتم تمثيله كنقطتَين متقابلتَين قطريًا "lo" و "hi".
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
بالإضافة إلى رسالة RouteNote التي تمثّل رسالة تم إرسالها في وقت معيّن.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
سنحتاج أيضًا إلى رسالة RouteSummary. يتم تلقّي هذه الرسالة ردًا على RecordRoute RPC، وهو ما سيتم توضيحه في القسم التالي. يحتوي هذا الملف على عدد النقاط الفردية التي تم تلقّيها، وعدد الميزات التي تم رصدها، وإجمالي المسافة المقطوعة كمجموع تراكمي للمسافة بين كل نقطة.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
تحديد طرق الخدمة
لتحديد خدمة، عليك تحديد خدمة مسماة في ملف .proto. يحتوي الملف route_guide.proto على بنية service باسم RouteGuide تحدّد طريقة واحدة أو أكثر توفّرها خدمة التطبيق.
حدِّد طرق RPC داخل تعريف الخدمة، مع تحديد أنواع الطلبات والردود. في هذا القسم من الدرس العملي، لنحدّد ما يلي:
ListFeatures
يحصل على Feature المتاحة ضمن Rectangle المحدّدة. يتم بث النتائج بدلاً من عرضها دفعة واحدة (مثلاً في رسالة ردّ تتضمّن حقلًا متكرّرًا)، لأنّ المستطيل قد يغطّي مساحة كبيرة ويحتوي على عدد كبير من العناصر.
النوع المناسب لهذا الإجراء عن بُعد هو إجراء عن بُعد للبث من جهة الخادم: يرسل العميل طلبًا إلى الخادم ويتلقّى بثًا لقراءة سلسلة من الرسائل. يقرأ العميل من البث الذي تم إرجاعه إلى أن لا تتبقى أي رسائل. كما هو موضّح في مثالنا، يمكنك تحديد طريقة البث من جهة الخادم من خلال وضع الكلمة الرئيسية stream قبل نوع الاستجابة.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
تقبل هذه السمة سلسلة من Point على مسار يتم اجتيازه، وتعرض RouteSummary عند اكتمال عملية الاجتياز.
يبدو أنّ طلب إجراء مكالمة RPC ببث من جهة العميل مناسب في هذه الحالة: يكتب العميل سلسلة من الرسائل ويرسلها إلى الخادم، وذلك باستخدام بث يتم توفيره مرة أخرى. بعد أن ينتهي العميل من كتابة الرسائل، ينتظر أن يقرأها الخادم كلها ويرسل رده. يمكنك تحديد طريقة البث من جهة العميل عن طريق وضع الكلمة الرئيسية stream قبل نوع الطلب.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
يقبل هذا النوع مجموعة من RouteNotes يتم إرسالها أثناء التنقّل في مسار، مع تلقّي RouteNotes أخرى (مثل تلك الواردة من مستخدمين آخرين).
هذا هو بالضبط نوع حالة الاستخدام للبث الثنائي الاتجاه. يحتوي استدعاء الإجراء عن بُعد (RPC) ثنائي الاتجاه على كلا الجانبين لإرسال سلسلة من الرسائل باستخدام بث للقراءة والكتابة. يعمل كل من هذين النوعين بشكل مستقل، ما يتيح للعملاء والخوادم القراءة والكتابة بأي ترتيب يفضلونه.
على سبيل المثال، يمكن للخادم الانتظار إلى أن يتلقّى جميع رسائل العميل قبل كتابة ردوده، أو يمكنه بدلاً من ذلك قراءة رسالة ثم كتابة رسالة، أو أي مجموعة أخرى من عمليات القراءة والكتابة.
يتم الحفاظ على ترتيب الرسائل في كل بث. يمكنك تحديد هذا النوع من الطرق من خلال وضع الكلمة الرئيسية stream قبل كلّ من الطلب والاستجابة.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
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 البعيدة للخدمة، ودوال يستخدمها الخادم لتقديم تلك الخدمة البعيدة.
بعد ذلك، سننفّذ الطرق من جهة الخادم، حتى يتمكّن الخادم من الردّ على الطلب الذي يرسله العميل.
5. تنفيذ الخدمة
لنلقِ نظرة أولاً على كيفية إنشاء خادم RouteGuide. يتضمّن إنجاح خدمة RouteGuide جزأين:
- تنفيذ واجهة الخدمة التي تم إنشاؤها من تعريف الخدمة: تنفيذ "العمل" الفعلي لخدمتنا
- تشغيل خادم gRPC للاستماع إلى الطلبات من العملاء وإرسالها إلى التنفيذ الصحيح للخدمة
لننفّذ RouteGuide في server/server.go.
تنفيذ RouteGuide
علينا تنفيذ واجهة RouteGuideService التي تم إنشاؤها. إليك طريقة التنفيذ.
type routeGuideServer struct {
...
}
...
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
...
}
...
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
...
}
...
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
...
}
لنلقِ نظرة على كل عملية تنفيذ لطلب إجراء عن بُعد بالتفصيل.
استدعاء إجراء عن بُعد (RPC) للبث من جهة الخادم
ابدأ بأحد إجراءات استدعاء الدوال عن بُعد (RPC) الخاصة بالبث. ListFeatures هو إجراء عن بُعد (RPC) لبث البيانات من جهة الخادم، لذا علينا إرسال عدة Feature إلى العميل.
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
for _, feature := range s.savedFeatures {
if inRange(feature.Location, rect) {
if err := stream.Send(feature); err != nil {
return err
}
}
}
return nil
}
كما ترى، بدلاً من الحصول على كائنات بسيطة للطلبات والردود في مَعلمات الطريقة، نحصل هذه المرة على كائن طلب (Rectangle الذي يريد عميلنا العثور على Features فيه) وكائن RouteGuide_ListFeaturesServer خاص لكتابة ردودنا. في الطريقة، نملأ أكبر عدد ممكن من عناصر Feature التي نحتاج إلى إرجاعها، ونكتبها في RouteGuide_ListFeaturesServer باستخدام طريقة Send(). أخيرًا، كما هو الحال في طلب استدعاء الإجراء عن بُعد (RPC) البسيط، نعرض خطأ nil لإعلام gRPC بأنّنا انتهينا من كتابة الردود. في حال حدوث أي خطأ في هذا الاستدعاء، سنعرض خطأ غير فارغ، وستحوّله طبقة gRPC إلى حالة RPC مناسبة ليتم إرسالها عبر الشبكة.
استدعاء إجراء عن بُعد للبث من جهة العميل
لنلقِ نظرة الآن على طريقة أكثر تعقيدًا: طريقة البث من جهة العميل RecordRoute، حيث نحصل على بث من Points من العميل ونعرض RouteSummary واحدًا يتضمّن معلومات حول رحلته. كما تلاحظ، لا تتضمّن الطريقة هذه المرة أي مَعلمة طلب. بدلاً من ذلك، يحصل على بث RouteGuide_RecordRouteServer، ويمكن للخادم استخدامه لقراءة الرسائل وكتابتها. ويمكنه تلقّي رسائل العميل باستخدام طريقة Recv() وعرض ردّه الفردي باستخدام طريقة SendAndClose().
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
var pointCount, featureCount, distance int32
var lastPoint *pb.Point
startTime := time.Now()
for {
point, err := stream.Recv()
if err == io.EOF {
endTime := time.Now()
return stream.SendAndClose(&pb.RouteSummary{
PointCount: pointCount,
FeatureCount: featureCount,
Distance: distance,
ElapsedTime: int32(endTime.Sub(startTime).Seconds()),
})
}
if err != nil {
return err
}
pointCount++
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
featureCount++
}
}
if lastPoint != nil {
distance += calcDistance(lastPoint, point)
}
lastPoint = point
}
}
في نص الطريقة، نستخدم طريقة Recv() الخاصة بـ RouteGuide_RecordRouteServer لقراءة طلبات العميل بشكل متكرر في عنصر طلب (في هذه الحالة Point) إلى أن لا تكون هناك أي رسائل أخرى: يجب أن يتحقّق الخادم من الخطأ الذي تم إرجاعه من Recv() بعد كل عملية استدعاء. إذا كانت القيمة nil، يكون البث جيدًا ويمكن مواصلة القراءة، أما إذا كانت القيمة io.EOF، يكون بث الرسائل قد انتهى ويمكن للخادم عرض القيمة RouteSummary. إذا كانت تتضمّن أي قيمة أخرى، نعرض الخطأ "كما هو" ليتم ترجمته إلى حالة RPC من خلال طبقة gRPC.
استدعاء إجراء عن بُعد (RPC) باستخدام البث الثنائي الاتجاه
أخيرًا، لنلقِ نظرة على استدعاء الإجراء عن بُعد (RPC) RouteChat() الذي يتيح البث الثنائي الاتجاه.
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
key := serialize(in.Location)
s.mu.Lock()
s.routeNotes[key] = append(s.routeNotes[key], in)
// Note: this copy prevents blocking other clients while serving this one.
// We don't need to do a deep copy, because elements in the slice are
// insert-only and never modified.
rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
copy(rn, s.routeNotes[key])
s.mu.Unlock()
for _, note := range rn {
if err := stream.Send(note); err != nil {
return err
}
}
}
}
في هذه المرة، نحصل على بث RouteGuide_RouteChatServer يمكن استخدامه لقراءة الرسائل وكتابتها، كما هو الحال في مثال البث من جهة العميل. ومع ذلك، فإنّنا نعرض هذه المرة القيم من خلال بث الطريقة بينما لا يزال العميل يكتب الرسائل في بث الرسائل. إنّ بنية القراءة والكتابة هنا تشبه إلى حد كبير طريقة البث من العميل، باستثناء أنّ الخادم يستخدم طريقة send() للبث بدلاً من SendAndClose() لأنّه يكتب ردودًا متعددة. على الرغم من أنّ كل طرف سيتلقّى دائمًا رسائل الطرف الآخر بالترتيب الذي تمت كتابتها به، يمكن لكل من العميل والخادم القراءة والكتابة بأي ترتيب، إذ تعمل عمليات البث بشكل مستقل تمامًا.
بدء تشغيل الخادم
بعد تنفيذ جميع طرقنا، نحتاج أيضًا إلى بدء تشغيل خادم gRPC حتى يتمكّن العملاء من استخدام خدمتنا. يوضّح المقتطف التالي كيفية إجراء ذلك في خدمة RouteGuide:
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
s := &routeGuideServer{routeNotes: make(map[string][]*pb.RouteNote)}
s.loadFeatures()
pb.RegisterRouteGuideServer(grpcServer, s)
grpcServer.Serve(lis)
في ما يلي الخطوات المفصّلة لما يحدث في main():
- حدِّد منفذ TCP الذي سيتم استخدامه للاستماع إلى طلبات العميل البعيد، باستخدام
lis, err := net.Listen(...). تستخدم التطبيقات تلقائيًا منفذ TCP رقم50051كما هو محدّد في المتغيّرportأو من خلال تمرير الخيار--portفي سطر الأوامر عند تشغيل الخادم. إذا تعذّر فتح منفذ TCP، ينتهي التطبيق بحدوث خطأ فادح. - أنشئ مثيلاً لخادم gRPC باستخدام
grpc.NewServer(...)، وسمِّ هذا المثيلgrpcServer. - أنشئ مؤشرًا إلى
routeGuideServer، وهو بنية تمثّل خدمة واجهة برمجة التطبيق، وسمِّ المؤشرs. - استخدِم
s.loadFeatures()لملء المصفوفةs.savedFeatures. - تسجيل تنفيذ الخدمة في خادم gRPC
- قم باستدعاء
Serve()على الخادم مع تزويده بتفاصيل المنفذ لبدء وضع الانتظار المعطِّل لطلبات العميل، ويستمر ذلك إلى أن يتم إنهاء العملية أو يتم استدعاءStop().
تستمدّ الدالة loadFeatures() عمليات الربط بين الإحداثيات والأماكن من server/testdata.go.
6. إنشاء العميل
عدِّل client/client.go،حيث ستقوم بتشغيل رمز العميل.
لاستدعاء طرق الخدمة البعيدة، علينا أولاً إنشاء قناة gRPC للتواصل مع الخادم. سننشئها من خلال تمرير سلسلة معرف الموارد المنتظم (URI) المستهدفة للخادم (وهي في هذه الحالة العنوان ورقم المنفذ) إلى grpc.NewClient() في دالة main() الخاصة بالعميل على النحو التالي:
// 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()
عنوان الخادم، الذي يحدّده المتغيّر 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_streaming/routeguide")
client := pb.NewRouteGuideClient(conn)
طُرق خدمات الاستدعاء
لنلقِ نظرة الآن على كيفية استدعاء طرق الخدمة. في gRPC-Go، تعمل طلبات استدعاء الإجراء عن بُعد (RPC) في وضع الحظر/التزامن، ما يعني أنّ طلب RPC ينتظر رد الخادم، وسيعرض إما ردًا أو خطأ.
استدعاء إجراء عن بُعد (RPC) للبث من جهة الخادم
هنا نستدعي طريقة البث من جهة الخادم ListFeatures، والتي تعرض بثًا لعناصر Feature جغرافية.
rect := &pb.Rectangle{ ... } // initialize a pb.Rectangle
log.Printf("Looking for features within %v", rect)
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
log.Fatalf("client.ListFeatures failed: %v", err)
}
for {
// For server-to-client streaming RPCs, you call stream.Recv() until it
// returns io.EOF.
feature, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("client.ListFeatures failed: %v", err)
}
log.Printf("Feature: name: %q, point:(%v, %v)", feature.GetName(),
feature.GetLocation().GetLatitude(), feature.GetLocation().GetLongitude())
}
كما هو الحال في إجراء RPC البسيط، نمرّر إلى الطريقة سياقًا وطلبًا. ومع ذلك، بدلاً من الحصول على عنصر استجابة، نحصل على مثيل من RouteGuide_ListFeaturesClient. يمكن للعميل استخدام بث RouteGuide_ListFeaturesClient لقراءة ردود الخادم. نستخدم طريقة RouteGuide_ListFeaturesClient في Recv() لقراءة ردود الخادم بشكل متكرّر إلى عنصر Protocol Buffers للرد (في هذه الحالة Feature) إلى أن لا تتوفر المزيد من الرسائل: على العميل التحقّق من الخطأ err الذي تم إرجاعه من Recv() بعد كل عملية استدعاء. إذا كانت القيمة nil، يكون البث جيدًا ويمكن مواصلة القراءة، وإذا كانت القيمة io.EOF، يكون بث الرسائل قد انتهى، وإلا يجب أن يكون هناك خطأ في طلب استدعاء الإجراء عن بُعد (RPC)، ويتم تمريره من خلال err.
استدعاء إجراء عن بُعد للبث من جهة العميل
تشبه طريقة البث من جهة العميل RecordRoute طريقة البث من جهة الخادم، باستثناء أنّنا نمرّر إلى الطريقة سياقًا فقط ونحصل على بث RouteGuide_RecordRouteClient، يمكننا استخدامه في كل من كتابة الرسائل وقراءتها.
// Create a random number of random points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
c2sStream, err := client.RecordRoute(context.TODO())
if err != nil {
log.Fatalf("client.RecordRoute failed: %v", err)
}
// Stream each point to the server.
for _, point := range points {
if err := c2sStream.Send(point); err != nil {
log.Fatalf("client.RecordRoute: stream.Send(%v) failed: %v", point, err)
}
}
// Close the stream and receive the RouteSummary from the server.
reply, err := c2sStream.CloseAndRecv()
if err != nil {
log.Fatalf("client.RecordRoute failed: %v", err)
}
log.Printf("Route summary: %v", reply)
يحتوي RouteGuide_RecordRouteClient على طريقة Send() يمكننا استخدامها لإرسال الطلبات إلى الخادم. بعد الانتهاء من كتابة طلبات العميل إلى البث باستخدام Send()، علينا استدعاء CloseAndRecv() على البث لإعلام gRPC بأنّنا انتهينا من الكتابة ونتوقّع تلقّي ردّ. نحصل على حالة RPC من الخطأ الذي تم إرجاعه من CloseAndRecv(). إذا كانت الحالة nil، ستكون القيمة المرجَعة الأولى من CloseAndRecv() استجابة صالحة من الخادم.
استدعاء إجراء عن بُعد (RPC) باستخدام البث الثنائي الاتجاه
أخيرًا، لنلقِ نظرة على استدعاء الإجراء عن بُعد (RPC) RouteChat() الذي يتيح البث الثنائي الاتجاه. كما هو الحال في RecordRoute، لا نمرّر إلى الطريقة سوى عنصر سياق ونستردّ منها مصدر بيانات يمكننا استخدامه لكتابة الرسائل وقراءتها. ومع ذلك، نعرض هذه المرة القيم من خلال بث الطريقة بينما لا يزال الخادم يكتب الرسائل إلى بث الرسائل.
biDiStream, err := client.RouteChat(context.Background())
if err != nil {
log.Fatalf("client.RouteChat failed: %v", err)
}
// this channel is used to wait for the receive goroutine to finish.
recvDoneCh := make(chan struct{})
// receive goroutine.
go func() {
for {
in, err := biDiStream.Recv()
if err == io.EOF {
// read done.
close(recvDoneCh)
return
}
if err != nil {
log.Fatalf("client.RouteChat failed: %v", err)
}
log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
}
}()
// send messages simultaneously.
for _, note := range notes {
if err := biDiStream.Send(note); err != nil {
log.Fatalf("client.RouteChat: stream.Send(%v) failed: %v", note, err)
}
}
biDiStream.CloseSend()
// wait for the receive goroutine to finish.
<-recvDoneCh
إنّ بنية القراءة والكتابة هنا تشبه إلى حد كبير طريقة البث من جهة العميل، باستثناء أنّنا نستخدم طريقة CloseSend() للبث بعد الانتهاء من المكالمة. على الرغم من أنّ كل طرف سيتلقّى دائمًا رسائل الطرف الآخر بالترتيب الذي تمت كتابتها به، يمكن لكل من العميل والخادم القراءة والكتابة بأي ترتيب، إذ تعمل عمليات البث بشكل مستقل تمامًا.
7. للتجربة:
تأكَّد من أنّ الخادم والعميل يعملان معًا بشكل صحيح من خلال تنفيذ الأوامر التالية في دليل العمل الخاص بالتطبيق:
- شغِّل الخادم في إحدى الوحدات الطرفية:
cd server go run .
- شغِّل العميل من نافذة طرفية أخرى:
cd client go run .
ستظهر لك نتيجة مشابهة لما يلي، مع حذف الطوابع الزمنية لتوضيح الصورة:
Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 > name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 > ... name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 > Traversing 56 points. Route summary: point_count:56 distance:497013163 Got message First message at point(0, 1) Got message Second message at point(0, 2) Got message Third message at point(0, 3) Got message First message at point(0, 1) Got message Fourth message at point(0, 1) Got message Second message at point(0, 2) Got message Fifth message at point(0, 2) Got message Third message at point(0, 3) Got message Sixth message at point(0, 3)
8. الخطوات التالية
- تعرَّف على طريقة عمل gRPC في مقدمة عن gRPC والمفاهيم الأساسية.
- اتّبِع الخطوات الواردة في البرنامج التعليمي الخاص بالأساسيات.
- استكشف مرجع واجهة برمجة التطبيقات.