مرحبا بالجميع. سنلقي نظرة اليوم على كيفية ربط إطار عمل gRPC في C ++ ومكتبة Qt. توفر المقالة رمزًا يلخص استخدام جميع أوضاع التفاعل الأربعة في gRPC. بالإضافة إلى ذلك ، يتم توفير رمز يسمح باستخدام gRPC من خلال إشارات Qt وفتحات. قد تكون المقالة ذات أهمية في المقام الأول لمطوري Qt المهتمين باستخدام gRPC. ومع ذلك ، فإن تعميم أوضاع التشغيل الأربعة لـ gRPC مكتوب بلغة C ++ بدون استخدام Qt ، مما سيسمح للمطورين غير المرتبطين بـ Qt بتكييف الشفرة. أطلب من جميع المهتمين تحت القط.
الخلفية
منذ حوالي ستة أشهر ، تم تعليق مشروعين عليّ ، باستخدام أجزاء العميل والخادم في gRPC. انخفض كلا المشروعين في الإنتاج. تمت كتابة هذه المشاريع من قبل المطورين الذين استقالوا بالفعل. كان الخبر السار الوحيد هو أنني شاركت بنشاط في كتابة خادم gRPC ورمز العميل. لكن ذلك كان قبل نحو عام. لذلك ، كالعادة ، كان علي التعامل مع كل شيء من الصفر.
تمت كتابة رمز خادم gRPC مع توقع أنه سيتم إنشاؤه أكثر بواسطة ملف .proto. تمت كتابة الرمز بشكل جيد. ومع ذلك ، كان لدى الخادم عيب واحد كبير: يمكن أن يتصل به عميل واحد فقط.
تمت كتابة عميل gRPC فظيعًا.
اكتشفت gRPC لرمز العميل والخادم بعد بضعة أيام فقط. وأدركت أنه إذا أخذت مشروعًا لبضعة أسابيع ، فسيتعين علي التعامل مع الخادم وعميل gRPC مرة أخرى.
ثم قررت أن الوقت قد حان لكتابة وتصحيح عميل وخادم gRPC بحيث:
يمكنك النوم بسلام في الليل.
لم تكن هناك حاجة لتذكر كيفية عمل ذلك في كل مرة تحتاج فيها إلى كتابة عميل أو خادم gRPC ؛
يمكنك استخدام عميل وخادم gRPC المكتوب في مشاريع أخرى.
عند كتابة التعليمات البرمجية ، استرشدت بالمتطلبات التالية:
يمكن لكل من عميل وخادم gRPC العمل باستخدام إشارات وفتحات مكتبة Qt بطريقة طبيعية ؛
لا يحتاج رمز العميل والخادم gRPC إلى الإصلاح عند تغيير ملف .proto ؛
يجب أن يكون عميل gRPC قادرًا على إخبار رمز العميل بحالة الاتصال بالخادم.
هيكل المقالة على النحو التالي. أولاً ، ستكون هناك لمحة موجزة عن نتائج العمل مع رمز العميل وبعض التفسيرات له. في نهاية المراجعة ، رابط إلى المستودع. علاوة على ذلك ، ستكون هناك أشياء عامة في الهندسة المعمارية. ثم وصف الخادم والعميل رمز (ما تحت غطاء محرك السيارة) واستنتاج.
مراجعة قصيرة
تم استخدام أبسط ملف pingproto.proto كملف .proto ، حيث تم تحديد RPCs لجميع أنواع التفاعل:
syntax = "proto3"; package pingpong; service ping { rpc SayHello (PingRequest) returns (PingReply) {} rpc GladToSeeMe(PingRequest) returns (stream PingReply){} rpc GladToSeeYou(stream PingRequest) returns (PingReply){} rpc BothGladToSee(stream PingRequest) returns (stream PingReply){} } message PingRequest { string name = 1; string message = 2; } message PingReply { string message = 1; }
يكرر ملف pingpong.proto ملف helloworld.proto من المقالة حول أوضاع gRPC غير المتزامنة في C ++ إلى الاسم الدقيق.
ونتيجة لذلك ، يمكن استخدام خادم مكتوب على النحو التالي:
class A: public QObject { Q_OBJECT; QpingServerService pingservice; public: A() { bool is_ok; is_ok = connect(&pingservice, SIGNAL(SayHelloRequest(SayHelloCallData*)), this, SLOT(onSayHello(SayHelloCallData*))); assert(is_ok); is_ok = connect(&pingservice, SIGNAL(GladToSeeMeRequest(GladToSeeMeCallData*)), this, SLOT(onGladToSeeMe(GladToSeeMeCallData*))); assert(is_ok); is_ok = connect(&pingservice, SIGNAL(GladToSeeYouRequest(GladToSeeYouCallData*)), this, SLOT(onGladToSeeYou(GladToSeeYouCallData*))); assert(is_ok); is_ok = connect(&pingservice, SIGNAL(BothGladToSeeRequest(BothGladToSeeCallData*)), this, SLOT(onBothGladToSee(BothGladToSeeCallData*))); assert(is_ok); } public slots: void onSayHello(SayHelloCallData* cd) { std::cout << "[" << cd->peer() << "][11]: request: " << cd->request.name() << std::endl; cd->reply.set_message("hello " + cd->request.name()); cd->Finish(); }
عندما يستدعي عميل RPC ، يخطر خادم gRPC رمز العميل (في هذه الحالة ، الفئة A) بالإشارة المناسبة.
يمكن استخدام عميل gRPC على النحو التالي:
class B : public QObject { Q_OBJECT QpingClientService pingPongSrv; public: B() { bool c = false; c = connect(&pingPongSrv, SIGNAL(SayHelloResponse(SayHelloCallData*)), this, SLOT(onSayHelloResponse(SayHelloCallData*))); assert(c); c = connect(&pingPongSrv, SIGNAL(GladToSeeMeResponse(GladToSeeMeCallData*)), this, SLOT(onGladToSeeMeResponse(GladToSeeMeCallData*))); assert(c); c = connect(&pingPongSrv, SIGNAL(GladToSeeYouResponse(GladToSeeYouCallData*)), this, SLOT(onGladToSeeYouResponse(GladToSeeYouCallData*))); assert(c); c = connect(&pingPongSrv, SIGNAL(BothGladToSeeResponse(BothGladToSeeCallData*)), this, SLOT(onBothGladToSeeResponse(BothGladToSeeCallData*))); assert(c); c = connect(&pingPongSrv, SIGNAL(channelStateChanged(int, int)), this, SLOT(onPingPongStateChanged(int, int))); assert(c); } void usage() { //Unary PingRequest request; request.set_name("user"); request.set_message("user"); pingPongSrv.SayHello(request); //Server streaming PingRequest request2; request2.set_name("user"); pingPongSrv.GladToSeeMe(request2); //etc. } public slots: void SayHelloResponse(SayHelloCallData* response) { std::cout << "[11]: reply: " << response->reply.message() << std::endl; if (response->CouldBeDeleted()) delete response; } //etc. };
يسمح لك عميل gRPC بالاتصال بـ RPC مباشرة والاشتراك في استجابة الخادم باستخدام الإشارات المناسبة.
يحتوي عميل gRPC أيضًا على إشارة:
channelStateChanged(int, int);
الذي يقدم تقارير عن حالات اتصال الخادم السابقة والحالية. كل كود
عينة موجود في
مستودع qgrpc .
كيف يعمل
يظهر مبدأ تضمين العميل وخادم gRPC في المشروع في الشكل.
في ملف مشروع .pro ، يتم تحديد ملفات .proto ، والتي ستعمل على أساسها gRPC. يحتوي ملف grpc.pri على أوامر لإنشاء ملفات gRPC و QgRPC. ينشئ المترجم protoc ملفات gRPC [protofile] .grpc.pb.h و [protofile] .grpc.pb.cc. [protofile] هو اسم ملف .proto الذي تم تمريره إلى إدخال المترجم.
يتم معالجة إنشاء ملفات QgRPC [protofile] .qgrpc. [Config] .h بواسطة النص البرمجي genQGrpc.py. [config] هو إما "خادم" أو "عميل".
تحتوي ملفات QgRPC التي تم إنشاؤها على غلاف Qt حول فئات gRPC والمكالمات ذات الإشارات المقابلة. في الأمثلة السابقة ، يتم الإعلان عن فئتي QpingServerService و QpingClientService على التوالي في الملفات التي تم إنشاؤها pingpong.qgrpc.server.h و pingpong.qgrpc.client.h. تتم إضافة ملفات QgRPC التي تم إنشاؤها إلى معالجة moc.
في ملفات QgRPC التي تم إنشاؤها ، يتم تضمين ملفات Qgrpc [config] .h ، والتي تتم فيها جميع الأعمال الرئيسية. اقرأ المزيد عن هذا أدناه.
لربط كل هذا البناء بالمشروع ، تحتاج إلى تضمين ملف grpc.pri في ملف .pro للمشروع وتحديد ثلاثة متغيرات. يعرّف متغير GRPC ملفات .proto التي سيتم نقلها إلى مدخلات المترجم البرمجي والبرنامج النصي genQGrpc.py. يحدد متغير QGRPC_CONFIG قيمة التكوين لملفات QgRPC التي تم إنشاؤها وقد يحتوي على القيم "server" أو "client". يمكنك أيضًا تحديد متغير GRPC_VERSION الاختياري للإشارة إلى إصدار gRPC.
لمزيد من المعلومات حول كل ما قيل ، اقرأ ملف grpc.pri وملفات أمثلة .pro.
هندسة الخادم
يظهر الرسم التخطيطي لفئة الخادم في الشكل.
تُظهر الأسهم السميكة التسلسل الهرمي للميراث الطبقي ، بينما تُظهر الأسهم الرفيعة عضوية الأعضاء والأساليب في الفصول الدراسية. بشكل عام ، يتم إنشاء فئة Q [servicename] ServerService للخدمة ، حيث servicename هو اسم الخدمة المعلنة في ملف .proto. RPCCallData هي هياكل تحكم تم إنشاؤها لكل RPC في الخدمة. في مُنشئ فئة QpingServerService ، تتم تهيئة الفئة الأساسية QGrpcServerService مع خدمة gRPC pingpong :: ping :: AsyncService غير المتزامنة. لبدء الخدمة ، تحتاج إلى استدعاء الأسلوب Start () بالعنوان والمنفذ الذي سيتم تشغيل الخدمة عليه. تطبق الوظيفة Start () الإجراء القياسي لبدء الخدمة.
في نهاية دالة Start () ، يتم استدعاء الدالة الافتراضية الخالصة makeRequests () ، والتي يتم تنفيذها في فئة QpingServerService التي تم إنشاؤها:
void makeRequests() { needAnotherCallData< SayHello_RPCtypes, SayHelloCallData >(); needAnotherCallData< GladToSeeMe_RPCtypes, GladToSeeMeCallData >(); needAnotherCallData< GladToSeeYou_RPCtypes, GladToSeeYouCallData >(); needAnotherCallData< BothGladToSee_RPCtypes, BothGladToSeeCallData >(); }
معلمة القالب الثاني للدالة needAnotherCallData هي هياكل RPCCallData التي تم إنشاؤها. نفس الهياكل هي معلمات الإشارات في فئة Qt المولدة للخدمة.
ترث بنيات RPCCallData التي تم إنشاؤها من فئة ServerCallData. في المقابل ، يتم توريث فئة ServerCallData من المستجيب ServerResponder. وبالتالي ، يؤدي إنشاء كائن من الهياكل المتماسكة إلى إنشاء كائن مستجيب.
يأخذ المُنشئ لفئة ServerCallData معلمتين: signal_func و request_func. signal_func هي إشارة مولدة يتم استدعاؤها بعد تلقي علامة من قائمة الانتظار. request_func هي وظيفة يجب استدعاؤها عند إنشاء مستجيب جديد. على سبيل المثال ، في هذه الحالة قد تكون الوظيفة RequestSayHello (). يحدث طلب request_func في دالة needAnotherCallData (). يتم ذلك بحيث تتم إدارة المستجيبين (الإنشاء والحذف) في الخدمة.
يتكون رمز وظيفة needAnotherCallData () من إنشاء كائن مستجيب واستدعاء وظيفة تربط المستجيب باستدعاء RPC:
template<class RPCCallData, class RPCTypes> void needAnotherCallData() { RPCCallData* cd = new RPCCallData();
دالات RequestRPC () هي وظائف قالب لأربعة أنواع من التفاعل. نتيجة لذلك ، يتلخص استدعاء RequestRPC () في مكالمة:
service_->(cd->request_func_)(...,cd->responder, (void*)cd)
حيث service_ هي خدمة gRPC. في هذه الحالة ، يكون pingpong :: ping :: AsyncService.
للتحقق من قائمة انتظار الأحداث بشكل متزامن أو غير متزامن ، يجب استدعاء الدالتين CheckCQ () أو AsyncCheckCQ () ، على التوالي. ينزل رمز وظيفة CheckCQ () إلى المكالمات إلى العلامة المتزامنة من قائمة الانتظار ومعالجة هذه العلامة:
virtual void CheckCQ() override { void* tag; bool ok; server_cq_->Next(&tag, &ok); //tagActions_ call if (!tag) return; AbstractCallData* cd = (AbstractCallData*)tag; if (!started_.load()) { destroyCallData(cd); return; } cd->cqReaction(this, ok); }
بعد استلام العلامة من قائمة الانتظار ، يتم التحقق من صحة العلامة وبدء الخادم. إذا تم إيقاف تشغيل الخادم ، فلن تكون العلامة مطلوبة بعد ذلك - يمكن حذفها. بعد ذلك ، تسمى الدالة cqReaction () المعرفة في فئة ServerCallData:
void cqReaction(const QGrpcServerService* service_, bool ok) { if (!first_time_reaction_) { first_time_reaction_ = true; service_->needAnotherCallData<RPC, RPCCallData>(); } auto genRpcCallData = dynamic_cast<RPCCallData*>(this); void* tag = static_cast<void*>(genRpcCallData); if (this->CouldBeDeleted()) { service_->destroyCallData(this); return; } if (!this->processEvent(tag, ok)) return;
تشير إشارة first_time_reaction_ إلى أنك بحاجة إلى إنشاء مجيب جديد لـ RPC المسمى. يتم توريث الدالتين DoesBeDeleted () و ProcessEvent () من فئة المستجيب ServerResponder المقابل. ترجع الدالة mightBeDeleted () علامة على أنه قد يتم حذف كائن المستجيب. تعالج الدالة processEvent () العلامة وعلامة موافق. لذلك ، على سبيل المثال ، بالنسبة إلى مستجيب نوع تدفق العميل ، تبدو الوظيفة كما يلي:
bool processEvent(void* tag, bool ok) { this->tag_ = tag; read_mode_ = ok; return true; }
ترجع الدالة ProcessEvent () ، بغض النظر عن نوع المجيب ، true. يتم ترك القيمة المرجعة لهذه الوظيفة للتمديد المحتمل للوظيفة ، ونظريًا ، لإزالة الأخطاء.
بعد معالجة الحدث ، يتبع المكالمة:
service_->(*signal_func_)(genRpcCallData)
المتغير service_ هو مثيل للخدمة المولدة ، في حالتنا QpingServerService. المتغير signal_func_ عبارة عن إشارة خدمة تتوافق مع RPC محدد. على سبيل المثال ، SayHelloRequest (). إن genRpcCallData المتغير هو كائن مستجيب من النوع المقابل. من وجهة نظر رمز الاستدعاء ، يعد genRpcCallData المتغير كائنًا من إحدى هياكل RPCCallData التي تم إنشاؤها.
هندسة العملاء
كلما أمكن ، تتطابق أسماء فئات ووظائف العميل مع أسماء فئات ووظائف الخادم. يظهر الرسم التخطيطي لفئة العميل في الشكل.
تُظهر الأسهم السميكة التسلسل الهرمي للميراث الطبقي ، بينما تُظهر الأسهم الرفيعة عضوية الأعضاء والأساليب في الفصول الدراسية. بشكل عام ، بالنسبة للخدمة ، يتم إنشاء فئة ClientService من فئة Q [servicename] ، حيث يكون servicename هو اسم الخدمة المعلنة في ملف .proto. RPCCallData هي هياكل تحكم تم إنشاؤها لكل RPC في الخدمة. لاستدعاء RPC ، توفر الفئة التي تم إنشاؤها وظائف تتطابق أسماؤها تمامًا مع RPC المعلن في ملف .proto. في مثالنا ، في ملف .proto RPC ، يتم التصريح عن SayHello () على أنه:
rpc SayHello (PingRequest) returns (PingReply) {}
في فئة QpingClientService التي تم إنشاؤها ، تبدو دالة RPC المقابلة كما يلي:
void SayHello(PingRequest request) { if(!connected()) return; SayHelloCallData* call = new SayHelloCallData; call->request = request; call->responder = stub_->AsyncSayHello(&call->context, request, &cq_); call->responder->Finish(&call->reply, &call->status, (void*)call); }
يتم في النهاية وراثة هياكل RPCCallData التي تم إنشاؤها ، كما في حالة الخادم ، من فئة ClientResponder. لذلك ، يؤدي إنشاء كائن من البنية المولدة إلى إنشاء مستجيب. بعد إنشاء المستجيب ، يتم استدعاء RPC ويرتبط المستجيب بحدث تلقي استجابة من الخادم. فيما يتعلق برمز العميل ، تبدو مكالمة RPC كما يلي:
void ToSayHello() { PingRequest request; request.set_name("user"); request.set_message("user"); pingPongSrv.SayHello(request); }
بخلاف فئة خادم QpingServerService المولدة ، ترث فئة QpingClientService من فئتي قالب: ConnectivityFeatures و MonitorFeatures.
إن فئة ConnectivityFeatures مسؤولة عن حالة اتصال العميل بالخادم وتوفر ثلاث وظائف للاستخدام: grpc_connect () و grpc_disconnect () و grpc_reconnect (). تزيل وظيفة grpc_disconnect () ببساطة جميع هياكل البيانات المسؤولة عن التفاعل مع الخادم. يتم تقليل الاستدعاء إلى grpc_connect إلى الاستدعاءات للدالة grpc_connect_ () ، التي تنشئ هياكل بيانات التحكم:
void grpc_connect_() { channel_ = grpc::CreateChannel(target_, creds_); stub_ = GRPCService::NewStub(channel_); channelFeatures_ = std::make_unique<ChannelFeatures>(channel_); channelFeatures_->checkChannelState(); }
تراقب فئة ChannelFeatures حالة اتصال القناة_ بالقناة. فئة ConnectivityFeatures تغلف كائنًا من فئة ChannelFeatures وتنفذ الوظائف المجردة channelState () و checkChannelState () ومتصلة () باستخدام هذا الكائن. تعرض الدالة channelState () آخر حالة تمت ملاحظتها لقناة الاتصال بالخادم. ترجع الدالة checkChannelState () ، في الواقع ، الحالة الحالية للقناة. تُرجع الدالة () المتصلة علامة العميل المتصل بالخادم.
إن فئة MonitorFeatures مسؤولة عن استقبال ومعالجة الأحداث من الخادم وتوفر وظيفة CheckCQ () للاستخدام:
bool CheckCQ() { auto service_ = dynamic_cast< SERVICE* >(this); //connection state auto old_state = conn_->channelState(); auto new_state = conn_->checkChannelState(); if (old_state != new_state) service->*channelStateChangedSignal_(old_state, new_state); //end of connection state void* tag; bool ok = false; grpc::CompletionQueue::NextStatus st; st = cq_.AsyncNext(&tag, &ok, deadlineFromMSec(100)); if ((st == grpc::CompletionQueue::SHUTDOWN) || (st == grpc::CompletionQueue::TIMEOUT)) return false; (AbstractCallData< SERVICE >*)(tag)->cqActions(service_, ok); return true; }
هيكل الكود هو نفسه كما في حالة الخادم. على عكس الخادم ، تتم إضافة كتلة التعليمات البرمجية المسؤولة عن معالجة الحالة الحالية إلى العميل. إذا تغيرت حالة قناة الاتصال ، يتم استدعاء قناة الإشارةStateChangedSignal_ (). في جميع الخدمات المولدة ، هذه إشارة:
void channelStateChanged(int, int);
أيضًا ، على عكس الخادم ، يتم استخدام وظيفة AsyncNext () هنا بدلاً من Next (). وقد تم ذلك لعدة أسباب. أولاً ، عند استخدام AsyncNext () ، فإن كود العميل لديه القدرة على التعرف على التغيير في حالة قناة الاتصال. ثانيًا ، عند استخدام AsyncNext () ، من الممكن استدعاء RPCs مختلفة في رمز العميل في أي عدد من المرات. سيؤدي استخدام الوظيفة Next () في هذه الحالة إلى حظر مؤشر الترابط حتى يتم استلام حدث من قائمة الانتظار ، ونتيجة لذلك ، سيفقد السمتين الموصوفتين.
بعد استلام الحدث من قائمة الانتظار ، كما في حالة الخادم ، تُسمى دالة cqReaction () ، المحددة في فئة ClientCallData ، بما يلي:
void cqActions(RPC::Service* service, bool ok) { auto response = dynamic_cast<RPCCallData*>(this); void* tag = static_cast<void*>(response); if (!this->processEvent(tag, ok)) return; service->*func_( response ); }
كما هو الحال مع الخادم ، تقوم الدالة processEvent () بمعالجة العلامة وعلامة موافق وتقوم دائمًا بإرجاع true. كما في حالة الخادم ، بعد معالجة الحدث ، يجب استدعاء إشارة الخدمة التي تم إنشاؤها. ومع ذلك ، هناك اختلافان مهمان عن وظيفة الخادم التي تحمل الاسم نفسه. الاختلاف الأول هو أنه لا يتم إنشاء المستجيبين في هذه الوظيفة. يحدث إنشاء المستجيبين ، كما هو موضح أعلاه ، عندما يتم استدعاء RPC. والفرق الثاني هو أنه لا يتم حذف المستجيبين في هذه الوظيفة. يتم غياب إبعاد المستجيبين لسببين. أولاً ، يمكن أن يستخدم رمز العميل مؤشرات لإنشاء هياكل RPCCallData لأغراضها الخاصة. يمكن أن يؤدي حذف المحتوى بواسطة هذا المؤشر ، المخفي عن رمز العميل ، إلى عواقب غير سارة. ثانيًا ، ستؤدي إزالة المستجيب إلى حقيقة أنه لن يتم إنشاء إشارة بالبيانات. لذلك ، لن يتلقى رمز العميل رسالة الخادم الأخيرة. من بين العديد من البدائل لحل المشاكل المشار إليها ، تم اتخاذ قرار بتحويل إزالة المستجيب (الهياكل المولدة) إلى رمز العميل. وبالتالي ، يجب أن تحتوي وظائف معالج الإشارة (الفتحات) على الكود التالي:
void ResponseHandler(RPCCallData* response) { if (response->CouldBeDeleted()) delete response;
لن يؤدي عدم إزالة المستجيب في رمز العميل إلى حدوث تسرب للذاكرة فحسب ، بل سيؤدي أيضًا إلى مشاكل محتملة في قناة الاتصال. يتم تنفيذ معالجات الإشارة لجميع أنواع تفاعلات RPC في نموذج التعليمات البرمجية.
الخلاصة
في الختام نلفت الانتباه إلى نقطتين. تتعلق النقطة الأولى باستدعاء وظائف CheckCQ () للعميل والخادم. تعمل ، كما هو موضح أعلاه ، وفقًا لمبدأ واحد: إذا كان هناك حدث في قائمة الانتظار ، يتم "إرسال" إشارة مع بنية RPCCallData المولدة المقابلة. يمكنك استدعاء هذه الوظيفة يدويًا والتحقق من وجود حدث (في حالة وجود عميل). ولكن في البداية كانت هناك فكرة لنقل جزء الشبكة بالكامل المرتبط بـ gRPC إلى سلسلة محادثات أخرى. لهذه الأغراض ، تمت كتابة الفئات المساعدة QGrpcSrvMonitor لخادم gRPC و QGrpcCliServer لعميل gRPC. يعمل كلا الفصلين على نفس المبدأ: يقومان بإنشاء دفق منفصل ، ووضع الخدمة التي تم إنشاؤها في هذا الدفق واستدعاء وظيفة CheckCQ () لهذه الخدمة بشكل دوري. وبالتالي ، عند استخدام كل من الفئات المساعدة ، ليست هناك حاجة لاستدعاء وظائف CheckCQ () في رمز العميل. إشارات الخدمة المولدة ، في هذه الحالة ، "تأتي" من تيار آخر. يتم تنفيذ أمثلة العميل والخادم باستخدام هذه الفئات المساعدة.
النقطة الثانية تتعلق بغالبية المطورين الذين لا يستخدمون مكتبة Qt في عملهم. يتم استخدام فئات Qt ووحدات الماكرو في QgRPC في مكانين فقط: في ملفات الخدمة التي تم إنشاؤها ، وفي الملفات التي تحتوي على فئات مساعدة: QGrpcServerMonitor.h و QGrpcClientMonitor.h. الملفات المتبقية في مكتبة Qt ليست مرتبطة بأي شكل من الأشكال. تم التخطيط لإضافة التجميع باستخدام cmake ، ولتحديد بعض توجيهات Qt. على وجه الخصوص ، فئة QObject وماكرو Q_OBJECT. لكن الأيدي لم تصل إلى هذا. لذلك ، نرحب بأي اقتراحات.
هذا كل شيء. شكرا لكم جميعا!
المراجع