بناء خدمة تدفق بيانات من الخادم (Server-Side Streaming) باستخدام gRPC ولغة Go

دقائق القراءة: 7

مقدمة إلى تدفق البيانات من الخادم (Server-Side Streaming) مع gRPC و Go

هل فكرت يومًا في إمكانية إرسال عدة استجابات من الخادم عبر اتصال واحد فقط؟ هذا بالضبط ما يدور حوله مقالنا اليوم. سنستكشف معًا كيفية تطبيق تدفق البيانات من الخادم (Server-Side Streaming) باستخدام إطار عمل gRPC ولغة البرمجة Go، مما يتيح لك بناء تطبيقات ويب عالية الأداء ومرنة.

شعار gRPC ولغة Go يمثلان تدفق البيانات

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

في هذا المقال، سأبدأ بوصف كل من gRPC وتدفق البيانات من الخادم. إذا كنت تعرفهما بالفعل، فلا تتردد في تخطي القسمين الأولين.

ما هو gRPC؟

رسم توضيحي يوضح آلية عمل gRPC بين العميل والخادم

هل حلمت يومًا باستدعاء طلب من الخادم باستخدام استدعاء دالة (function call) بدلاً من استخدام استدعاء HTTP مع عنوان URL؟ حسنًا، هذا موجود بالفعل منذ فترة طويلة، ونطلق عليه اسم Remote Procedure Call (اختصارًا: RPC). وفي عام 2015، قدمت جوجل شيئًا يسمى gRPC، والذي هو في الأساس Remote Procedure Call محسّن بشكل كبير.

يعمل gRPC بنفس طريقة Remote Procedure Call التقليدي تقريبًا، لكن جوجل أدخلت استخدام HTTP/2 كبروتوكول اتصال و protobuf (Protocol Buffers) كعقد اتصال بين الخادم والعميل. تم إنشاء HTTP/2 أيضًا بواسطة جوجل، ويسمح للاتصال بأن يكون أكثر كفاءة بشكل ملحوظ. كما أنه يدعم تعدد الإرسال (multiplexing)، والذي سأتحدث عنه لاحقًا. أما Protobuf فهو في الأساس العقد المستخدم لتمكين الاتصال بين الخادم والعميل عبر استدعاء دالة.

هذه نظرة عامة أساسية على gRPC. إذا كنت مهتمًا وترغب في التعمق أكثر، يمكنك قراءة المزيد من التفاصيل هنا.

ما هو تدفق البيانات من الخادم (Server-Side Streaming)؟

صورة توضيحية لمفهوم تدفق البيانات

الصورة من Jon Flobrant عبر Unsplash

والآن، ماذا عن تدفق البيانات من الخادم؟ بحكم تصميمه، يستخدم gRPC بروتوكول HTTP/2 ويدعم شيئًا يسمى تعدد الإرسال (multiplexing). لن أدخل في الكثير من التفاصيل هنا، لكنه يسمح لطلب واحد بالحصول على استجابات متعددة، والعكس صحيح. يتم تطبيق هذه الآلية في gRPC وتسمى streaming (التدفق).

هناك 3 أنواع من التدفق في gRPC:

  • تدفق من جانب العميل (Client-side streaming): حيث يرسل العميل طلبات متعددة ويقوم الخادم بإرجاع استجابة واحدة فقط.
  • تدفق ثنائي الاتجاه (Bidirectional streaming): حيث يمكن لكل من العميل والخادم إرسال واستقبال طلبات واستجابات متعددة معًا ضمن اتصال واحد.
  • تدفق من جانب الخادم (Server-side streaming): حيث يرسل العميل طلبًا واحدًا ويمكن للخادم إرجاع عدة استجابات معًا. هذا هو النوع الذي سأوضح لك كيفية تنفيذه اليوم.

كيفية تنفيذ تدفق البيانات من الخادم (Server-Side Streaming)

صورة توضيحية لعملية البرمجة والتنفيذ

الصورة من Cam Adams عبر Unsplash

حان وقت التنفيذ! إذا كنت تقرأ هذا القسم، أفترض أنك تعرف بالفعل هذه المفاهيم الثلاثة:

  • gRPC
  • تدفق البيانات من الخادم (Server-side streaming)
  • لغة Go

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

حسنًا، دعنا ننتقل مباشرة إلى الكود.

إنشاء ملف Proto

للبداية، سنحتاج إلى تعريف ملف protobuf الخاص بنا والذي سيستخدمه كل من العميل والخادم. دعنا ننشئ ملفًا بسيطًا هنا، مثل هذا:

syntax = "proto3";
package protobuf;

service StreamService {
  rpc FetchResponse (Request) returns (stream Response) {}
}

message Request {
  int32 id = 1;
}

message Response {
  string result = 1;
}

يحتوي ملف proto هذا بشكل أساسي على استدعاء دالة واحدة مع معلمة Request ويعيد تدفقًا من Response. قبل المتابعة، نحتاج أيضًا إلى إنشاء ملف pb الخاص بنا والذي سيستخدمه برنامج Go الخاص بنا. سيكون لكل لغة برمجة طريقة مختلفة لإنشاء ملف protocol buffer. في لغة Go، سنستخدم مكتبة protoc. إذا لم تكن قد قمت بتثبيتها بعد، فإن جوجل توفر دليل التثبيت لذلك هنا.

دعنا ننشئ ملف protocol buffer عن طريق تشغيل الأمر التالي:

protoc --go_out=plugins=grpc:. *.proto

والآن لدينا ملف data.pb.go جاهز للاستخدام.

إنشاء ملف العميل (Client File)

بالنسبة للخطوة التالية، يمكنك إنشاء ملف العميل أو ملف الخادم، بأي ترتيب. ولكن في هذا المثال، سأقوم بإنشاء ملف العميل أولاً.

