找回密码
 立即注册
查看: 173|回复: 0

SRPC学习系列2 :一次RPC请求过程

[复制链接]
发表于 2024-7-15 18:11 | 显示全部楼层 |阅读模式
比来给SRPC项目写几篇学习文章,但愿协助小伙伴通过这个轻量级的框架快速了解RPC相关内容。
本篇为第二篇,注重于解读一次RPC请求的过程,是最简单、最主干的部门,而里边每一个层级怎么做资源调剂和复用都不会包罗在内,因此有基础的小伙伴可以直接跳读源码解析。
整体阅读预计5分钟
1. RPC概念简述

SRPC是个轻量级、高性能、代码量很少的C++ RPC框架,目前在公司每天超百亿线上请求,兼具学习+使用的特点。项目地址:
花一点点时间补充RPC的基本概念,其字面意思是Remote Procedure Call,长途过程调用。也就是说:
- 如果我们是客户端,通过RPC调用,把某些事情交给长途机器去做;
- 如果我们是处事端,就是被调用,别人交一些事情让我做;   
那么必然涉及到三个小问题:
1. 请求是什么?
2. 怎么指定调用哪个过程?
3. 怎么给填好答复?
整个调用由客户端倡议。处事端启动处事器之后,等待他人调用。   
我们举个小例子,上述三个小问题可以这样:
1. 请求是int a和int b;
2. 指定调用对方的sum;
3. 答复是求和后的值int ret;
一会儿用这个例子看看RPC框架的代码是怎么做的。   
2. 协议与框架

我们常提到的RPC可能是一种框架:
用来帮我们做网络收发。   
比如SRPC是个轻量级的RPC框架,还有大师熟知的GRPC、Thrift等。
也可能是一种协议:
RPC协议让分歧语言、分歧框架都可以互通。
个人理解协议的本质是为了生态处事的,RPC承担的是衔接整个生态系统的桥梁。
两者关系:   
以SRPC为例,撑持多种协议,包罗SRPCThriftBRPCtRPC,也是目前独一的tRPC协议开源实现,此外还可以收发HTTP协议
我们给出一张RPC请求过程的图及此中涉及到的关键函数接口,然后正式开始下面的学习。



总图:一次RPC请求过程

这里我们看到几个有意思的事情:

  • 请求/答复,是对称的。
    对客户端Client来说,请求时发出SRPCRequest,收到SRPCResponse;
    对处事端Server来说,收到SRPCRequest,答复时发出SRPCResponse;
  • 收/发,接口是对称的。
    动员静的接口都是encode(),无论我要发的是SRPCRequest还是SRPCResponse;
    收动静的接口都是append(),无论我要收的是SRPCRequest还是SRPCResponse;
  • Client/Server,也是对称的
    Client主动发出请求,然后答复时是被动调起callback()的(哪怕我们用同步接口,那也是调用完代码再往下走);
    Server被动接收请求,然后答复是process()措置完之后主动进行的。
附上以前画的,从调用模块角度来看的one round图,协助从全局理解:



几年前画的老图:one round模块层次

然后我们开始结合代码讲解,一次RPC请求的过程。
3. 定义RPC接口

我们刚才三个小问题怎么定义呢?可以使用protobuf作为接口描述文件:
  1. // [ MyService.proto ]
  2. syntax=”proto2”;
  3. message Request { // request包含了a和b
  4.     required int32 a = 1;
  5.     required int32 b = 2;
  6. };
  7. message Response { // response包含了ret
  8.     required int32 ret = 1;
  9. };
  10. service MyService { // 处事名,用来区分我们的处事
  11.     rpc Sum(Request) returns (Response); // 调用名字为Sum的函数
  12. };
复制代码
也可以配合srpc小东西的api命令,发生一个简单的protobuf描述文件,并进行改削。命令参考如下:
  1. ./srpc api MyService
复制代码
然后就可以按照提示,打开MyService.proto并编纂此中的接口定义。
4. step-0 : client发出请求

