RPC 同步调用基本使用方法:基于官方 RouteGuide 示例

RPC 同步调用基本使用方法:基于官方 RouteGuide 示例

一、RPC 和 gRPC 的基本理解

1. RPC 要解决什么问题

RPC 的全称是 Remote Procedure Call,也就是远程过程调用。它要解决的问题很直接:一个进程中的代码,想调用另一个进程、甚至另一台机器上的函数,能不能像调用本地函数一样简单?如果是普通网络编程,客户端要自己组织请求报文,服务端要自己解析请求、分发接口、封装响应;而 RPC 框架把这些过程封装起来,让调用方看到的是一个"函数调用",底层实际完成的是序列化、网络传输、反序列化和远程执行

单机函数调用只需要知道函数名、参数和返回值,而远程调用还必须解决三个问题:

  1. 双方要提前约定调用语义,也就是有哪些接口、每个接口的参数和返回值是什么
  2. 请求和响应要通过网络传输
  3. 传输内容要有统一的数据格式。到了分布式场景,还会继续引出服务注册发现、负载均衡、故障处理、超时控制等问题

gRPC 就是一个围绕这些问题设计的 RPC 框架,它基于服务的思想来组织接口,默认使用 Protobuf 作为接口描述语言和数据序列化方式,底层基于 HTTP/2 传输

2. gRPC 的基本模型

gRPC 的开发流程可以简单概括为:先写 .proto 文件,在里面定义 messageservice;然后用 protoc 生成对应语言的代码;服务端继承生成出来的 Service 类并实现 RPC 方法;客户端通过生成出来的 Stub 对象调用远程服务。这里有几个概念要分清楚:

  • Service 是服务定义,描述服务端提供哪些 RPC
  • RPC 是一次远程调用动作
  • Channel 是客户端到服务端的通信通道
  • Stub 是客户端代理对象,真正发起 RPC 请求的是它
  • ServerBuilder 用来创建服务端,配置监听地址、注册服务实现,然后构建并启动 Server

gRPC 支持同步和异步两种调用方式。同步调用的特点是代码结构简单,客户端发起调用后会阻塞等待服务端处理完成,拿到响应和状态之后再继续往下执行;异步调用不会一直卡在调用处,而是借助 CompletionQueue、回调或者其他异步机制在请求完成后再处理结果。本文主要讲同步调用

3. Protobuf 和 HTTP/2 的作用

RPC 调用本质上还是网络通信,所以一定要解决"传什么"和"怎么传"的问题。早期很多 RPC 或 HTTP API 会用 JSON 作为数据格式,JSON 的优点是可读性强、跨语言方便,但是字段名、符号、字符串等内容会带来额外体积,复杂类型表达能力和传输效率也不如二进制协议。gRPC 默认使用 Protobuf,.proto 文件中每个字段都有明确类型、字段名和编号,能够表达普通字段、数组、嵌套对象、map 等结构;生成 C++ 代码后,message 会变成 C++ 类,对象可以被序列化成二进制,也可以从二进制反序列化回来

Protobuf参考:Protobuf使用详解

HTTP/2 主要解决传输效率问题。HTTP/1.x 在同一个连接上很难高效区分多个并发请求和响应,容易出现串行排队。HTTP/2 引入了 的概念,一个请求对应一个流,每个流有自己的 ID,请求和响应数据可以拆成多个帧在同一个 TCP 连接中交错传输,接收方再根据流 ID 组装回对应请求。gRPC 借助 HTTP/2 的多路复用、二进制帧和流控制能力,可以比较自然地支持一元 RPC、服务端流、客户端流和双向流

4. gRPC 的四种 RPC 模式

gRPC 一共支持四种常见调用模式:一元 RPC、服务端流式 RPC、客户端流式 RPC、双向流式 RPC:

  1. 一元 RPC 最像普通函数调用,客户端发送一个请求,服务端返回一个响应
  2. 服务端流式 RPC 是客户端发送一个请求,服务端返回一串响应,客户端不断从流里读取结果
  3. 客户端流式 RPC 是客户端连续发送多个请求,服务端最后返回一个响应
  4. 双向流式 RPC 是客户端和服务端都持有一个流,双方都可以按自己的逻辑读写消息

