Serveur et client écho avec brpc : diagnostic d'une réponse manquante après modification du paquet

Définition du service Protobuf

Commencez par décrire l'interface dans un fichier echo.proto. L'option cc_generic_services est nécessaire pour que brpc génère les stubs C++.

syntax = "proto2";

option cc_generic_services = true;

message PacketRequest {
    required string payload = 1;
}

message PacketResponse {
    required string payload = 1;
}

service EchoService {
    rpc Send(PacketRequest) returns (PacketResponse);
}

Générez les fichiers C++ avec :

protoc --cpp_out=. echo.proto

Implémentation du serveur

Le serveur hérite du service généré et fournit l'implémentation de la méthode Send. Le garde brpc::ClosureGuard garantit que done->Run() est appelé en sortie du traitement, même en cas d'erreur.

#include <brpc/server.h>
#include <brpc/controller.h>
#include "echo.pb.h"

class EchoServiceImpl : public EchoService {
public:
    void Send(google::protobuf::RpcController* base,
              const PacketRequest* req,
              PacketResponse* resp,
              google::protobuf::Closure* done) override {
        brpc::ClosureGuard guard(done);
        brpc::Controller* ctl = static_cast<brpc::Controller*>(base);

        if (req == nullptr || !req->has_payload()) {
            ctl->SetFailed("Requête invalide");
            return;
        }

        LOG(INFO) << "Reçu de " << ctl->remote_side()
                  << " : " << req->payload();
        resp->set_payload(req->payload());
    }
};

int main(int argc, char* argv[]) {
    google::ParseCommandLineFlags(&argc, &argv, true);

    brpc::Server server;
    EchoServiceImpl impl;

    if (server.AddService(&impl, brpc::SERVER_DOESNT_OWN_SERVICE) != 0) {
        LOG(FATAL) << "Impossible d'ajouter le service";
        return -1;
    }

    brpc::ServerOptions opts;
    opts.num_threads = 4;

    if (server.Start(8080, &opts) != 0) {
        LOG(FATAL) << "Impossible de démarrer le serveur";
        return -1;
    }

    server.RunUntilAskedToQuit();
    return 0;
}

Implémentation du client

Un seul brpc::Channel peut être partagé entre plusieurs threads, mais il doit être correctement initialisé avant toute utilisation concurrente.

#include <brpc/channel.h>
#include <brpc/controller.h>
#include "echo.pb.h"

int main(int argc, char* argv[]) {
    google::ParseCommandLineFlags(&argc, &argv, true);

    brpc::Channel channel;
    brpc::ChannelOptions opts;
    opts.protocol = "baidu_std";
    opts.timeout_ms = 1000;
    opts.max_retry = 1;

    if (channel.Init("127.0.0.1", 8080, &opts) != 0) {
        LOG(FATAL) << "Échec de l'initialisation du canal";
        return -1;
    }

    EchoService_Stub stub(&channel);
    PacketRequest req;
    PacketResponse resp;
    brpc::Controller ctl;

    req.set_payload("hello brpc");
    stub.Send(&ctl, &req, &resp, nullptr);

    if (ctl.Failed()) {
        LOG(ERROR) << "RPC en échec (code=" << ctl.ErrorCode()
                   << ") : " << ctl.ErrorText();
        return -1;
    }

    LOG(INFO) << "Réponse : " << resp.payload();
    return 0;
}

Flux d'une requête brpc

Une invocation client passe par les étapes suivantes :

  1. Le Controller est attaché à la requête et un identifiant de corrélation est créé.
  2. Le canal choisit un serveur cible selon le mode de conenxion (single, pooled, short).
  3. La requête est sérialisée selon le protocole indiqué (baidu_std par défaut).
  4. Si un délai d'attente est configuré, une minuterie est armée.
  5. La réponse est reçue, décodée, et Controller::Failed() reflète le résultat.

Pourquoi aucune réponse n'est reçue après modification du paquet

Le titre mentionne le symptôme « pas de réponse du serveur distant après modification du paquet ». Ce comportement vient généralement d'une altération du message brpc alors que l'infrastructure attend toujours un cadre (framing) et une sérialité valides.

Le protocole baidu_std ajoute un en-tête contenant au moins la taille du message et un champ magique. Si vous modifiez manuellement le contenu sérialisé sans recalculer la taille, le décodeur côté serveur peut :

  • ne plus trouver une limite de message valide et fermer la connexion ;
  • obtenir une taille incohérente et attendre indéfiniment le reste du paquet ;
  • déclencher une erreur de parsing qui ne génère pas de réponse fonctionnelle.

Du point de vue du client, cela se traduit par un Controller::Failed() vrai avec un code d'erreur tel que EEOF, EREQUEST ou ERPCTIMEDOUT, plutôt que par un retour contenant une réponse.

Vérifications recommandées

  • Ne modifiez jamais le contenu d'un IOBuf ou d'un message Protobuf sérialisé sans reconstruire l'en-tête de taille correspondant.
  • Utilisez cntl->ErrorText() et cntl->ErrorCode() pour identifier précisément la cause.
  • Sur le serveur, si une erreur de traitement est détectée, appelez cntl->SetFailed(...) avant que le done ne soit exécuté. Le mécanisme de ClosureGuard transmet alors automatiquement l'erreur au client.
  • Assurez-vous que done->Run() n'est appelé qu'une seule fois et qu'aucun code asynchrone ne tente d'écrire dans le Controller après cet appel.

Exemple de propagation d'ereur côté serveur

void Send(google::protobuf::RpcController* base,
          const PacketRequest* req,
          PacketResponse* resp,
          google::protobuf::Closure* done) override {
    brpc::ClosureGuard guard(done);
    brpc::Controller* ctl = static_cast<brpc::Controller*>(base);

    if (!ValidatePacket(*req)) {
        ctl->SetFailed(brpc::EREQUEST, "Paquet modifié ou invalide");
        return;
    }

    resp->set_payload(req->payload());
}

Exemple de récupérasion d'erreur côté client

PacketResponse resp;
brpc::Controller ctl;
req.set_payload("test");
stub.Send(&ctl, &req, &resp, nullptr);

if (ctl.Failed()) {
    LOG(WARNING) << "Échec côté " << ctl.remote_side()
                 << " code=" << ctl.ErrorCode()
                 << " msg=" << ctl.ErrorText();
} else {
    LOG(INFO) << "Réponse : " << resp.payload();
}

Diagnostic à l'aide des logs

Activez les traces utiles lors du développement :

./client -logtostderr -minloglevel=0
./server -logtostderr -minloglevel=0

Les messages suivants indiquent souvent un paquet corrompu ou une fermeture prématurée de la connexion :

  • Got EOF : le client a fermé le socket avant que le serveur ne réponde, typique d'un timeout client ou d'une trame mal formée.
  • Remote side was closed : la connexion a été fermée par le pair, souvent après un échec de parsing.
  • A message ... is bigger than ... : le champ de taille a été écrasé et pointe hors des limites autorisées par -max_body_size.

Commandes complètes de compilation

protoc --cpp_out=. echo.proto

g++ -std=c++11 server.cpp echo.pb.cc \
    -lbrpc -lgflags -lprotobuf -lleveldb -lssl -lcrypto -lpthread -o server

g++ -std=c++11 client.cpp echo.pb.cc \
    -lbrpc -lgflags -lprotobuf -lleveldb -lssl -lcrypto -lpthread -o client

Étiquettes: brpc Protobuf C++ RPC baidu_std

Publié le 4 août à 00h51