如总图的step-0,我们想要发出请求,就需要调用上述定义的RPC接口Sum( ):
  1. // [ client_main.cc ]
  2. int main()
  3. {                                                   
  4.     MyService::SRPCClient client(”127.0.0.1”, 1412);                                                                                          
  5.     Request req;   // 筹备好Request
  6.     Response resp; // 筹备好Response
  7.     RPCSyncContext ctx; // 一些必要的请求上下文,包罗调用状态码
  8.     req.set_a(1);  // 填a
  9.     req.set_b(2);  // 填b
  10.     client.Sum(&req, &resp, &ctx); // 调用Sum()
  11.     ...
  12. }
复制代码
当然想要框架知道怎么从上述的protobuf文件进行调用,我们需要一些代码生成工作。这不是本篇的重点,因此这里仅列出一些命令供大师运行起来。   
我们按照刚才srpc小东西的示例,通过改好的proto文件把项目生成出来:
  1. ./srpc rpc my_rpc_project -f MyService.proto -p ./
复制代码
我们打开生成代码MyService.srpc.h,可以看到刚才调用的Sum()函数的异步接口和同步接口,定义如下:
  1. // [ MyService.srpc.h ]
  2. class SRPCClient : public srpc::SRPCClient                                         
  3. {                                                                                 
  4. public:
  5.     void Sum(const Request *req, SumDone done);
  6.     void Sum(const Request *req, Response *resp, srpc::RPCSyncContext *sync_ctx);
  7.     ...
  8. };
复制代码
5. step-1:框架为Client发出请求

以上,我们作为RPC的用户,代码就告一段落了。
接下来交给RPC框架干活,它要做的事情包罗但不仅限于:
把这个请求内容、以及用户要调用哪个处事(service)的哪个函数(method)告诉长途,并通过网络发送出去。
我们想要了解一个框架如何工作时,首先要了解它是基于什么构建起来的,包罗什么语言什么底层网络收发库等。  
SRPC是基于Workflow的任务流编程范式开发的,并使用了其携带的网络收发功能,因此我们可以不用手写I/O多路复用等事情,但是开发需要遵循Workflow的编程规范,即:任务流。
我们可以认为对于网络任务来说,一次会话就是一个task,对于client我们的task职责就在于把Request发给对方收回Response
继续围不雅观生成代码MyService.srpc.h,我们看一下最简单的异步接口实现是什么:
  1. // [ MyService.srpc.h ]
  2. inline void SRPCClient::Sum(const Request *req, SumDone done)                  
  3. {                                                                              
  4.     auto *task = this->create_rpc_client_task(”Sum”, std::move(done));         
  5.     task->serialize_input(req);                                                
  6.     task->start();                                                              
  7. }
复制代码
内部会构造出一个RPCClientTask,它被task->start();之后,就可以认为请求交给框架,用户态无需再关心,直到答复时框架通过回调等机制叫醒用户代码。
由于RPCClientTask的定义斗劲复杂,我们挑重点看:
  1. // [ rc/rpc_task.inl ]
  2. // 1. 它派生于Workflow的WFComplexClientTask,
  3. //    以REQ,RESP为模版,定义了内部请求与答复的格式,
  4. //    我们这里分袂是SRPCRequest和SRPCResponse。
  5. template<class RPCREQ, class RPCRESP>                                             
  6. class RPCClientTask : public WFComplexClientTask<RPCREQ, RPCRESP>
  7. {
  8.     ...
  9. protected:
  10.     // 2. SPRC框架从头实现了父类的方式message_out(),
  11.     //    用来告诉Workflow网络层面此次发出的请求内容时啥
  12.     CommMessageOut *message_out() override;
  13.     // 3. 保留了一个rpc_callback, 让网络答复了之后通知SRPC框架
  14.     //    SRPC框架再去做网络请求到用户Response的格式转换
  15.     void rpc_callback(WFNetworkTask<RPCREQ, RPCRESP> *task);
  16. };
复制代码
上述的RPCREQ就是我们发出的请求,SRPCRequest与SRPCResponse都从SRPCMessage派生:
  1. // [ src/message/rpc_message_srpc.h ]
  2. class SRPCRequest : public SRPCMessage
  3. {
  4.     ...
  5. };
  6. class SRPCResponse : public SRPCMessage
  7. {
  8.     ...
  9. };