从同步 C++ API 的角度看,可以这样记:

  1. 一元 RPC 直接通过 stub_->RpcName(&context, request, &response) 调用
  2. 服务端流是客户端拿到 ClientReader<T>,服务端使用 ServerWriter<T>
  3. 客户端流是客户端拿到 ClientWriter<T>,服务端使用 ServerReader<T>
  4. 双向流是客户端使用 ClientReaderWriter<Req, Resp>,服务端使用 ServerReaderWriter<Req, Resp>

读流时一般使用 Read(),写流时一般使用 Write(),客户端写完流后使用 WritesDone() 表示不再发送,最后通过 Finish() 获取 RPC 最终状态

二、同步 gRPC 的基本使用流程

1. 编写 proto 文件,定义消息和服务

同步 gRPC 的第一步是写 .proto 文件。.proto 文件不是普通 C++ 代码,而是接口描述文件,它负责描述传输的数据结构和远程调用接口。例如一个最简单的服务可以写成 rpc SayHello(HelloRequest) returns (HelloReply),它表示客户端发送一个 HelloRequest,服务端返回一个 HelloReply。在 C++ 中不能直接编译 .proto 文件,需要通过 protoc 生成 .pb.h/.pb.cc.grpc.pb.h/.grpc.pb.cc,前者主要对应 Protobuf 消息类,后者主要对应 gRPC 服务类和 Stub 类

在 RouteGuide 项目中,route_guide.proto 定义了一个 RouteGuide 服务,里面有四个 RPC 接口,正好覆盖四种 RPC 模式:

proto 复制代码
service RouteGuide {
  rpc GetFeature(Point) returns (Feature) {}
  rpc ListFeatures(Rectangle) returns (stream Feature) {}
  rpc RecordRoute(stream Point) returns (RouteSummary) {}
  rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}

这四行就是整个项目的接口核心

  1. GetFeature 是普通一元 RPC
  2. ListFeatures 是服务端流式 RPC
  3. RecordRoute 是客户端流式 RPC
  4. RouteChat 是双向流式 RPC

gRPC 的好处就在这里,接口形式在 .proto 中已经写死了,客户端和服务端都根据同一份 proto 生成代码,不需要双方手动约定 URL、请求体格式、响应体格式

2. 服务端继承 Service,实现 RPC 方法

生成代码后,服务端要继承 RouteGuide::Service,然后重写 proto 中定义的 RPC 方法。同步服务端的代码结构比较像普通 C++ 多态:父类由 gRPC 根据 proto 生成,子类由自己写业务逻辑

cpp 复制代码
class RouteGuideImpl final : public RouteGuide::Service {
 public:
  explicit RouteGuideImpl(const std::string& db) {
    routeguide::ParseDb(db, &feature_list_);
  }

  Status GetFeature(ServerContext* context, const Point* point,
                    Feature* feature) override {
    feature->set_name(GetFeatureName(*point, feature_list_));
    feature->mutable_location()->CopyFrom(*point);
    return Status::OK;
  }

 private:
  std::vector<Feature> feature_list_;
};

这里的 ServerContext* context 表示一次服务端 RPC 调用的上下文,里面可以携带元数据、认证信息、取消状态等;const Point* point 是客户端传来的请求;Feature* feature 是服务端要填写的响应对象;返回值 Status 表示 RPC 本身是否成功。同步一元 RPC 的实现方式很直观:读取请求对象,在响应对象里写入数据,然后返回 Status::OK

3. 服务端启动 Server,注册服务对象

RPC 方法实现好之后,还需要启动 gRPC 服务端。同步服务端一般使用 ServerBuilder:先指定监听地址,再注册服务实现对象,最后构建并启动 Server

