muduo库实现rpc框架和http框架

1.muduo库实现异步(非阻塞)rpc框架

陈硕视频教程
代码
代码解析

实现客户端和服务器时,我们要做的工作有:

  • 客户端:调用rpc函数时,需要和服务器端建立连接,并发送request,并设置response返回时的回调函数。这些功能都需要我们自己实现,本项目在RpcChannel类中进行了实现。
    • 发送request的功能定义在RpcChannel::CallMethod()中,CallMethod()为RpcChannel的父类::google::protobuf::RpcChannel中的方法,我们自己需要实现它。实现它以后,在使用时就可以将其注册到SudokuService::Stub中,那么CallMethod()会在stub调用远程函数时自动被执行。
    • 接收response并处理,这也都是我们自定义的行为,而不是protobuf框架中的东西。
  • 服务器端:开启一个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库实现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协议版本,判断头部字段Connectionclose还是Keep-Alive,协议大于1.1且Keep-Alive就要使用长连接。
      【改进:如果response中的body很大就会导致内存爆掉。】
    • httpCallback_用于处理request,返回response。外部定义具体的httpCallback_,并传入HttpServer中。
  • 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_的回调函数放在同一个类中。

posted @ 2023-06-27 14:43  好人~  阅读(515)  评论(0编辑  收藏  举报