复制代码
那么谁定义了 SRPCMessage的内存布局呢?就是SRPC协议。
下图可以清晰地看到,我们在SRPC协议头部就有meta部门,上述提到的service和method就是填在里边。尔后面的message就是我们的Request。



srpc_protocol

我们把动静按照上述布局,通过SRPCMessage::encode()接口填好。
这是Workflow的接口,它会在进行网络发送时entry->session->out->encode()被调用。
  1. // [ src/message/rpc_message_srpc.h ]
  2. inline int SRPCMessage::encode(struct iovec vectors[], int max, size_t size_limit)
  3. {
  4.     // 这里用上了RPC协议,我们按照协议布局填内容。
  5. }
复制代码
6. step-2:与操作系统相关的网络操作

这部门在Workflow中实现,涉及到的网络基础常识很多,后续会针对性展开写学习心得,包罗:

  • 定名处事
  • 方针拔取
  • 负载均衡
  • 连接打点
  • IO多路复用
等等,此刻暂时跳过。
7. server接收请求

我们切换一下视角,来到上述总图的右半边,server要接收请求了。
当然server作为一个被动接收者,它需要先被用户启动起来。
以下是用户代码:
  1. // [ server_main.cc ]
  2. int main()                                                                        
  3. {
  4.     SRPCServer server; // 1. 构造一个server,负责网络请求
  5.     MyServiceServiceImpl impl; // 2. 构造一个处事,负责实现Sum
  6.     server.add_service(&impl);  // 3. 把处事实现加到server中
  7.     if (server.start(1412) == 0)  // 4. 传入端口,把server跑起来
  8.     {
  9.         printf(”my_rpc_project SRPC server started, port %u\n”, 1412);
  10.         wait_group.wait(); // 5. server start也是异步的,暂时要卡住主线程不退出
  11.         server.stop();
  12.     }
  13.     else                                                                           
  14.         perror(”server start”);                                                   
  15.     return 0;                                                                     
  16. }
复制代码
然后就可以愉快地按照SRPC协议来接受请求了。
这是谁来做的呢?RPCServer来做的。
8. step-4:框架为server接受请求
  1. // [ src/rpc_server.h ]
  2. // 1. 从Workflow的WFServer派生
  3. //    由RPCTYPE::REQ和RPCTYPE::RESP来指定请求与答复的类型
  4. template<class RPCTYPE>
  5. class RPCServer : public WFServer<typename RPCTYPE::REQ,
  6.                                   typename RPCTYPE::RESP>
  7. {
  8. ...
  9. protected:
  10.     //  2. 需要实现怎么构造一次会话,即RPCServerTask
  11.     CommSession *new_session(long long seq, CommConnection *conn) override;
  12.     // 3. 调用具体server接口的处所
  13.     void server_process(NETWORKTASK *task) const;
  14.     ...
  15. };
复制代码
我们的父类WFServer是可以帮我们按照某种协议收网络包的,只需要:

  • 我们实现new_session(), new 一个RPCServerTask给它;
  • 在模版参中指定的RPCTYPE::REQ上实现append()接口,指引Workflow网络层面如何从操作系统收到的数据上切一份完整的REQ下来。
此中第一步不是必需的,但我们SRPC框架需要,因为我们在本次会话有一些上下文要措置。但本文中我们只需关心REQ。
这个REQ就是SRPCRequest,父类就是SRPCMessage,刚才也有提到过它的encode()实现,此刻看看它的append()实现:
  1. // [ src/message/rpc_message_srpc.cc ]
  2. int SRPCMessage::append(const void *buf, size_t *size, size_t size_limit)
  3. {
  4.     ... // 把网络收到的一批buf,按照RPC协议保留到我的内存里,
  5.     ... // 并通过返回值奉告核心我收发完没有,因为内部需要维护状态
  6. }