cpp 复制代码
void RunServer(const std::string& db_path) {
  // 0.0.0.0:50051 表示服务端监听本机所有网卡的 50051 端口
  std::string server_address("0.0.0.0:50051");
  RouteGuideImpl service(db_path);

  ServerBuilder builder;
  // InsecureServerCredentials() 表示使用明文通信,不启用 TLS
  // 学习和本地测试可以这样写,生产环境一般不建议
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  // RegisterService(&service) 把具体服务实现注册到 gRPC Server 中
  builder.RegisterService(&service);
  std::unique_ptr<Server> server(builder.BuildAndStart());

  std::cout << "Server listening on " << server_address << std::endl;
  // server->Wait() 会阻塞当前线程,让服务端持续等待客户端请求
  server->Wait();
}

4. 客户端创建 Channel 和 Stub,发起同步调用

客户端要先创建 Channel,再基于 Channel 创建 StubChannel 表示到某个服务端地址的通信通道,Stub 是客户端代理对象,业务代码通过 Stub 调用远程 RPC

cpp 复制代码
RouteGuideClient(std::shared_ptr<Channel> channel, const std::string& db)
    : stub_(RouteGuide::NewStub(channel)) {
  routeguide::ParseDb(db, &feature_list_);
}

客户端主函数中创建连接的方式如下:

cpp 复制代码
RouteGuideClient guide(
    grpc::CreateChannel("localhost:50051",
                        grpc::InsecureChannelCredentials()),
    db);

这里的 localhost:50051 要和服务端监听地址对应。InsecureChannelCredentials() 表示客户端也使用明文连接。创建好 guide 后,客户端依次调用 guide.GetFeature()guide.ListFeatures()guide.RecordRoute()guide.RouteChat(),也就是依次演示四种同步 RPC 的使用方式

三、RouteGuide 项目整体结构

1. 这个项目的业务含义

RouteGuide 是 gRPC 官方示例中的路线导航项目,可以把它理解成一个简化版地图服务。它里面有几个核心数据结构:Point 表示经纬度点,Rectangle 表示一个矩形区域,Feature 表示某个坐标上的地点信息,RouteSummary 表示路线统计结果,RouteNote 表示某个坐标上的留言。客户端可以查询某个点有没有景点,也可以查询某个矩形区域内有哪些景点,还可以模拟走过一段路线,让服务端统计经过的点数、景点数、总距离和耗时,最后还可以在某个坐标发送留言并接收同一位置已有留言

Point 使用的是 E7 表示法,也就是把真实经纬度乘以 10000000 后存成整数,例如 409146138 实际表示 40.9146138。这样比直接用浮点数传输更稳定,也更适合 Protobuf 中的整数编码

proto 复制代码
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

message Feature {
  string name = 1;
  Point location = 2;
}

2. proto 文件决定客户端和服务端的代码形态

RouteGuide 的 proto 文件非常适合用来理解 gRPC,因为四个接口刚好对应四种同步调用模式。对于服务端来说,生成代码后需要实现的函数形态大概是这样:

cpp 复制代码
// 一元 RPC
Status GetFeature(ServerContext* context, const Point* point, Feature* feature);

// 服务端流式 RPC
Status ListFeatures(ServerContext* context,
                    const Rectangle* rectangle,
                    ServerWriter<Feature>* writer);

// 客户端流式 RPC
Status RecordRoute(ServerContext* context,
                   ServerReader<Point>* reader,
                   RouteSummary* summary);

// 双向流式 RPC
Status RouteChat(ServerContext* context,
                 ServerReaderWriter<RouteNote, RouteNote>* stream);

对于客户端来说,对应的同步调用方式大概是这样:

cpp 复制代码
// 一元 RPC
Status status = stub_->GetFeature(&context, point, feature);

// 服务端流式 RPC
std::unique_ptr<ClientReader<Feature>> reader(
    stub_->ListFeatures(&context, rect));

