1. مقدمة
في هذا الدرس العملي، ستستخدم gRPC-Python لإنشاء عميل وخادم يشكّلان أساس تطبيق لربط المسارات مكتوب بلغة Python.
في نهاية هذا البرنامج التعليمي، سيكون لديك برنامج يتصل بخادم بعيد باستخدام gRPC للحصول على معلومات حول الميزات على مسار العميل، وإنشاء ملخّص لمسار العميل، وتبادل معلومات المسار، مثل آخر الأخبار عن حركة المرور، مع الخادم والعملاء الآخرين.
يتم تحديد الخدمة في ملف Protocol Buffers، وسيتم استخدام هذا الملف لإنشاء رمز نموذجي للبرنامج العميل والخادم حتى يتمكّنا من التواصل مع بعضهما البعض، ما يوفّر عليك الوقت والجهد في تنفيذ هذه الوظيفة.
لا يهتم هذا الرمز الذي تم إنشاؤه بتعقيدات الاتصال بين الخادم والعميل فحسب، بل أيضًا بتسلسل البيانات وإلغاء تسلسلها.
أهداف الدورة التعليمية
- كيفية استخدام مخزن البروتوكولات المؤقت لتحديد واجهة برمجة تطبيقات الخدمة
- كيفية إنشاء برنامج عميل وخادم يستندان إلى gRPC من تعريف Protocol Buffers باستخدام إنشاء الرموز البرمجية المبرمَج
- فهم عملية نقل البيانات المتدفقة بين العميل والخادم باستخدام gRPC
هذا الدرس التطبيقي حول الترميز مخصّص لمطوّري Python الجدد على gRPC أو الذين يريدون مراجعة gRPC، أو أي شخص آخر مهتم بإنشاء أنظمة موزّعة. لا يُشترط توفّر خبرة سابقة في gRPC.
2. قبل البدء
المتطلبات
- الإصدار 3.9 أو الإصدارات الأحدث من Python ننصح باستخدام الإصدار 3.13 من Python. للحصول على تعليمات التثبيت الخاصة بكل نظام أساسي، يُرجى الاطّلاع على إعداد Python واستخدامه. بدلاً من ذلك، ثبِّت إصدارًا غير تابع للنظام من Python باستخدام أدوات مثل uv أو pyenv.
- pip لتثبيت حِزم Python
- venv لإنشاء بيئات Python افتراضية
الحزمتان ensurepip
وvenv
هما جزء من مكتبة Python العادية، وتتوفّران عادةً بشكل تلقائي.
ومع ذلك، تختار بعض التوزيعات المستندة إلى Debian (بما في ذلك Ubuntu) استبعادها عند إعادة توزيع Python. لتثبيت الحِزم، نفِّذ ما يلي:
sudo apt install python3-pip python3-venv
الحصول على الشفرة
لتبسيط عملية التعلّم، يوفّر لك هذا الدرس التطبيقي حول الترميز بنية رمز مصدر مُنشأة مسبقًا لمساعدتك على البدء. ستساعدك الخطوات التالية في إكمال الطلب، بما في ذلك إنشاء رمز gRPC باستخدام grpc_tools.protoc
مكوّن إضافي لمترجم Protocol Buffer.
grpc-codelabs
يتوفّر رمز المصدر الأساسي لهذا الدرس التطبيقي حول الترميز في الدليل codelabs/grpc-python-streaming/start_here. إذا كنت تفضّل عدم تنفيذ الرمز بنفسك، يتوفّر رمز المصدر المكتمل في الدليل completed
.
أولاً، أنشئ دليل عمل الدرس التطبيقي وادخله:
mkdir grpc-python-streaming && cd grpc-python-streaming
نزِّل الدرس التطبيقي حول الترميز واستخرِجه:
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-python-streaming/start_here
بدلاً من ذلك، يمكنك تنزيل ملف .zip الذي يحتوي على دليل الدرس العملي فقط وفك ضغطه يدويًا.
3- تحديد الرسائل والخدمات
تتمثّل خطوتك الأولى في تحديد خدمة gRPC للتطبيق وطريقة RPC وأنواع رسائل الطلبات والاستجابات باستخدام بروتوكول المخازن المؤقتة. ستوفّر خدمتك ما يلي:
- طُرق استدعاء الإجراء عن بُعد (RPC) التي تحمل الأسماء
ListFeatures
وRecordRoute
وRouteChat
والتي ينفّذها الخادم ويستدعيها العميل - أنواع الرسائل
Point
وFeature
وRectangle
وRouteNote
وRouteSummary
، وهي عبارة عن بنى بيانات يتم تبادلها بين العميل والخادم عند استدعاء طرق RPC
سيتم تحديد جميع طرق RPC وأنواع الرسائل الخاصة بها في ملف protos/route_guide.proto
الخاص برمز المصدر المقدَّم.
يُشار إلى Protocol Buffers عادةً باسم protobufs. لمزيد من المعلومات عن مصطلحات gRPC، يُرجى الاطّلاع على المفاهيم الأساسية والبنية ودورة الحياة في gRPC.
تحديد أنواع الرسائل
في ملف protos/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
المحدّد. يتم بث النتائج بدلاً من عرضها دفعة واحدة لأنّ المستطيل قد يغطي مساحة كبيرة ويحتوي على عدد كبير من العناصر.
بالنسبة إلى هذا التطبيق، ستستخدم استدعاء إجراء عن بُعد (RPC) للبث من جهة الخادم: يرسل العميل طلبًا إلى الخادم ويتلقّى بثًا لقراءة سلسلة من الرسائل. يقرأ العميل من البث الذي تم إرجاعه إلى أن لا تتبقى أي رسائل. كما ترى في مثالنا، يمكنك تحديد طريقة البث من جهة الخادم عن طريق وضع الكلمة الرئيسية stream قبل نوع الاستجابة.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
تقبل هذه السمة مجموعة من النقاط على مسار يتم اجتيازه، وتعرض RouteSummary
عند اكتمال عملية الاجتياز.
في هذه الحالة، يكون طلب إجراء مكالمة RPC ببث من جهة العميل مناسبًا: يكتب العميل سلسلة من الرسائل ويرسلها إلى الخادم، وذلك باستخدام بث يتم توفيره مرة أخرى. بعد أن ينتهي العميل من كتابة الرسائل، ينتظر أن يقرأها الخادم كلها ويرسل رده. يمكنك تحديد طريقة البث من جهة العميل عن طريق وضع الكلمة الرئيسية stream قبل نوع الطلب.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
يقبل هذا النوع من الطلبات بثًا من RouteNotes
يتم إرساله أثناء التنقّل في مسار، مع تلقّي RouteNotes
أخرى (مثل تلك الواردة من مستخدمين آخرين).
هذا هو بالضبط نوع حالة الاستخدام للبث الثنائي الاتجاه. إجراء استدعاء إجراء عن بُعد (RPC) لبث البيانات في اتجاهين، حيث يرسل كلا الجانبين سلسلة من الرسائل باستخدام بث للقراءة والكتابة يعمل كل من هذين التدفقين بشكل مستقل، لذا يمكن للعملاء والخوادم القراءة والكتابة بأي ترتيب يريدونه: على سبيل المثال، يمكن للخادم الانتظار لتلقّي جميع رسائل العميل قبل كتابة ردوده، أو يمكنه بدلاً من ذلك قراءة رسالة ثم كتابة رسالة، أو أي مجموعة أخرى من عمليات القراءة والكتابة. يتم الحفاظ على ترتيب الرسائل في كل بث. يمكنك تحديد هذا النوع من الطرق من خلال وضع الكلمة الرئيسية "بث" قبل كلّ من الطلب والاستجابة.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. إنشاء رمز العميل والخادم
بعد ذلك، أنشئ رمز gRPC النموذجي لكل من البرنامج الخادم والبرنامج العميل من ملف .proto
باستخدام برنامج ترجمة مخزن البروتوكولات المؤقت.
لإنشاء رمز Python في gRPC، أنشأنا grpcio-tools. ويشمل ذلك:
- برنامج التجميع العادي protoc الذي ينشئ رمز Python من تعريفات
message
- مكوّن إضافي لبروتوكول gRPC protobuf ينشئ رمز Python (طرق كعب العميل والخادم) من تعريفات
service
.
سنثبّت حزمة grpcio-tools
Python باستخدام pip. لننشئ بيئة افتراضية جديدة بلغة Python (venv) لعزل تبعيات مشروعك عن حِزم النظام:
python3 -m venv --upgrade-deps .venv
لتفعيل البيئة الافتراضية في shell bash/zsh، اتّبِع الخطوات التالية:
source .venv/bin/activate
بالنسبة إلى Windows والأصداف غير العادية، يمكنك الاطّلاع على الجدول في https://docs.python.org/3/library/venv.html#how-venvs-work.
بعد ذلك، ثبِّت grpcio-tools (سيؤدي ذلك أيضًا إلى تثبيت حزمة grpcio):
pip install grpcio-tools
استخدِم الأمر التالي لإنشاء رمز Python الأولي:
python -m grpc_tools.protoc --proto_path=./protos \
--python_out=. --pyi_out=. --grpc_python_out=. \
./protos/route_guide.proto
سيؤدي ذلك إلى إنشاء الملفات التالية للواجهات التي حدّدناها في route_guide.proto
:
- يحتوي
route_guide_pb2.py
على الرمز الذي ينشئ الفئات بشكل ديناميكي من تعريفاتmessage
. route_guide_pb2.pyi
هو "ملف فارغ" أو "ملف تلميح النوع" تم إنشاؤه من تعريفاتmessage
. يحتوي فقط على التواقيع بدون تنفيذ. يمكن أن تستخدم بيئات التطوير المتكاملة ملفات Stub لتوفير ميزة الإكمال التلقائي ورصد الأخطاء بشكل أفضل.- يتم إنشاء
route_guide_pb2_grpc.py
من تعريفاتservice
ويحتوي على فئات ودوال خاصة بـ gRPC.
يتضمّن الرمز البرمجي الخاص بـ gRPC ما يلي:
5- إنشاء الخادم
لنبدأ بالاطّلاع على كيفية إنشاء خادم RouteGuide
. ينقسم إنشاء خادم RouteGuide
وتشغيله إلى عنصرَي عمل:
- تنفيذ واجهة مقدّم الخدمة التي تم إنشاؤها من تعريف الخدمة باستخدام وظائف تنفّذ "العمل" الفعلي للخدمة
- تشغيل خادم gRPC للاستماع إلى الطلبات من العملاء وإرسال الردود
لنلقِ نظرة على route_guide_server.py
.
تنفيذ RouteGuide
يحتوي route_guide_server.py
على صف RouteGuideServicer
يندرج ضمن فئة الصف route_guide_pb2_grpc.RouteGuideServicer
التي تم إنشاؤها:
# RouteGuideServicer provides an implementation of the methods of the RouteGuide service.
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
تنفّذ RouteGuideServicer
جميع طرق الخدمة RouteGuide
.
Server-side streaming RPC
ListFeatures
هي عملية RPC لبث الردود ترسل عدة Feature
إلى العميل:
def ListFeatures(self, request, context):
"""List all features contained within the given Rectangle."""
left = min(request.lo.longitude, request.hi.longitude)
right = max(request.lo.longitude, request.hi.longitude)
top = max(request.lo.latitude, request.hi.latitude)
bottom = min(request.lo.latitude, request.hi.latitude)
for feature in self.db:
lat, lng = feature.location.latitude, feature.location.longitude
if left <= lng <= right and bottom <= lat <= top:
yield feature
في هذا المثال، رسالة الطلب هي route_guide_pb2.Rectangle
التي يريد العميل العثور على Feature
ضمنها. بدلاً من عرض ردّ واحد، تعرض الطريقة صفر ردّ أو أكثر.
Client-side streaming RPC
تستخدم طريقة بث الطلبات RecordRoute
مكرّرًا لقيم الطلبات وتعرض قيمة استجابة واحدة.
def RecordRoute(self, request_iterator, context):
"""Calculate statistics about the trip composed of Points."""
point_count = 0
feature_count = 0
distance = 0.0
prev_point = None
start_time = time.time()
for point in request_iterator:
point_count += 1
if get_feature(self.db, point):
feature_count += 1
if prev_point:
distance += get_distance(prev_point, point)
prev_point = point
elapsed_time = time.time() - start_time
return route_guide_pb2.RouteSummary(
point_count=point_count,
feature_count=feature_count,
distance=int(distance),
elapsed_time=int(elapsed_time),
)
استدعاء إجراء عن بُعد (RPC) ثنائي الاتجاه
أخيرًا، لنلقِ نظرة على RPC RouteChat()
للبث الثنائي الاتجاه:
def RouteChat(self, request_iterator, context):
"""
Receive a stream of message/location pairs, and responds with
a stream of all previous messages for the given location.
"""
prev_notes = []
for new_note in request_iterator:
for prev_note in prev_notes:
if prev_note.location == new_note.location:
yield prev_note
prev_notes.append(new_note)
تجمع دلالات هذه الطريقة بين دلالات طريقة بث الطلبات وطريقة بث الردود. يتم تمرير مكرّر لقيم الطلبات إليه، وهو نفسه مكرّر لقيم الاستجابات.
بدء الخادم
بعد تنفيذ جميع طرق RouteGuide
، تتمثل الخطوة التالية في بدء تشغيل خادم gRPC حتى يتمكّن العملاء من استخدام خدمتك فعليًا:
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
RouteGuideServicer(),
server,
)
listen_addr = "localhost:50051"
server.add_insecure_port(listen_addr)
print(f"Starting server on {listen_addr}")
server.start()
server.wait_for_termination()
طريقة الخادم start()
غير حظر. سيتم إنشاء سلسلة محادثات جديدة للتعامل مع الطلبات. في كثير من الأحيان، لن يكون لدى سلسلة التعليمات التي تستدعي server.start()
أي عمل آخر لتنفيذه في الوقت الحالي. في هذه الحالة، يمكنك استدعاء server.wait_for_termination()
لحظر سلسلة استدعاء الدوال البرمجية بشكل كامل إلى أن يتم إيقاف الخادم.
6. إنشاء العميل
لنلقِ نظرة على route_guide_client.py
.
إنشاء مقالة موجزة
لاستدعاء طرق الخدمة، يجب أولاً إنشاء رمز بديل.
ننشئ مثيلاً لفئة RouteGuideStub
من وحدة route_guide_pb2_grpc
، تم إنشاؤها من طريقة .proto.
In run()
:
with grpc.insecure_channel("localhost:50051") as channel:
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
يُرجى العِلم أنّه يتم استخدام channel
هنا كأداة لإدارة السياق، وسيتم إغلاقها تلقائيًا بمجرد أن يغادر المترجم كتلة with
.
طُرق خدمة المكالمات
بالنسبة إلى طرق RPC التي تعرض استجابة واحدة (طرق "response-unary")، يتيح gRPC Python دلالات التحكّم المتزامنة (الحظر) وغير المتزامنة (عدم الحظر) في تدفّق التحكّم. بالنسبة إلى طرق RPC التي تتضمّن بث الاستجابة، تُرجع الطلبات على الفور مكرّرًا لقيم الاستجابة. يتم حظر طلبات إلى كتلة طريقة next()
الخاصة بهذا المكرّر إلى أن يصبح الرد الذي سيتم إنشاؤه من المكرّر متاحًا.
Server-side streaming RPC
يشبه استدعاء ListFeatures
لبث الردود طريقة التعامل مع أنواع التسلسلات:
def guide_list_features(stub):
_lo = route_guide_pb2.Point(latitude=400000000, longitude=-750000000)
_hi = route_guide_pb2.Point(latitude=420000000, longitude=-730000000)
rectangle = route_guide_pb2.Rectangle(
lo=_lo,
hi=_hi,
)
print("Looking for features between 40, -75 and 42, -73")
features = stub.ListFeatures(rectangle)
for feature in features:
print(
f"Feature called '{feature.name}'"
f" at {format_point(feature.location)}"
)
Client-side streaming RPC
يشبه استدعاء RecordRoute
لطلب البث تمرير مكرّر إلى طريقة محلية. كما هو الحال مع RPC البسيط أعلاه الذي يعرض أيضًا ردًا واحدًا، يمكن استدعاؤه بشكل متزامن:
def guide_record_route(stub):
feature_list = route_guide_resources.read_route_guide_database()
route_iterator = generate_route(feature_list)
route_summary = stub.RecordRoute(route_iterator)
print(f"Finished trip with {route_summary.point_count} points")
print(f"Passed {route_summary.feature_count} features")
print(f"Traveled {route_summary.distance} meters")
print(f"It took {route_summary.elapsed_time} seconds")
استدعاء إجراء عن بُعد (RPC) ثنائي الاتجاه
يحتوي استدعاء RouteChat
ثنائي الاتجاه (كما هو الحال على جهة الخادم) على مجموعة من دلالات بث الطلب وبث الرد.
أنشئ رسائل الطلبات وأرسِلها واحدةً تلو الأخرى باستخدام yield
.
def generate_notes():
home = route_guide_pb2.Point(latitude=1, longitude=1)
work = route_guide_pb2.Point(latitude=2, longitude=2)
notes = [
make_route_note("Departing from home", home),
make_route_note("Arrived at work", work),
make_route_note("Having lunch at work", work),
make_route_note("Departing from work", work),
make_route_note("Arrived home", home),
]
for note in notes:
print(
f"Sending RouteNote for {format_point(note.location)}:"
f" {note.message}"
)
yield note
# Sleep to simulate moving from one point to another.
# Only for demonstrating the order of the messages.
time.sleep(0.1)
تلقّي استجابات الخادم ومعالجتها:
def guide_route_chat(stub):
responses = stub.RouteChat(generate_notes())
for response in responses:
print(
"< Found previous note at"
f" {format_point(response.location)}: {response.message}"
)
استدعاء طرق المساعدة
في run، نفِّذ الطرق التي أنشأناها للتو، ومرِّر إليها stub
.
print("-------------- ListFeatures --------------")
guide_list_features(stub)
print("-------------- RecordRoute --------------")
guide_record_route(stub)
print("-------------- RouteChat --------------")
guide_route_chat(stub)
7. جرّبه الآن
شغِّل الخادم:
python route_guide_server.py
من وحدة طرفية مختلفة، فعِّل البيئة الافتراضية مرة أخرى (source .venv/bin/activate)
)، ثم شغِّل العميل:
python route_guide_client.py
لنلقِ نظرة على الناتج.
ListFeatures
أولاً، ستجد قائمة الميزات. يتم بث كل ميزة من الخادم (RPC للبث من جهة الخادم) عند اكتشاف أنّها تقع ضمن المستطيل المطلوب:
-------------- ListFeatures -------------- Looking for features between 40, -75 and 42, -73 Feature called 'Patriots Path, Mendham, NJ 07945, USA' at (lat=407838351, lng=-746143763) Feature called '101 New Jersey 10, Whippany, NJ 07981, USA' at (lat=408122808, lng=-743999179) Feature called 'U.S. 6, Shohola, PA 18458, USA' at (lat=413628156, lng=-749015468) Feature called '5 Conners Road, Kingston, NY 12401, USA' at (lat=419999544, lng=-740371136) ...
RecordRoute
ثانيًا، يعرض RecordRoute
قائمة بالنقاط التي تمت زيارتها بشكل عشوائي والتي يتم بثها من العميل إلى الخادم (بث RPC من جهة العميل):
-------------- RecordRoute -------------- Visiting point (lat=410395868, lng=-744972325) Visiting point (lat=404310607, lng=-740282632) Visiting point (lat=403966326, lng=-748519297) Visiting point (lat=407586880, lng=-741670168) Visiting point (lat=406589790, lng=-743560121) Visiting point (lat=410322033, lng=-747871659) Visiting point (lat=415464475, lng=-747175374) Visiting point (lat=407586880, lng=-741670168) Visiting point (lat=402647019, lng=-747071791) Visiting point (lat=414638017, lng=-745957854)
بعد أن ينتهي العميل من بث جميع النقاط التي تمت زيارتها، سيتلقّى ردًا غير متعلّق بالبث (طلب إجراء مكالمة RPC أحادية) من الخادم. سيتضمّن هذا الردّ ملخّصًا للحسابات التي تم إجراؤها على المسار الكامل للعميل.
Finished trip with 10 points Passed 10 features Traveled 654743 meters It took 0 seconds
RouteChat
أخيرًا، يوضّح الناتج RouteChat
البث ثنائي الاتجاه. عندما "يزور" العميل النقطتَين home
أو work
، يسجّل ملاحظة للنقطة عن طريق إرسال RouteNote إلى الخادم. عندما تتم زيارة نقطة سبقًا، يعرض الخادم جميع الملاحظات السابقة لهذه النقطة.
-------------- RouteChat -------------- Sending RouteNote for (lat=1, lng=1): Departing from home Sending RouteNote for (lat=2, lng=2): Arrived at work Sending RouteNote for (lat=2, lng=2): Having lunch at work < Found previous note at (lat=2, lng=2): Arrived at work Sending RouteNote for (lat=2, lng=2): Departing from work < Found previous note at (lat=2, lng=2): Arrived at work < Found previous note at (lat=2, lng=2): Having lunch at work Sending RouteNote for (lat=1, lng=1): Arrived home < Found previous note at (lat=1, lng=1): Departing from home
8. الخطوات التالية
- تعرَّف على طريقة عمل gRPC في مقدمة عن gRPC والمفاهيم الأساسية
- اتّبِع الخطوات الواردة في الدليل التعليمي الخاص بالأساسيات
- استكشاف مرجع Python API