复制代码
Workflow会不竭调用这个append()来把SRPC协议图里的动静收完。
我们这里通过返回值来奉告Workflow的网络层本条动静的接收情况:

  • 1:动静接受完成;
  • 0:未完成,继续收;
  • < 0:错误;
只要返回1,流程就会继续往下走,也就是到了process()函数。
9. step-5:调用开发者的rpc函数

SRPC框架收完动静之后,需要对meta进行一些措置:

  • 按照meta里的service去找到用户刚才server.add_service(impl)时的阿谁service;
  • 按照meta里的method去找用户的impl里实现的函数;
然后就可以调用server端开发者实现的rpc函数了。
查找过程很简单,以下是简化的流程:
  1. // [ src/rpc_server.h ]
  2. template<class RPCTYPE>                                                            
  3. void RPCServer<RPCTYPE>::server_process(NETWORKTASK *task) const
  4. {
  5.     // 1. 把SRPC协议中的meta信息反序列化出来
  6.     req->deserialize_meta();
  7.     ...
  8.     // 2. 找service对象
  9.     auto *service = this->find_service(req->get_service_name());
  10.     ...
  11.     // 3. 找method对象
  12.     auto *rpc = service->find_method(req->get_method_name());
  13.     ...
  14.     // 4. 进一步措置
  15.     status_code = (*rpc)(server_task->worker);
  16.     ...
  17. }
复制代码
注意上述的进一步措置是因为,我们还需要对body进行反序列化:
  1. // [ src/rpc_service.h ]
  2. template<class INPUT, class OUTPUT, class SERVICE>                                 
  3. static inline int                                                                  
  4. ServiceRPCCallImpl(SERVICE *service,                                               
  5.                    RPCWorker& worker,                                             
  6.                    void (SERVICE::*rpc)(INPUT *, OUTPUT *, RPCContext *))         
  7. {
  8.     // 1. new一片请求,是一开始定义的包含a和b的Request,它是个ProtobufMessage
  9.     auto *in = new INPUT;
  10.     // 2. 按照网络包里的body,从req反序列化处出来到in上
  11.     int status_code = worker.req->deserialize(in);
  12.     // 3. new一片答复,是一开始定义的包含ret的Response,它也是个ProtobufMessage
  13.     auto *out = new OUTPUT;
  14.     // 4. 调用用户代码实现的rpc函数,进行计算
  15.     (service->*rpc)(in, out, worker.ctx);
  16. }
复制代码
之后就可以交给框架做答复返回的事情了。
10. 对称的回程

我们最后简单看一下用户代码里一般长啥样,也就是刚才impl里的rpc实现:
  1. // [ server_main.cc ]
  2. class MyServiceServiceImpl : public MyService::Service                          
  3. {                                                                              
  4. public:
  5.     void Sum(Request *request, Response *response, srpc::RPCContext *ctx) override
  6.     {
  7.         // 这里是我们本身实现的加法
  8.         response->set_ret(request->a() + request->b());                                         
  9.     }                                                                           
  10. };
复制代码
之后,用户无需进行任何代码编写,SRPC和Workflow会进行step-6和step-7,与先前的法式类似且对称地,把答复填好并发出。
而client端又会先从Workflow和SRPC进行step-8和step-9,同样与上述法式类型且对称地,把答复收好,并调用到我们的callback,或者在同步接口中(也就是文中的Sum调用示例)填好Response,此次请求就完整结束了。
  1. // [ client_main.cc ]
  2. int main()
  3. {
  4.     ...
  5.     client.Sum(&req, &resp, &ctx);
  6.     if (ctx.success)                                                               
  7.         fprintf(stderr, ”ret = %d\n”, resp.ret());            
  8.     else                                                                           
  9.         fprintf(stderr, ”sync status[%d] error[%d] errmsg:%s\n”, ctx.status_code, ctx.error, ctx.errmsg.c_str());
  10.     return 0;
  11. }
复制代码
更多内容参考:https://github.com/sogou/srpc/blob/master/docs/wiki.md
SRPC系列上一篇:1412:快速入门SRPC

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-27 22:42 , Processed in 0.112665 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表