// 客户端流式 RPC
std::unique_ptr<ClientWriter<Point>> writer(
    stub_->RecordRoute(&context, &stats));

// 双向流式 RPC
std::shared_ptr<ClientReaderWriter<RouteNote, RouteNote>> stream(
    stub_->RouteChat(&context));

所以学习这个项目时,重点不是地图业务,而是看清楚 proto 写法服务端函数参数客户端调用对象 三者之间的对应关系。只要这个关系理顺了,以后自己写 gRPC 项目就是换一批 message 和 service,本质流程是一样的

四、一元 RPC:GetFeature

1. 接口定义

GetFeature 是最简单的同步 RPC,它的 proto 定义是:

proto 复制代码
rpc GetFeature(Point) returns (Feature) {}

意思是客户端发送一个 Point,服务端返回一个 Feature。这个过程和普通函数调用最像,只不过普通函数调用发生在本进程内,而 RPC 调用会经过 Stub、序列化、网络传输、服务端反序列化、执行业务、再序列化返回等过程

2. 服务端实现

服务端实现如下:

cpp 复制代码
Status GetFeature(ServerContext* context, const Point* point,
                  Feature* feature) override {
  feature->set_name(GetFeatureName(*point, feature_list_));
  feature->mutable_location()->CopyFrom(*point);
  return Status::OK;
}

这里服务端先用 GetFeatureName(*point, feature_list_) 在内存中的地点列表里查找当前坐标对应的名字,然后把名字写入 feature,再把客户端传来的坐标原样拷贝到 feature->location 中。mutable_location() 用来拿到内部嵌套对象的可修改指针,CopyFrom() 用来把一个 Point 拷贝进去。最后返回 Status::OK,表示这次 RPC 正常完成

辅助函数 GetFeatureName 本质上就是遍历查找:

cpp 复制代码
std::string GetFeatureName(const Point& point,
                           const std::vector<Feature>& feature_list) {
  for (const Feature& f : feature_list) {
    if (f.location().latitude() == point.latitude() &&
        f.location().longitude() == point.longitude()) {
      return f.name();
    }
  }
  return "";
}

3. 客户端调用

客户端真正发起 RPC 的地方是:

cpp 复制代码
bool GetOneFeature(const Point& point, Feature* feature) {
  ClientContext context;
  // 调用服务端
  Status status = stub_->GetFeature(&context, point, feature);
  // 后面都是对返回结果的判断输出
  if (!status.ok()) {
    std::cout << "GetFeature rpc failed." << std::endl;
    return false;
  }

  if (feature->name().empty()) {
    std::cout << "Found no feature at "
              << feature->location().latitude() / kCoordFactor_ << ", "
              << feature->location().longitude() / kCoordFactor_ << std::endl;
  } else {
    std::cout << "Found feature called " << feature->name() << " at "
              << feature->location().latitude() / kCoordFactor_ << ", "
              << feature->location().longitude() / kCoordFactor_ << std::endl;
  }
  return true;
}

同步调用最明显的特点就在 Status status = stub_->GetFeature(&context, point, feature); 这一行:调用发出后,当前线程会等待服务端返回;返回后,feature 中已经有服务端填好的响应内容,status 中保存 RPC 状态。业务是否查到地点,不一定等于 RPC 是否成功。比如服务端正常返回一个空名字的 Feature,说明 RPC 是成功的,只是该坐标没有景点;只有 status.ok() 为 false,才表示 RPC 调用本身失败

五、服务端流式 RPC:ListFeatures

1. 接口定义

ListFeatures 的 proto 定义是:

proto 复制代码
rpc ListFeatures(Rectangle) returns (stream Feature) {}

它表示客户端发送一个矩形区域 Rectangle,服务端返回多个 Feature。这种模式适合"一个请求对应很多结果"的场景,比如查询某个区域内的所有景点。如果用普通一元 RPC,服务端可能需要一次性构造一个很大的数组返回;而服务端流式 RPC 可以边查边写,客户端边读边处理

