muduo库实现rpc框架和http框架
1.muduo库实现异步(非阻塞)rpc框架
实现客户端和服务器时,我们要做的工作有:
- 客户端:调用rpc函数时,需要和服务器端建立连接,并发送request,并设置response返回时的回调函数。这些功能都需要我们自己实现,本项目在RpcChannel类中进行了实现。
- 发送request的功能定义在
RpcChannel::CallMethod()
中,CallMethod()为RpcChannel的父类::google::protobuf::RpcChannel
中的方法,我们自己需要实现它。实现它以后,在使用时就可以将其注册到SudokuService::Stub
中,那么CallMethod()会在stub调用远程函数时自动被执行。 - 接收response并处理,这也都是我们自定义的行为,而不是protobuf框架中的东西。
- 发送request的功能定义在
- 服务器端:开启一个tcp服务器,然后定义如下内容
- 注册service:继承proto中生成的service类,然后实现其中的函数,将这个类放进services_中。
- 连接到来时的回调函数:muduo中每个连接封装成一个TcpConnection,在rpc实现中,每个TcpConnection与一个RpcChannel绑定,所以建立每个连接以后,都会创建一个RpcChannel。
- 远程方法调用的请求到来时的回调函数:调用protobuf的提供的函数将二进制request解析为cpp格式的类型对象,查看request中请求的方法,并调用,从而的到一个reponse。最后将response进行序列化发送回去。
protobuf在服务器和客户端实现中的作用总结:基于protobuf实现的rpc,protobuf生成了我们在.proto
中定义的service和method,一般service和method对应类和方法。客户端和服务器端都会生成一份类和方法。
- 在客户端中我们通过重写
::google::protobuf::RpcChannel::CallMethod()
,来实现将request发送给服务器端的功能。解释:protobuf在客户端和服务器端生成了一个同名的函数,客户端调用这个函数时会调用CallMethod(),我们需要在CallMethod()中实现网络通信功能,即将request通过socket发送出去。 - 服务器端本身只使用了protobuf序列化和反序列的功能。但是在使用服务器端时,我们需要继承对应的service类并实现service中的方法,解析request以后,会调用对应service中的方法,然后将response返回。
长连接:使用的是长连接,不过暂时还没考虑连接的定时清除问题。客户端在不用时,需要自己关闭连接。
rpc框架是异步的非阻塞的:设置好回调函数,当reponse返回时就会调用回调函数进行处理。【TcpClient的实现还没搞懂,先不说了】
1.1 rpc服务器
1.1.1 rpc服务器的实现
1.首先使用protobuf定义rpc的service、method、request和response数据类型:
enum MessageType
{
REQUEST = 1;
RESPONSE = 2;
ERROR = 3; // not used
}
enum ErrorCode
{
NO_ERROR = 0;
WRONG_PROTO = 1;
NO_SERVICE = 2;
NO_METHOD = 3;
INVALID_REQUEST = 4;
INVALID_RESPONSE = 5;
TIMEOUT = 6;
}
message RpcMessage
{
required MessageType type = 1;
required fixed64 id = 2;
optional string service = 3;
optional string method = 4;
optional bytes request = 5;
optional bytes response = 6;
optional ErrorCode error = 7;
}
本项目的rpc request的格式如下:
- 1.RPC0 4-byte
- 2.然后就是使用protobuf自动生成的代码将RpcMessage格式的数据转换为特定的二进制,具体可以参考链接,就是tag+(len)+value的形式。
- 3.Adler-32校验和 4-byte
【注】在muduo/examples/protobuf/rpc中,client发送request时,会将Adler-32校验和放在buffer对象预留的八个字节里的后四个字节中。但是server接收到数据的时候,并不会将这四个字节的Adler-32校验和放在buffer对象预留的八个字节里的后四个字节中,而是直接放在数据部分。所以server接收的二进制中的最前面会比client发送的二进制多一些0。
2.server的执行流程:
RpcServer::onConnection() // 接收连接
RpcChannelPtr channel(new RpcChannel(conn));
conn->setContext(channel); // TcpConnectionPtr conn // 每个TcpConnection与一个RpcChannel绑定
TcpConnection::onMessage()
RpcCodec::onMessage() // 解码得到RpcMessage
RpcServer::onRpcMessage()
// parse request,prepare empty response
// find service, then service->CallMethod( )
SudokuService::CallMethod() override // 代码生成器生成的代码,在其中调用SudokuServiceImpl::Solve()
SudokuServiceImpl::Solve() // 我们自己实现的
done() // 执行完Solve()就会执行done()
// done()将response发送出去
// 资源释放
TcpConnection::onMessage()
处理客户端发送的rpc请求。RpcCodec::onMessage()
将rpc请求解码成RpcMessage。RpcServer::onRpcMessage()
从RpcMessage中解析出service和method,然后调用对应的method。- method执行完成以后,会执行done(),将结果发送给客户端,并且释放资源。
3.RpcServer实现的伪代码:
class RpcServer {
std::map<std::string, Service*> services_; // 存储所有Service
onRpcMessage(const TcpConnection& conn, const RpcMessage& message) { // 收到RpcMessage,RpcMessage是在protobuf中定义的。
Service* service = services_[message.service()]; // 根据service名字找到对应的Service
const ServiceDescriptor* desc = service->GetDescriptor();
const MethodDescriptor* method = desc->FindMethodByName(message.method()); // 根据method名字从service中找到对应的Method
Message* request = Deserialize(message.request()); // 反序列化request
Message* response = service->GetResponsePrototype (method).New(); // 根据method找到对应的response类型,然后new一个出来
int64_t id = message.id(); // 使用id来标识每个请求,这样就不用先来的请求一定先回复,而是谁先处理完谁先回复
service->CallMethod(method, NULL /* RpcController */, request, response,
[=] { // 匿名函数 代表执行完method得到response以后,所执行的回调函数
RpcMessage message;
message.set_type(RESPONSE);
message.set_id(id);
message.set_response(response->SerializeAsString()); // 将response序列化为string
codec_.send(conn, message); // 发送response
delete request;
delete response;
});
}
};
解析RpcMessage,取出需要调用的方法。service->CallMethod()
会执行需要调用的方法,执行完以后,会执行后面的匿名函数将response发送给客户端。
1.1.2 service的实现
上面讲解了rpc服务器的实现,但还没有说明rpc服务器中的service是如何实现的,接下来我来介绍一下。
1.假设需要一个SudokuService,首先就需要使用protobuf定义一个Service,如下:
package sudoku;
// option cc_generic_services = false;
option java_generic_services = true;
option py_generic_services = true;
option java_package = "com.chenshuo.muduo.protorpc.sudoku";
option java_outer_classname = "SudokuProto";
message SudokuRequest {
required string checkerboard = 1;
}
message SudokuResponse {
optional bool solved = 1 [default=false];
optional string checkerboard = 2;
}
service SudokuService {
rpc Solve (SudokuRequest) returns (SudokuResponse);
}
2.使用protoc可以将上述代码会生成名为SudokuService的C++类,下面是对SudokuService的简单解析,了解即可。
class SudokuService : public ::google::protobuf::Service {
typedef SudokuService_Stub Stub;
// 客户端和服务器都会继承SudokuService,用于实现SudokuService_Stub和SudokuServiceImpl。
virtual void Solve(::PROTOBUF_NAMESPACE_ID::RpcController* controller,
const ::sudoku::SudokuRequest* request,
::sudoku::SudokuResponse* response,
::google::protobuf::Closure* done);
// Closure是std::function<void()>
// ServiceDescriptor用于说明Service中有哪些method,method的参数和返回值
const ServiceDescriptor* GetDescriptor();
// server端根据request,调用相应的回调函数(如Solve)
void CallMethod(const MethodDescriptor* method,
RpcController* controller,
const Message* request,
Message* response,
Closure* done);
};
void SudokuService::CallMethod(const MethodDescriptor* method,
RpcController* controller,
const Message* request,
Message* response,
Closure* done) {
GOOGLE_DCHECK_EQ(method->service(), file_level_service_descriptors_sudoku_2eproto[0]);
switch(method->index()) { // 根据method的索引调用对应的函数
case 0:
Solve(controller, DownCast<const SudokuRequest*>(request),
DownCast<SudokuResponse*>(response),
done);
break;
default:
GOOGLE_LOG(FATAL) << "Bad method index; this should never happen.";
break;
}
}
3.实现Solve():
class SudokuService_Stub : public SudokuService {
public:
SudokuService_Stub(::PROTOBUF_NAMESPACE_ID::RpcChannel* channel);
void Solve(::PROTOBUF_NAMESPACE_ID::RpcController* controller,
const ::sudoku::SudokuRequest* request,
::sudoku::SudokuResponse* response,
::google::protobuf::Closure* done);
private:
::PROTOBUF_NAMESPACE_ID::RpcChannel* channel_;
};
1.2 rpc客户端
1.rpc客户端的执行流程:
// 发送rpc请求
Client calls SudokuService_Stub::Solve(Closure* done )
RpcChannel::CallMethod()
// 创建RpcMessage request
// 将回调函数done放入outstanding map中,会在收到response以后被拿出来调用
// 接收rpc response
TcpConnection::onMessage()
RpcCodec::onMessage() // 解析出RpcMessage
RpcChannel::onRpcMessage() // 从outstanding中找到done回调函数
done->Run()
// Free resource
- 客户端调用Solve(),Solve()会调用RpcChannel::CallMethod()将rpc请求发送出去。
- 当接收rpc response时,会触发TcpConnection::onMessage(),进而调用一系列处理函数。
下面rpc客户端使用的演示代码:
RpcChannel channel;
TcpConnectionPtr conn; // an established TCP connection
conn->setMessageCallback(
::bind(&RpcChannel::onMessage, &channel_ ,1, _2, 3));
channel.setConnection(conn) ;
SudokuService::Stub stub(&channel); // channel可以被多个Stub一起用
SudokuRequest request;
request.set_checkerboard("001030");
SudokuResponse* response = new SudokuResponse ;
stub.Solve(NULL /* RpcController */, &request, response, [response](){
LOG_INFO << response->DebugString(); // 打印响应
});
2.使用protoc可以生成名为SudokuService_Stub的C++类,下面是对SudokuService_Stub的简单解析,了解即可。
class SudokuService_Stub : public SudokuService {
public:
SudokuService_Stub(::PROTOBUF_NAMESPACE_ID::RpcChannel* channel);
SudokuService_Stub(::PROTOBUF_NAMESPACE_ID::RpcChannel* channel,
::PROTOBUF_NAMESPACE_ID::Service::ChannelOwnership ownership);
~SudokuService_Stub();
inline ::PROTOBUF_NAMESPACE_ID::RpcChannel* channel() { return channel_; }
void Solve(::PROTOBUF_NAMESPACE_ID::RpcController* controller,
const ::sudoku::SudokuRequest* request,
::sudoku::SudokuResponse* response,
::google::protobuf::Closure* done);
private:
::PROTOBUF_NAMESPACE_ID::RpcChannel* channel_;
bool owns_channel_;
GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(SudokuService_Stub);
};
3.RpcChannel的实现:
class RpcChannel : public ::google::protobuf::RpcChannel
{
public:
void CallMethod(const ::google::protobuf::MethodDescriptor* method,
::google::protobuf::RpcController* controller,
const ::google::protobuf::Message* request,
::google::protobuf::Message* response,
::google::protobuf::Closure* done) override;
private:
struct OutstandingCall
{
::google::protobuf::Message* response;
::google::protobuf::Closure* done;
};
RpcCodec codec_; // 调用 onRpcMessage()
AtomicInt64 id_;
TcpClient client_;
std::map<int64_t, OutstandingCall> outstandings_ GUARDED_BY(mutex_); // 记录哪些发送了请求但是没有收到response
};
4.发送rpc请求:
void RpcChannel::CallMethod(const ::google::protobuf::MethodDescriptor* method,
google::protobuf::RpcController* controller,
const ::google::protobuf::Message* request,
::google::protobuf::Message* response,
::google::protobuf::Closure* done)
{
RpcMessage message;
message.set_type(REQUEST);
int64_t id = id_.incrementAndGet();
message.set_id(id);
message.set_service(method->service()->full_name());
message.set_method(method->name());
message.set_request(request->SerializeAsString()); // FIXME: error check
OutstandingCall out = { response, done };
outstandings_[id] = out;
codec_.send(conn, message);
}
5.接收response:
void RpcChannel::onRpcMessage(const TcpConnectionPtr& conn,
const RpcMessagePtr& messagePtr,
Timestamp receiveTime)
{
std::map<int64_t, OutstandingCall>::iterator it = outstandings_.find(id);
OutstandingCall out = it->second;
outstandings_.erase(it);
out.response->ParseFromString(message.response());
out.done->Run();
delete out.response;
}
1.3 muduo库中rpc的实现
muduo库中rpc的实现:muduo/net/protorpc
0.使用muduo网络库的方法:设置连接回调,并在连接回调中设置消息回调,即给每个到来的连接设置消息回调。
比如RpcServer中,RpcServer::onConnection()设置为TcpServer的连接回调,连接到来时,都会设置RpcChannel::onMessage()作为消息回调。
1.RpcServer:
- 成员变量TcpServer对象用于向外提供网络服务。
- registerService():将自定义的Service放入services_中。
- onConnection():在连接建立以后,会创建一个RpcChannel,RpcChannel中传入services_。并且将RpcChannel::onMessage()设置为当前连接conn的消息回调函数,但有rpc请求的时候就会调用RpcChannel::onMessage()。
RpcChannelPtr channel(new RpcChannel(conn));
channel->setServices(&services_);
conn->setMessageCallback(
std::bind(&RpcChannel::onMessage, get_pointer(channel), _1, _2, _3));
conn->setContext(channel);
2.RpcChannel:
- RpcChannel继承自
::google::protobuf::RpcChannel
,实现了其中的CallMethod()方法用来将客户的request发送出去。SudokuService::Stub stub_;
使用RpcChannel对象进行了初始化,那么在执行stub_.Solve()时会调用CallMethod()方法用来将客户的request发送出去。 - RpcChannel():设置codec_的回调为RpcChannel::onRpcMessage()。
- outstandings_:用于记录发送的request但未接收到的response的rpc请求。
- onMessage():RpcServer::onConnection()中设置的回调函数,当消息到来时会调用此函数。底层调用了ProtobufCodecLite::onMessage()。
- onRpcMessage():
- REQUEST:从services_找到service,然后执行service->CallMethod()调用SudokuServiceImpl::Solve()和RpcChannel::doneCallback()
- RESPONSE:从outstandings_中找到request,并调用stub_.Solve()中设置的回调函数用于处理接收到的response
- doneCallback():发送response
- CallMethod():将request设置到outstandings_中,并将客户的request发送出去。
3.ProtobufCodecLite:
- onMessage():将接收到的二进制数据解析成request或response对象。然后调用RpcChannel::onRpcMessage()来处理request或response。
- send():发送request或response。
4.服务器接收并处理请求:
RpcChannel::onMessage() // 其中调用ProtobufCodecLite::onMessage()
ProtobufCodecLite::onMessage() // 将rpc客户端发送过来的二进制数据解析成request对象
RpcChannel::onRpcMessage() // 从services_找到service,然后执行service->CallMethod()调用SudokuServiceImpl::Solve()和
// RpcChannel::doneCallback()
SudokuServiceImpl::Solve() // 生成response 然后调用done.Run(),即RpcChannel::doneCallback()
RpcChannel::doneCallback() // 发送response
ProtobufCodecLite::send()
5.客户端发送request和接收response:
// 发起连接
RpcClient::connect()
RpcClient::onConnection() // 连接成功后的回调函数来设置需要发送的request
执行stub_.Solve()时会调用RpcChannel::CallMethod()方法将request设置到outstandings_中,并将客户的request发送出去。
ProtobufCodecLite::send()
// 接收回应
RpcChannel::onMessage()
ProtobufCodecLite::onMessage() // 将rpc客户端发送过来的二进制数据解析成response对象
RpcChannel::onRpcMessage() // 从outstandings_中找到request,并调用
// stub_.Solve()中设置的回调函数用于处理接收到的response
1.4 muduo-protorpc
上面介绍的是muduo/net/protorpc种实现的rpc,muduo-protorpc实现的rpc更加完善,主要有以下几点:
- 使用智能指针,而不是原始指针
- 使用std::function,而不是Closure
- 需要自己实现代码生成器,不能直接使用protoc生成代码。
muduo-protorpc中没有使用RpcController,RpcController的一个重要的功能是timeout,即在发送request一段时间以后没有收到response,就释放资源,而不是一直占着资源。所以muduo-protorpc只是一个简单的rpc,实际要用的话,推荐使用grpc。
2.muduo库实现http框架
muduo/net/http中定义了http协议解析,muduo/net/inspect定义了url的解析。
http协议解析:
- HttpContext:parseRequest()中使用状态机解析请求行和头部信息,没有对body进行解析。解析的结果放入HttpRequest对象中。
状态机:一个循环里有多个if,每个if对应一个状态,每处理完一个if,就转换到另一个状态,从而执行另外的逻辑。
【注】请求行最后有回车和换行作为结尾,每个头部都有回车和换行作为结尾。
【改进:可以只解析json格式的body】 - HttpRequest:存储http请求的内容,包括方法(get、post...)、协议版本(HTTP/1.0、HTTP/1.1)、url、url中?后面的内容、head信息、接收到http的时间。
- HttpServer:
- 主要处理逻辑在onRequest()。onRequest()中,设置长短连接,调用httpCallback_,返回并发送response。
判断请求行中的http协议版本,判断头部字段Connection
为close
还是Keep-Alive
,协议大于1.1且Keep-Alive
就要使用长连接。
【改进:如果response中的body很大就会导致内存爆掉。】 - httpCallback_用于处理request,返回response。外部定义具体的httpCallback_,并传入HttpServer中。
- 主要处理逻辑在onRequest()。onRequest()中,设置长短连接,调用httpCallback_,返回并发送response。
- HttpResponse:
- 在构造函数中定义是否长连接。
- 存储http response的内容,包括head、状态码、是否是长连接、
Content-Length:
、body。短连接就不需要
Content-Length`,因为不存在粘包问题。
url的解析:
-
假设url为/proc/opened_files/123/24,则其中proc称为modules_,opened_files称为command,123/24会作为参数传递给当前url对应的回调函数。
【改进:request中的body可以只有json格式的数据,然后将json格式的数据作为参数传递给url的回调函数】 -
数据结构:
std::map<string, Callback> CommandList;
用于存储command对应的回调函数。
std::map<string, CommandList> modules_;
用于存储modules_对应的command列表。 -
Inspector::onRequest()
被设置为HttpServer中的httpCallback_,用于处理request,返回response。
Inspector::onRequest()
中解析url,查找modules_是否有对应的url,如果有,就调用相应的回调函数。 -
add()将url和回调函数放入modules_中。当请求到来时,解析请求中的url,从modules_取出相应的回调函数。
回调函数的输入参数:body中的json和url后面的其他参数。 -
PerformanceInspector类就是将同一个modules_的回调函数放在同一个类中。