Halo semuanya. Hari ini kita akan melihat bagaimana Anda dapat menautkan kerangka kerja gRPC di C ++ dan perpustakaan Qt. Artikel ini menyediakan kode yang merangkum penggunaan keempat mode interaksi di gRPC. Selain itu, disediakan kode yang memungkinkan penggunaan gRPC melalui sinyal dan slot Qt. Artikel ini mungkin menarik terutama untuk pengembang Qt yang tertarik menggunakan gRPC. Namun demikian, generalisasi dari empat mode operasi gRPC ditulis dalam C ++ tanpa menggunakan Qt, yang akan memungkinkan pengembang yang tidak terkait dengan Qt untuk mengadaptasi kode. Saya meminta semua orang tertarik pada kucing.
Latar belakang
Sekitar enam bulan yang lalu, dua proyek tergantung pada saya, menggunakan bagian klien dan server gRPC. Kedua proyek jatuh dalam produksi. Proyek-proyek ini ditulis oleh pengembang yang sudah berhenti. Satu-satunya kabar baik adalah saya mengambil bagian aktif dalam menulis server gRPC dan kode klien. Tapi itu sekitar setahun yang lalu. Karena itu, seperti biasa, saya harus berurusan dengan semuanya dari awal.
Kode server gRPC ditulis dengan harapan akan dibuat lebih lanjut oleh file .proto. Kode itu ditulis dengan baik. Namun, server memiliki satu kelemahan besar: hanya satu klien yang dapat terhubung.
Klien gRPC ditulis sangat buruk.
Saya menemukan kode klien dan server gRPC hanya beberapa hari kemudian. Dan saya menyadari bahwa jika saya mengambil proyek selama beberapa minggu, saya harus berurusan dengan server dan klien gRPC lagi.
Saat itulah saya memutuskan bahwa sudah waktunya untuk menulis dan men-debug klien dan server gRPC sehingga:
Anda bisa tidur nyenyak di malam hari;
Tidak perlu mengingat cara kerjanya setiap kali Anda perlu menulis klien atau server gRPC;
Anda dapat menggunakan klien dan server gRPC tertulis di proyek lain.
Saat menulis kode, saya dipandu oleh persyaratan berikut:
Baik klien dan server gRPC dapat beroperasi menggunakan sinyal dan slot pustaka Qt secara alami;
Kode klien dan server gRPC tidak perlu diperbaiki ketika mengubah file .proto;
Klien gRPC harus dapat memberi tahu kode klien status koneksi ke server.
Struktur artikel adalah sebagai berikut. Pertama, akan ada ikhtisar singkat tentang hasil bekerja dengan kode klien dan penjelasan kecil untuk itu. Di akhir ulasan, tautan ke repositori. Selanjutnya akan ada hal-hal umum pada arsitektur. Kemudian deskripsi kode server dan klien (apa yang ada di balik tenda) dan sebuah kesimpulan.
Ulasan singkat
File pingproto.proto paling sederhana digunakan sebagai file .proto, di mana RPC dari semua jenis interaksi didefinisikan:
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; }
File pingpong.proto mengulangi file helloworld.proto dari artikel tentang mode gRPC asinkron di C ++ dengan nama yang tepat.
Akibatnya, server tertulis dapat digunakan seperti ini:
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(); }
Ketika klien memanggil RPC, server gRPC memberitahukan kode klien (dalam hal ini, kelas A) dengan sinyal yang sesuai.
Klien gRPC dapat digunakan seperti ini:
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. };
Klien gRPC memungkinkan Anda untuk memanggil RPC secara langsung, dan berlangganan respons server menggunakan sinyal yang sesuai.
Klien gRPC juga memiliki sinyal:
channelStateChanged(int, int);
yang melaporkan status koneksi server masa lalu dan saat ini. Semua kode
sampel ada di
dalam repositori qgrpc .
Bagaimana cara kerjanya
Prinsip menyertakan klien dan server gRPC dalam proyek ditunjukkan pada gambar.
Dalam file proyek .pro, file .proto ditentukan, berdasarkan gRPC mana yang akan bekerja. File grpc.pri berisi perintah untuk membuat file gRPC dan QgRPC. Kompilator protoc menghasilkan file gRPC [protofile] .grpc.pb.h dan [protofile] .grpc.pb.cc. [protofile] adalah nama file .proto yang diteruskan ke input kompiler.
Pembuatan file QgRPC [protofile] .qgrpc. [Config] .h ditangani oleh skrip genQGrpc.py. [config] adalah "server" atau "klien".
File QgRPC yang dihasilkan berisi pembungkus Qt di sekitar kelas dan panggilan gRPC dengan sinyal yang sesuai. Dalam contoh sebelumnya, kelas QpingServerService dan QpingClientService dideklarasikan dalam file yang dihasilkan pingpong.qgrpc.server.h dan pingpong.qgrpc.client.h masing-masing. File QgRPC yang dihasilkan ditambahkan ke pemrosesan moc.
Dalam file QgRPC yang dihasilkan, file [config] .h QGrpc disertakan, di mana semua pekerjaan utama dilakukan. Baca lebih lanjut tentang ini di bawah ini.
Untuk menghubungkan semua konstruksi ini ke proyek, Anda perlu memasukkan file grpc.pri dalam file propro proyek dan menentukan tiga variabel. Variabel GRPC mendefinisikan file .proto yang akan ditransfer ke input dari kompiler protoc dan skrip genQGrpc.py. Variabel QGRPC_CONFIG mendefinisikan nilai konfigurasi dari file QgRPC yang dihasilkan dan mungkin berisi nilai "server" atau "klien". Anda juga dapat menentukan variabel GRPC_VERSION opsional untuk menunjukkan versi gRPC.
Untuk informasi lebih lanjut tentang semua yang dikatakan, baca file grpc.pri dan file .pro contoh.
Arsitektur server
Diagram kelas server ditunjukkan pada gambar.
Panah tebal menunjukkan hierarki warisan kelas, dan panah tipis menunjukkan keanggotaan anggota dan metode di kelas. Secara umum, kelas Q [servicename] ServerService dihasilkan untuk layanan, di mana servicename adalah nama layanan yang dinyatakan dalam file .proto. RPCCallData adalah struktur kontrol yang dihasilkan untuk setiap RPC dalam layanan. Dalam konstruktor kelas QpingServerService, kelas dasar QGrpcServerService diinisialisasi dengan layanan pingRong gpPC :: ping :: AsyncService. Untuk memulai layanan, Anda perlu memanggil metode Mulai () dengan alamat dan port tempat layanan akan dijalankan. Fungsi Start () mengimplementasikan prosedur standar untuk memulai layanan.
Pada akhir fungsi Start (), fungsi virtual murni makeRequests () dipanggil, yang diimplementasikan dalam kelas QpingServerService yang dihasilkan:
void makeRequests() { needAnotherCallData< SayHello_RPCtypes, SayHelloCallData >(); needAnotherCallData< GladToSeeMe_RPCtypes, GladToSeeMeCallData >(); needAnotherCallData< GladToSeeYou_RPCtypes, GladToSeeYouCallData >(); needAnotherCallData< BothGladToSee_RPCtypes, BothGladToSeeCallData >(); }
Parameter templat kedua dari fungsi needAnotherCallData adalah struktur RPCCallData yang dihasilkan. Struktur yang sama adalah parameter sinyal dalam kelas Qt yang dihasilkan dari layanan.
Struktur RPCCallData yang dihasilkan mewarisi dari kelas ServerCallData. Pada gilirannya, kelas ServerCallData diwarisi dari responden ServerResponder. Dengan demikian, penciptaan objek struktur yang koheren mengarah pada penciptaan objek responden.
Konstruktor untuk kelas ServerCallData mengambil dua parameter: signal_func dan request_func. signal_func adalah sinyal yang dihasilkan yang dipanggil setelah menerima tag dari antrian. request_func adalah fungsi yang harus dipanggil saat membuat responder baru. Misalnya, dalam hal ini mungkin fungsi RequestSayHello (). Panggilan request_func terjadi dalam fungsi needAnotherCallData (). Hal ini dilakukan agar manajemen responden (penciptaan dan penghapusan) terjadi dalam layanan.
Kode fungsi needAnotherCallData () terdiri dari membuat objek responden dan memanggil fungsi yang menghubungkan responden ke panggilan RPC:
template<class RPCCallData, class RPCTypes> void needAnotherCallData() { RPCCallData* cd = new RPCCallData();
Fungsi RequestRPC () adalah fungsi template untuk empat jenis interaksi. Akibatnya, panggilan RequestRPC () bermuara pada panggilan:
service_->(cd->request_func_)(...,cd->responder, (void*)cd)
di mana service_ adalah layanan gRPC. Dalam hal ini, ini adalah pingpong :: ping :: AsyncService.
Untuk memeriksa antrian acara secara sinkron atau asinkron, Anda harus memanggil fungsi CheckCQ () atau AsyncCheckCQ (). Kode fungsi CheckCQ () diturunkan untuk panggilan ke tag sinkron dari antrian dan pemrosesan tag ini:
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); }
Setelah menerima tag dari antrian, validitas tag dan server mulai diperiksa. Jika server dimatikan, maka tag tidak lagi diperlukan - itu dapat dihapus. Setelah itu, fungsi cqReaction () yang didefinisikan dalam kelas ServerCallData disebut:
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;
Bendera first_time_reaction_ menunjukkan bahwa Anda perlu membuat responden baru untuk RPC yang disebut. Fungsi CouldBeDeleted () dan ProcessEvent () diwarisi dari kelas responden ServerResponder yang sesuai. Fungsi CouldBeDeleted () mengembalikan tanda bahwa objek responder dapat dihapus. Fungsi processEvent () memproses tag dan flag ok. Jadi, misalnya, untuk responden tipe Streaming Klien, fungsinya terlihat seperti ini:
bool processEvent(void* tag, bool ok) { this->tag_ = tag; read_mode_ = ok; return true; }
Fungsi ProcessEvent (), terlepas dari jenis responden, selalu mengembalikan true. Nilai pengembalian fungsi ini dibiarkan untuk kemungkinan perluasan fungsi dan, secara teoritis, untuk menghilangkan kesalahan.
Setelah memproses acara, panggilan berikut:
service_->(*signal_func_)(genRpcCallData)
Service_ variabel adalah turunan dari layanan yang dihasilkan, dalam kasus kami QpingServerService. Signal_func_ variabel adalah sinyal layanan yang sesuai dengan RPC tertentu. Misalnya, SayHelloRequest (). GenRpcCallData variabel adalah objek responden dari tipe yang sesuai. Dari sudut pandang kode panggilan, variabel genRpcCallData adalah objek dari salah satu struktur RPCCallData yang dihasilkan.
Arsitektur pelanggan
Kapanpun memungkinkan, nama kelas dan fungsi klien cocok dengan nama kelas dan fungsi server. Diagram kelas klien ditunjukkan pada gambar.
Panah tebal menunjukkan hierarki warisan kelas, dan panah tipis menunjukkan keanggotaan anggota dan metode di kelas. Secara umum, untuk layanan, kelas Q [servicename] ClientService dihasilkan, di mana servicename adalah nama layanan yang dinyatakan dalam file .proto. RPCCallData adalah struktur kontrol yang dihasilkan untuk setiap RPC dalam layanan. Untuk memanggil RPC, kelas yang dihasilkan menyediakan fungsi yang namanya sama persis dengan RPC yang dinyatakan dalam file .proto. Dalam contoh kami, dalam file .proto RPC, SayHello () dinyatakan sebagai:
rpc SayHello (PingRequest) returns (PingReply) {}
Di kelas QpingClientService yang dihasilkan, fungsi RPC yang sesuai terlihat seperti ini:
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); }
Struktur RPCCallData yang dihasilkan, seperti dalam kasus server, pada akhirnya diwarisi dari kelas ClientResponder. Oleh karena itu, penciptaan objek dari struktur yang dihasilkan mengarah ke penciptaan responden. Setelah membuat responder, RPC dipanggil dan responder dikaitkan dengan kejadian menerima respons dari server. Dalam hal kode klien, panggilan RPC terlihat seperti ini:
void ToSayHello() { PingRequest request; request.set_name("user"); request.set_message("user"); pingPongSrv.SayHello(request); }
Tidak seperti kelas server QpingServerService yang dihasilkan, kelas QpingClientService mewarisi dari dua kelas templat: ConnectivityFeatures dan MonitorFeatures.
Kelas ConnectivityFeatures bertanggung jawab untuk keadaan koneksi klien-server dan menyediakan tiga fungsi untuk digunakan: grpc_connect (), grpc_disconnect (), grpc_reconnect (). Fungsi grpc_disconnect () hanya menghapus semua struktur data yang bertanggung jawab untuk berinteraksi dengan server. Panggilan ke grpc_connect dikurangi menjadi panggilan ke fungsi grpc_connect_ (), yang membuat struktur data kontrol:
void grpc_connect_() { channel_ = grpc::CreateChannel(target_, creds_); stub_ = GRPCService::NewStub(channel_); channelFeatures_ = std::make_unique<ChannelFeatures>(channel_); channelFeatures_->checkChannelState(); }
Kelas ChannelFeatures memonitor status komunikasi channel_ channel dengan server. Kelas ConnectivityFeatures merangkum objek kelas ChannelFeatures dan mengimplementasikan fungsi abstrak channelState (), checkChannelState (), dan terhubung () menggunakan objek ini. Fungsi channelState () mengembalikan keadaan teramati terakhir dari saluran komunikasi dengan server. Fungsi checkChannelState (), pada kenyataannya, mengembalikan kondisi saluran saat ini. Fungsi terhubung () mengembalikan tanda klien terhubung ke server.
Kelas MonitorFeatures bertanggung jawab untuk menerima dan memproses acara dari server dan menyediakan fungsi CheckCQ () untuk digunakan:
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; }
Struktur kode sama dengan server. Tidak seperti server, blok kode yang bertanggung jawab untuk memproses keadaan saat ini ditambahkan ke klien. Jika keadaan saluran komunikasi telah berubah, sinyal channelStateChangedSignal_ () dipanggil. Di semua layanan yang dihasilkan, ini adalah sinyal:
void channelStateChanged(int, int);
Selain itu, tidak seperti server, fungsi AsyncNext () digunakan di sini daripada Next (). Ini telah dilakukan karena beberapa alasan. Pertama, saat menggunakan AsyncNext (), kode klien memiliki kemampuan untuk mempelajari tentang perubahan status saluran komunikasi. Kedua, ketika menggunakan AsyncNext (), dimungkinkan untuk memanggil berbagai RPC dalam kode klien beberapa kali. Menggunakan fungsi Next () dalam kasus ini akan memblokir utas sampai suatu peristiwa diterima dari antrian dan, sebagai akibatnya, akan kehilangan dua fitur yang dijelaskan.
Setelah menerima acara dari antrian, seperti dalam kasus server, fungsi cqReaction (), didefinisikan dalam kelas ClientCallData, dipanggil:
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 ); }
Seperti halnya server, fungsi processEvent () memproses tag dan flag ok dan selalu mengembalikan true. Seperti dalam kasus server, setelah memproses acara, sinyal dari layanan yang dihasilkan harus dipanggil. Namun, ada dua perbedaan signifikan dari fungsi server dengan nama yang sama. Perbedaan pertama adalah bahwa responden tidak dibuat dalam fungsi ini. Pembuatan responden, seperti yang ditunjukkan di atas, terjadi ketika RPC dipanggil. Perbedaan kedua adalah bahwa responden tidak dihapus dalam fungsi ini. Tidak adanya penghapusan responden dilakukan karena dua alasan. Pertama, kode klien dapat menggunakan pointer untuk menghasilkan struktur RPCCallData untuk keperluan mereka sendiri. Menghapus konten oleh pointer ini, disembunyikan dari kode klien, dapat menyebabkan konsekuensi yang tidak menyenangkan. Kedua, penghapusan responden akan mengarah pada fakta bahwa sinyal dengan data tidak akan dihasilkan. Oleh karena itu, kode klien tidak akan menerima pesan server terakhir. Di antara beberapa alternatif untuk memecahkan masalah yang ditunjukkan, keputusan dibuat untuk menggeser pemindahan responden (struktur yang dihasilkan) ke kode klien. Dengan demikian, fungsi pengendali sinyal (slot) harus berisi kode berikut:
void ResponseHandler(RPCCallData* response) { if (response->CouldBeDeleted()) delete response;
Tidak adanya penghapusan responden dalam kode klien tidak hanya akan menyebabkan kebocoran memori, tetapi juga kemungkinan masalah dengan saluran komunikasi. Penangan sinyal dari semua jenis interaksi RPC diimplementasikan dalam kode sampel.
Kesimpulan
Sebagai kesimpulan, kami menarik dua poin. Poin pertama terkait dengan memanggil fungsi-fungsi CheckCQ () dari klien dan server. Mereka bekerja, seperti yang ditunjukkan di atas, sesuai dengan satu prinsip: jika ada suatu kejadian dalam antrian, sinyal dengan struktur RPCCallData yang dihasilkan adalah โdipancarkanโ. Anda dapat memanggil fungsi ini secara manual dan memeriksa (dalam kasus klien) untuk suatu acara. Tetapi awalnya ada ide untuk mentransfer seluruh bagian jaringan yang terkait dengan gRPC ke utas lain. Untuk tujuan ini, kelas tambahan QGrpcSrvMonitor untuk server gRPC dan QGrpcCliServer untuk klien gRPC ditulis. Kedua kelas bekerja pada prinsip yang sama: mereka membuat aliran terpisah, menempatkan layanan yang dihasilkan dalam aliran ini dan secara berkala memanggil fungsi CheckCQ () dari layanan ini. Jadi, ketika menggunakan kedua kelas tambahan, tidak perlu memanggil fungsi CheckCQ () dalam kode klien. Sinyal layanan yang dihasilkan, dalam hal ini, "datang" dari aliran lain. Contoh klien dan server diimplementasikan menggunakan kelas pembantu ini.
Poin kedua menyangkut mayoritas pengembang yang tidak menggunakan perpustakaan Qt dalam pekerjaan mereka. Kelas dan makro Qt di QgRPC hanya digunakan di dua tempat: dalam file layanan yang dihasilkan, dan dalam file yang berisi kelas tambahan: QGrpcServerMonitor.h dan QGrpcClientMonitor.h. File yang tersisa dengan perpustakaan Qt sama sekali tidak terkait. Direncanakan untuk menambah perakitan menggunakan cmake, dan untuk mematikan beberapa arahan Qt. Secara khusus, kelas QObject dan makro Q_OBJECT. Tapi tangan tidak bisa melakukan ini. Karena itu, setiap saran dipersilahkan.
Itu saja. Terima kasih semuanya!
Referensi