2. 服务端实现

服务端实现如下:

cpp 复制代码
Status ListFeatures(ServerContext* context,
                    const routeguide::Rectangle* rectangle,
                    ServerWriter<Feature>* writer) override {
  auto lo = rectangle->lo();
  auto hi = rectangle->hi();
  long left = (std::min)(lo.longitude(), hi.longitude());
  long right = (std::max)(lo.longitude(), hi.longitude());
  long top = (std::max)(lo.latitude(), hi.latitude());
  long bottom = (std::min)(lo.latitude(), hi.latitude());

  for (const Feature& f : feature_list_) {
    if (f.location().longitude() >= left &&
        f.location().longitude() <= right &&
        f.location().latitude() >= bottom &&
        f.location().latitude() <= top) {
      writer->Write(f);
    }
  }
  return Status::OK;
}

这里 ServerWriter<Feature>* writer 是关键。服务端不再通过一个 Feature* response 返回单个响应,而是通过 writer->Write(f) 连续写多个响应。每调用一次 Write,客户端那边就有机会通过 Read 读到一个 Feature。当循环结束并返回 Status::OK 后,服务端流就结束了

3. 客户端调用

客户端调用如下:

cpp 复制代码
std::unique_ptr<ClientReader<Feature> > reader(
    stub_->ListFeatures(&context, rect));

while (reader->Read(&feature)) {
  std::cout << "Found feature called " << feature.name() << " at "
            << feature.location().latitude() / kCoordFactor_ << ", "
            << feature.location().longitude() / kCoordFactor_ << std::endl;
}

Status status = reader->Finish();

客户端拿到的是 ClientReader<Feature>,它负责从服务端返回流中读取消息。reader->Read(&feature) 每成功一次,就说明读到一个服务端写回来的 Feature;当服务端没有更多数据时,Read 返回 false,循环结束;最后调用 Finish() 获取这次 RPC 的最终状态。这里要注意,Read() 返回 false 不一定表示出错,它也可能只是正常读完了;是否真正失败要看最后的 Status

六、客户端流式 RPC:RecordRoute

1. 接口定义

RecordRoute 的 proto 定义是:

proto 复制代码
rpc RecordRoute(stream Point) returns (RouteSummary) {}

它表示客户端连续发送多个 Point,服务端最后返回一个 RouteSummary。这个接口模拟的是用户走过一段路线,客户端不断上传经过的坐标点,服务端统计总点数、经过的景点数量、路线距离和耗时

2. 服务端实现

服务端实现如下:

cpp 复制代码
Status RecordRoute(ServerContext* context, ServerReader<Point>* reader,
                   RouteSummary* summary) override {
  Point point;
  int point_count = 0;
  int feature_count = 0;
  float distance = 0.0;
  Point previous;

  system_clock::time_point start_time = system_clock::now();
  while (reader->Read(&point)) {
    point_count++;
    if (!GetFeatureName(point, feature_list_).empty()) {
      feature_count++;
    }
    if (point_count != 1) {
      distance += GetDistance(previous, point);
    }
    previous = point;
  }
  system_clock::time_point end_time = system_clock::now();

  summary->set_point_count(point_count);
  summary->set_feature_count(feature_count);
  summary->set_distance(static_cast<long>(distance));
  auto secs =
      std::chrono::duration_cast<std::chrono::seconds>(end_time - start_time);
  summary->set_elapsed_time(secs.count());

  return Status::OK;
}

这里服务端参数是 ServerReader<Point>* reader,说明服务端要从客户端发送的流里不断读 Pointwhile (reader->Read(&point)) 会一直读取,直到客户端发送完并关闭写方向。循环过程中,服务端统计点数,判断该点是不是已知景点,并用 GetDistance(previous, point) 累加两点之间的距离。等客户端流结束后,服务端再填写 summary 并返回

3. 客户端调用

客户端调用如下:

cpp 复制代码
std::unique_ptr<ClientWriter<Point> > writer(
    stub_->RecordRoute(&context, &stats));

for (int i = 0; i < kPoints; i++) {
  const Feature& f = feature_list_[feature_distribution(generator)];
  std::cout << "Visiting point " << f.location().latitude() / kCoordFactor_
            << ", " << f.location().longitude() / kCoordFactor_
            << std::endl;
  if (!writer->Write(f.location())) {
    break;
  }
  std::this_thread::sleep_for(
      std::chrono::milliseconds(delay_distribution(generator)));
}

writer->WritesDone();
Status status = writer->Finish();

客户端拿到的是 ClientWriter<Point>,通过 writer->Write(f.location()) 不断向服务端发送点。发送完所有点后,必须调用 writer->WritesDone(),它的意思是"客户端写方向结束,不再继续发送 Point"。如果不调用它,服务端的 reader->Read(&point) 可能一直等不到流结束,自然也就没法计算最终 summary。最后 writer->Finish() 会等待服务端返回最终状态,同时 stats 中已经被写入服务端返回的统计结果

这个例子很好地体现了客户端流的典型使用场景:请求数据不是一个对象,而是一组连续产生的数据;服务端需要等数据收完后做汇总,然后返回一个结果

七、双向流式 RPC:RouteChat

1. 接口定义

RouteChat 的 proto 定义是:

proto 复制代码
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

它表示客户端发送的是一个 RouteNote 流,服务端返回的也是一个 RouteNote 流。双向流最灵活,客户端和服务端都有读写能力,具体什么时候读、什么时候写,由双方业务逻辑决定。这个例子里,客户端发送一些位置留言;服务端每收到一条留言,就查找之前同一位置是否已有留言,如果有,就把历史留言写回给客户端

2. 服务端实现

服务端实现如下:

cpp 复制代码
Status RouteChat(ServerContext* context,
                 ServerReaderWriter<RouteNote, RouteNote>* stream) override {
  RouteNote note;
  while (stream->Read(&note)) {
    std::unique_lock<std::mutex> lock(mu_);
    for (const RouteNote& n : received_notes_) {
      if (n.location().latitude() == note.location().latitude() &&
          n.location().longitude() == note.location().longitude()) {
        stream->Write(n);
      }
    }
    received_notes_.push_back(note);
  }

  return Status::OK;
}

ServerReaderWriter<RouteNote, RouteNote>* stream 同时具备读和写的能力。服务端通过 stream->Read(&note) 读取客户端发来的留言,通过 stream->Write(n) 把历史留言写回客户端。这里用了 std::mutex,因为 received_notes_ 是服务对象中的共享成员,多个客户端同时调用 RouteChat 时可能并发访问这个 vector,所以需要加锁保护

不过这个示例也有一个可以改进的点:它在持有锁的情况下调用了 stream->Write(n),而写网络流可能阻塞。学习 demo 中这样写问题不大,但生产代码中一般会尽量避免持锁执行可能阻塞的 I/O 操作,可以先把要返回的消息拷贝到临时 vector,释放锁之后再写回客户端

3. 客户端调用

客户端调用如下:

cpp 复制代码
std::shared_ptr<ClientReaderWriter<RouteNote, RouteNote> > stream(
    stub_->RouteChat(&context));

std::thread writer([stream]() {
  std::vector<RouteNote> notes{MakeRouteNote("First message", 0, 0),
                               MakeRouteNote("Second message", 0, 1),
                               MakeRouteNote("Third message", 1, 0),
                               MakeRouteNote("Fourth message", 0, 0)};
  for (const RouteNote& note : notes) {
    std::cout << "Sending message " << note.message() << " at "
              << note.location().latitude() << ", "
              << note.location().longitude() << std::endl;
    stream->Write(note);
  }
  stream->WritesDone();
});

RouteNote server_note;
while (stream->Read(&server_note)) {
  std::cout << "Got message " << server_note.message() << " at "
            << server_note.location().latitude() << ", "
            << server_note.location().longitude() << std::endl;
}
writer.join();
Status status = stream->Finish();