package main

import (
	"context"
	"io"
	"log"

	pb "github.com/pramonow/go-grpc-server-streaming-example/src/proto"
	"google.golang.org/grpc"
)

func main() {
	// dial server
	conn, err := grpc.Dial(":50005", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("can not connect with server %v", err)
	}

	// create stream
	client := pb.NewStreamServiceClient(conn)
	in := &pb.Request{Id: 1}
	stream, err := client.FetchResponse(context.Background(), in)
	if err != nil {
		log.Fatalf("open stream error %v", err)
	}

	done := make(chan bool)

	go func() {
		for {
			resp, err := stream.Recv()
			if err == io.EOF {
				done <- true //means stream is finished
				return
			}
			if err != nil {
				log.Fatalf("cannot receive %v", err)
			}
			log.Printf("Resp received: %s", resp.Result)
		}
	}()

	<-done //we will wait until all response is received
	log.Printf("finished")
}

سيكون العميل هو الذي يرسل الطلب ويتلقى استجابات متعددة. سيستدعي العميل طريقة gRPC المسماة FetchResponse وينتظر جميع الاستجابات. أستخدم هنا goroutine لإظهار إمكانية التزامن، وأستخدم channel للانتظار حتى تنتهي جميع العمليات قبل إنهاء البرنامج.

إنشاء ملف الخادم (Server File)

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

package main

import (
	"fmt"
	"log"
	"net"
	"sync"
	"time"

	pb "github.com/pramonow/go-grpc-server-streaming-example/src/proto"
	"google.golang.org/grpc"
)

type server struct{}

func (s server) FetchResponse(in *pb.Request, srv pb.StreamService_FetchResponseServer) error {
	log.Printf("fetch response for id : %d", in.Id)
	//use wait group to allow process to be concurrent
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(count int64) {
			defer wg.Done()
			//time sleep to simulate server process time
			time.Sleep(time.Duration(count) * time.Second)
			resp := pb.Response{Result: fmt.Sprintf("Request #%d For Id:%d", count, in.Id)}
			if err := srv.Send(&resp); err != nil {
				log.Printf("send error %v", err)
			}
			log.Printf("finishing request number : %d", count)
		}(int64(i))
	}
	wg.Wait()
	return nil
}

func main() {
	// create listiner
	lis, err := net.Listen("tcp", ":50005")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// create grpc server
	s := grpc.NewServer()
	pb.RegisterStreamServiceServer(s, server{})
	log.Println("start server")

	// and start...
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

في ملف الخادم، أستخدم أيضًا goroutine لمحاكاة عملية متزامنة. لكل طلب، سأقوم بتدفق خمسة طلبات مرة أخرى إلى جانب العميل. سيكون لكل منها أيضًا وقت معالجة مختلف لمحاكاة أوقات المعالجة المختلفة التي قد تواجهها في سيناريو واقعي.

النتائج المتوقعة

الآن يأتي الجزء المثير. دعنا نبني ملفي العميل والخادم باستخدام go build للحصول على ملفنا الثنائي. ثم سنفتح أمرين منفصلين في سطر الأوامر لتشغيلهما. ملاحظة سريعة: يجب تشغيل الخادم أولاً قبل العميل، حيث سيستدعي العميل طريقة الخادم مباشرة.

لذلك، دعنا ننتقل إلى دليل كل من ملفاتنا الثنائية ونشغل كليهما باستخدام ./server و ./client.

سيخرج العميل ما يلي:

2020/11/10 22:26:11 Resp received: Request #0 For Id: 1
2020/11/10 22:26:12 Resp received: Request #1 For Id: 1
2020/11/10 22:26:13 Resp received: Request #2 For Id: 1
2020/11/10 22:26:14 Resp received: Request #3 For Id: 1
2020/11/10 22:26:15 Resp received: Request #4 For Id: 1
2020/11/10 22:26:15 finished

وسيخرج الخادم ما يلي:

2020/11/10 22:26:09 start server
2020/11/10 22:26:11 fetch response for id : 1
2020/11/10 22:26:11 finishing request number : 0
2020/11/10 22:26:12 finishing request number : 1
2020/11/10 22:26:13 finishing request number : 2
2020/11/10 22:26:14 finishing request number : 3
2020/11/10 22:26:15 finishing request number : 4

إذا سار كل شيء على ما يرام، فقد قمت بإنشاء خدمة تدفق بيانات من الخادم باستخدام gRPC ولغة Go بنجاح! إذا كنت بحاجة إلى كود GitHub للمثال بأكمله، يمكنك العثور عليه هنا.

الخلاصة التقنية

يُعد تنفيذ تدفق البيانات من الخادم (Server-Side Streaming) باستخدام gRPC ولغة Go إضافة قوية لأي مهندس برمجيات يسعى لتحسين كفاءة الاتصال بين الخادم والعميل، خاصة في سيناريوهات تتطلب إرسال كميات كبيرة من البيانات بشكل متقطع أو تحديثات في الوقت الفعلي. من خلال الاستفادة من بروتوكول HTTP/2 وتعدد الإرسال، يتجاوز gRPC قيود طلب/استجابة HTTP التقليدية، مما يفتح الباب أمام تطبيقات أكثر تفاعلية واستجابة.

على الرغم من أن هذا النمط قد لا يكون ضروريًا لكل مشروع، إلا أنه يمثل أداة قيمة في صندوق أدوات المطور، مما يمكنه من تصميم أنظمة موزعة أكثر قوة ومرونة. إن فهم كيفية عمل التدفق، إلى جانب إتقان لغة Go، يضع المطور في موقع متقدم لبناء حلول مبتكرة لمواجهة تحديات نقل البيانات الحديثة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *