Asynchronous-API tutorial
异步API教程
This tutorial shows you how to write a simple server and client in C++ using gRPC’s asynchronous/non-blocking APIs. It assumes you are already familiar with writing simple synchronous gRPC code, as described in Basics tutorial. The example used in this tutorial follows from the basic Greeter example used in the quick start. You’ll find it along with installation instructions in grpc/examples/cpp/helloworld.
本教程展示如何使用gRPC的异步/非阻塞API在C++中编写一个简单的服务器和客户端。假设已经熟悉编写简单的同步gRPC代码,如基础教程中所述。本教程中使用的示例来自快速入门中使用的基本Greeter示例。可以在grpc/examples/cpp/helloworld中找到它以及安装说明。
Overview
概述
gRPC uses the CompletionQueue API for asynchronous operations. The basic work flow is as follows:
gRPC使用CompletionQueue API进行异步操作。基本工作流程如下:
- bind a
CompletionQueue
to an RPC call - 将CompletionQueue绑定到RPC调用
- do something like a read or write, present with a unique
void*
tag - 做一些读或写的事情,用一个独特的void*标签表示
- call
CompletionQueue::Next
to wait for operations to complete. If a tag appears, it indicates that the corresponding operation is complete. - 调用CompletionQueue::Next以等待操作完成。如果出现标记,则表示相应的操作已完成。
Async client
异步客户端
To use an asynchronous client to call a remote method, you first create a channel and stub, just as you do in a synchronous client. Once you have your stub, you do the following to make an asynchronous call:
要使用异步客户端调用远程方法,首先要创建一个通道和存根,就像在同步客户端中一样。一旦有了存根,就可以执行以下操作来进行异步调用:
-
Initiate the RPC and create a handle for it. Bind the RPC to a
CompletionQueue
. -
初始化RPC并为其创建句柄。将RPC绑定到CompletionQueue。
CompletionQueue cq; std::unique_ptr<ClientAsyncResponseReader<HelloReply> > rpc( stub_->AsyncSayHello(&context, request, &cq));
-
Ask for the reply and final status, with a unique tag
-
使用唯一标签询问回复和最终状态
Status status; rpc->Finish(&reply, &status, (void*)1);
-
Wait for the completion queue to return the next tag. The reply and status are ready once the tag passed into the corresponding
Finish()
call is returned. -
等待完成队列返回下一个标记。一旦返回传递给相应Finish()调用的标记,应答和状态就准备好了。
void* got_tag; bool ok = false; cq.Next(&got_tag, &ok); if (ok && got_tag == (void*)1) { // check reply and status }
You can see the complete client example in greeter_async_client.cc.
可以在greetiner_async_client.cc中看到完整的客户端示例。
Async server
异步服务器
The server implementation requests an RPC call with a tag and then waits for the completion queue to return the tag. The basic flow for handling an RPC asynchronously is:
服务器实现请求带有标记的RPC调用,然后等待完成队列返回标记。异步处理RPC的基本流程是:
-
Build a server exporting the async service
-
建立导出异步服务的服务器
helloworld::Greeter::AsyncService service; ServerBuilder builder; builder.AddListeningPort("0.0.0.0:50051", InsecureServerCredentials()); builder.RegisterService(&service); auto cq = builder.AddCompletionQueue(); auto server = builder.BuildAndStart();
-
Request one RPC, providing a unique tag
-
请求一个RPC,提供唯一标记
ServerContext context; HelloRequest request; ServerAsyncResponseWriter<HelloReply> responder; service.RequestSayHello(&context, &request, &responder, &cq, &cq, (void*)1);
-
Wait for the completion queue to return the tag. The context, request and responder are ready once the tag is retrieved.
-
等待完成队列返回标记。一旦检索到标签,上下文、请求和响应程序就准备好了。
HelloReply reply; Status status; void* got_tag; bool ok = false; cq.Next(&got_tag, &ok); if (ok && got_tag == (void*)1) { // set reply and status responder.Finish(reply, status, (void*)2); }
-
Wait for the completion queue to return the tag. The RPC is finished when the tag is back.
-
等待完成队列返回标记。RPC在标记返回时完成。
void* got_tag; bool ok = false; cq.Next(&got_tag, &ok); if (ok && got_tag == (void*)2) { // clean up }
This basic flow, however, doesn’t take into account the server handling multiple requests concurrently. To deal with this, our complete async server example uses a CallData
object to maintain the state of each RPC, and uses the address of this object as the unique tag for the call.
然而,这个基本流程没有考虑服务器同时处理多个请求。为了解决这个问题,我们完整的异步服务器示例使用CallData对象来维护每个RPC的状态,并使用该对象的地址作为调用的唯一标记。
class CallData {
public:
// Take in the "service" instance (in this case representing an asynchronous
// server) and the completion queue "cq" used for asynchronous communication
// with the gRPC runtime.
//取“service”实例(在本例中表示异步
//服务器)和用于异步通信的完成队列“cq”
//使用gRPC运行时。
CallData(Greeter::AsyncService* service, ServerCompletionQueue* cq)
: service_(service), cq_(cq), responder_(&ctx_), status_(CREATE) {
// Invoke the serving logic right away.
//立即调用服务逻辑。
Proceed();
}
void Proceed() {
if (status_ == CREATE) {
// As part of the initial CREATE state, we *request* that the system
// start processing SayHello requests. In this request, "this" acts are
// the tag uniquely identifying the request (so that different CallData
// instances can serve different requests concurrently), in this case
// the memory address of this CallData instance.
//作为初始CREATE状态的一部分,我们*请求*系统
//开始处理SayHello请求。在本请求中,“this”行为是
//唯一标识请求的标签(以便不同的CallData
//实例可以同时为不同的请求提供服务),在这种情况下
//此CallData实例的内存地址。
service_->RequestSayHello(&ctx_, &request_, &responder_, cq_, cq_,
this);
// Make this instance progress to the PROCESS state.
//使此实例进入PROCESS状态。
status_ = PROCESS;
} else if (status_ == PROCESS) {
// Spawn a new CallData instance to serve new clients while we process
// the one for this CallData. The instance will deallocate itself as
// part of its FINISH state.
//生成一个新的CallData实例,在我们处理时为新客户提供服务
//这个CallData。实例将自身解除分配为
//完成状态的一部分。
new CallData(service_, cq_);
// The actual processing.
//实际处理。
std::string prefix("Hello ");
reply_.set_message(prefix + request_.name());
// And we are done! Let the gRPC runtime know we've finished, using the
// memory address of this instance as the uniquely identifying tag for
// the event.
//我们完了!让gRPC运行时知道我们已经完成,使用
//此实例的内存地址作为的唯一标识标签
//事件。
responder_.Finish(reply_, Status::OK, this);
status_ = FINISH;
} else {
GPR_ASSERT(status_ == FINISH);
// Once in the FINISH state, deallocate ourselves (CallData).
//一旦进入FINISH状态,就释放我们自己(CallData)。
delete this;
}
}
}
For simplicity the server only uses one completion queue for all events, and runs a main loop in HandleRpcs
to query the queue:
为了简单起见,服务器对所有事件只使用一个完成队列,并在HandleRpcs中运行一个主循环来查询队列:
void HandleRpcs() {
// Spawn a new CallData instance to serve new clients.
//生成一个新的CallData实例来为新客户端提供服务。
new CallData(&service_, cq_.get());
void* tag; // uniquely identifies a request.唯一地标识请求。
bool ok;
while (true) {
// Block waiting to read the next event from the completion queue. The
// event is uniquely identified by its tag, which in this case is the
// memory address of a CallData instance.
//阻止等待从完成队列中读取下一个事件。这个
//事件由其标记唯一标识,在本例中为
//CallData实例的内存地址。
cq_->Next(&tag, &ok);
GPR_ASSERT(ok);
static_cast<CallData*>(tag)->Proceed();
}
}
Shutting Down the Server
关闭服务器
We’ve been using a completion queue to get the async notifications. Care must be taken to shut it down after the server has also been shut down.
我们一直在使用一个完成队列来获取异步通知。在服务器也关闭后,必须小心关闭它。
Remember we got our completion queue instance cq_
in ServerImpl::Run()
by running cq_ = builder.AddCompletionQueue()
. Looking at ServerBuilder::AddCompletionQueue
’s documentation we see that
请记住,我们通过运行cq_=builder.AddCompletionQueue()在ServerImpl::Run()中获得了完成队列实例cq_。查看ServerBuilder::AddCompletionQueue的文档,我们可以看到
… Caller is required to shutdown the server prior to shutting down the returned completion queue.
…要求调用方在关闭返回的完成队列之前关闭服务器。
Refer to ServerBuilder::AddCompletionQueue
’s full docstring for more details. What this means in our example is that ServerImpl's
destructor looks like:
有关更多详细信息,请参阅ServerBuilder::AddCompletionQueue的完整文档字符串。在我们的例子中,这意味着ServerImpl的析构函数看起来像:
~ServerImpl() {
server_->Shutdown();
// Always shutdown the completion queue after the server.
//始终在服务器之后关闭完成队列。
cq_->Shutdown();
}
You can see our complete server example in greeter_async_server.cc.
可以在greetiner_async_server.cc中看到我们完整的服务器示例。
Last modified February 17, 2022: rename RegisterAsyncService to RegisterService (#891) (8f2378a)