客户端使用 ClientReaderWriter<RouteNote, RouteNote>,它既能 Write,也能 Read。示例中专门创建了一个写线程负责发送四条留言,主线程则不断读取服务端返回的历史留言。第一条留言是 (0,0),第四条留言也是 (0,0),所以当服务端收到第四条留言时,会发现之前同一位置已经有 First message,于是把它写回客户端

双向流这里要特别注意结束过程。客户端写完后调用 WritesDone() 表示客户端不再发送消息;主线程中的 Read() 会一直读服务端响应,直到服务端结束响应流;最后调用 Finish() 获取 RPC 最终状态。也就是说,流不是无限长连接,流也会结束。客户端发送流通过 WritesDone() 结束,服务端发送流一般通过 RPC 函数返回 Status 来结束,接收方通过 Read() 返回 false 感知流结束

八、同步调用中几个容易混淆的点

1. Stub 和 Service 的区别

Service 是服务端用的,Stub 是客户端用的。RouteGuide::Service 是 gRPC 根据 proto 生成的服务端基类,服务端继承它并重写 RPC 方法;RouteGuide::Stub 是客户端代理对象,客户端通过它发起远程调用。可以简单记成一句话:服务端实现 Service,客户端调用 Stub

2. ClientContext 和 ServerContext 的区别

ClientContext 是客户端一次 RPC 的上下文,客户端每发起一次 RPC 通常都要创建一个新的 ClientContextServerContext 是服务端处理一次 RPC 时拿到的上下文。它们可以携带 metadata、deadline、取消状态、认证相关信息等。这个 RouteGuide 示例里没有深入使用它们,但实际项目中设置超时、传 token、读取客户端信息时经常会用到

3. Status 表示 RPC 状态,不一定等于业务状态

Status 表示 RPC 调用本身是否正常完成,比如网络是否正常、服务端是否返回错误码等。业务上的"没查到数据"不一定是 RPC 失败。例如 GetFeature 中,如果某个点没有景点,服务端返回一个 name 为空的 Feature,这仍然可以是 Status::OK。所以写客户端代码时,一般先判断 status.ok(),再判断业务字段

4. Reader、Writer 的方向要站在当前端理解

服务端流式 RPC 中,服务端写、客户端读,所以是 ServerWriter 对应 ClientReader;客户端流式 RPC 中,客户端写、服务端读,所以是 ClientWriter 对应 ServerReader;双向流式 RPC 中,两边都读写,所以都是 ReaderWriter。不要死记类型名,要先判断"谁发多个消息,谁收多个消息"

相关推荐
kyriewen111 小时前
WebAssembly:前端界的“外挂”,让C++代码在浏览器里跑起来
开发语言·前端·javascript·c++·单元测试·ecmascript
浅念-4 小时前
刷穿LeetCode:BFS 解决 Flood Fill 算法
数据结构·c++·算法·leetcode·职场和发展·bfs·宽度优先
做cv的小昊5 小时前
【TJU】研究生应用统计学课程笔记(8)——第四章 线性模型(4.1 一元线性回归分析)
笔记·线性代数·算法·数学建模·回归·线性回归·概率论
楼田莉子6 小时前
Linux网络:NAT_代理
linux·运维·服务器·开发语言·c++·后端
我命由我123456 小时前
程序员的心理学学习笔记 - 空杯心态
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
南境十里·墨染春水6 小时前
C++日志 2——实现单线程日志系统
java·jvm·c++
zh_xuan6 小时前
api测试工具添加历史记录功能
c++·libcurl·duilib
晓梦林6 小时前
3170靶场学习笔记
笔记·学习
ZC跨境爬虫6 小时前
跟着 MDN 学 HTML day_17:媒体与 Web Audio API 自动播放指南——策略、检测与最佳实践
前端·笔记·ui·html·音视频·媒体