怎么理解rpc,既然有http请求了为啥还要用rpc?

作者:Ikerli
链接:https://www.zhihu.com/question/524580708/answer/2584782720
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

先说下什么是 RPC 吧。

一、为什么会出现RPC的概念?

RPC(Remote Procedure Call),直译为中文就是远程过程调用

首先从一个简单的例子说起。假设要做一个电商系统,起步初期,你的系统很简陋,仅仅包含商品浏览、下单购买等基本流程流程。于是,你觉得用一个服务就可以包含所有的功能绰绰有余。

逐渐的,随着业务扩展,越来越多的功能累加上来,这个服务也日渐壮大,代码变得臃肿,难以维护,最后变成一坨"屎山"。

一个优秀的程序员不应该一直在一座“屎山”再加垃圾。这么大的服务,能不能拆开呢?

当然能,聪明的你立马想到,我可以按照功能来拆分服务: 例如拆出一个订单服务:只负责下单、查单等操作;一个用户服务:只负责管理用户信息...

这样,拆分出来几个服务之后,只需要将几个服务结合使用即可。这就是微服务的概念。

可是问题又来了,原来只有一个服务,自然不要考虑服务间通信的问题。但是现在有多个微服务,那么各个服务之间怎么通信呢?或者说:多个进程之间怎么通信呢

这可难不到你,好歹是看过《UNP》的人,立马想出了一百种方法:

  • 管道通信
  • 消息队列
  • 共享内存
  • 网络socket

当然,最聪明的选择只有一个,使用网络socket通信。为什么不选其他的呢,因为前几种方式都有个致命缺点,只能本机通信。但是在这个满大街都是分布式部署的年代,你不可能只把服务部署到一台机器上吧。。不会吧。

此外,通信协议也不搞花里胡哨的,不选UDP,老老实实用TCP就行了。

规划完毕,立马开始动手写程序。这写代码多是一件美事啊。

拿起键盘咔咔一顿造,这写个微服务不是有手就行吗,为了追求性能,还用了高性能的Reactor架构。总之,一个个微服务写好了,每个微服务监听在一个端口上,等待其他服务连接。

Reactor模式介绍92 赞同 · 38 评论文章正在上传…重新上传取消

比如订单服务 OrderServer, 提供了下单(make_order)查单(query_order)两个功能。OrderServer也很聪明,它告诉别人,你只需要发个数据包,数据包里面有方法名字和参数就行了,我就帮你执行对应的方法。假设现在我要在用户服务 UserServer 要去查单,应该怎么做呢:

  1. UserServer 调用 connect 连接 OrderServer
  2. 连接成功后 UserServer 制造数据包,数据包里面表示需要调用哪个方法 例如 (方法名:[query_order], 参数[data = 1999-09-09]})。
  3. UserServer 向 OrderServer发送这个数据包。
  4. OrderServer 接受到数据包后,解析出来方法名字:query_order,参数是 date=xxxx。
  5. OrderServer 执行 query_order(date)得到数据,然后制作为数据包。
  6. OrderServer 发送数据包返回给UserServer,调用结束。

这几个过程抽象出来就是无非就是 连接->编码->发送->解码->业务处理->回包。然而,每次调用都要写这么多代码,会不会有点繁琐?要是能直接像调用本地函数一样调用远程的方法就好了。哎,这不就是RPC的概念吗。我直接在 UserServer里面封装一个 query_order函数,在这个函数里面执行如下逻辑:

void query_order() {

  1. 连接 OrderServer
  2. 编码数据包,说明要调用 query_order方法,以及参数是什么
  3. send
  4. 等到回包并解码
  5. 得到结果
}

而在远端的OrderServer,当收到数据包并解析后,就转而调用自己的query_order函数,然后会送数据包就行了。

因此,直接在UserServer 直接调用 query_order,不就等同于在调用OrderServerquery_order方法了吗,这个就叫远程过程调用,也就是RPC。

这里UserServer的query_order方法,就像是OrderServer的query_order方法的一个映射一样,又可以称为服务端在客户端的存根(stub)

二、RPC如何序列化?

再来研究下 query_order 函数,之前我们没考虑参数,实际上更完整的函数应该是这样:

class QueryReq {
  string date;
};

class QueryRes {
  int list_id;
  int price;
  string time;
};

void query_order(QueryReq* req, QueryRes* res) {

}

假设要查询 2022-01-01的一笔订单,那么要这样做:

QueryReq req;
req.date = "2022-01-01";
QueryRes res;

query_order(&req, &res);

这时候你发现了个问题, req是个对象,而TCP传输是基于字节流传输的,怎么把对象转换为字节流呢?

有办法,这时候 protobuf 就英雄登场了。protobuf是Google设计的一种序列化方案,他可以将 对象转换为字节流,这个过程就成为序列化;反之,也能将字节流再转换位对象,这个过程叫反序列化。

protobuf使用请参考:

https://github.com/protocolbuffers/protobuf

因此我们的逻辑就变得简单了:

  1. 使用protobuf将 req 序列化为字节流,发送给 OrderServer
  2. 将OrderServer返回的字节流反序列化成res对象。

要使用ProtoBuf,只需要撰写proto文件:

message QueryReq {
  string date;
};

message QueryRes {
  int32 list_id;
  int32 price;
  string time;
}

然后使用 protoc命令,他会自动生成类 QueryReq 以及类 QueryRes。这两个类是继承 google::protobuf::Message的,因此可以直接调用其成员函数进行序列化和反序列化。

注意,由于UserServer 和OrderServer都需要这两个类,因此双方都需要维持同一个 proto文件。

Protobuf只是序列化的方案之一,当然也有其他的方案,如使用json等。不过 protobuf 具有轻量化、快速等优点,通常是第一选择。

三、RPC如何处理数据包?

当然,理想的情况是每次rpc调用我只发送一次,一次就是一个完整的数据包。另一方可以得到这个完整的包并且成功解析。然而事实肯定没这么容易,由于tcp是基于字节流传输的,服务端当时收到的包可能不是一个完整的包,或者说无法区别每个数据包的边界。

因此,不能说直接把QueryReq 对象序列化之后就直接丢给网络传了,而不管其他的。这让服务端怎么解包,万一收到了半个包?那边是多了一个字节,那么反序列化的结果也可能千奇百怪。另外有一点,有没有发现方法名好像也没有传输过去?那服务端怎么知道调哪个方法?

基于此,通常我们需要自定义协议,来完成数据包的制造和拆分。这个过程又可以称为编码和解码。

参考陈硕的文章,这里设计一个简单地传输协议格式如下:

char* start;    // 0x02
int32_t pk_len;
int32_t service_full_name_len;
std::string service_full_name;
pb binary data;
int32_t checksum;
char* end;      // 0x03
  • start 和 end表示一个数据包的开始和结束。
  • pk_len为整个数据包的长度。
  • service_full_name_len 为方法名的长度
  • service_full_name 为方法名,如 "query_name"
  • data就是请求对象的序列化后的二进制数据
  • checksum是校验和,用于接收端校验是否数据包数据有变化

这里不需要考虑字节对齐,直接按顺序组合以上结构就行了。

综上,发送端在发送数据时需要按照这个格式编码,而服务端接受到数据时需要按这个格式解码。

注意:这个协议只是一个很简单的设计,实际业务中通常会附带更多的参数来保证可靠。

到此,完整的rpc逻辑应该就是这样的:

  1. client进行connect连接
  2. client序列化req
  3. client根据约定的协议编码,向server发送编码后的数据
  4. server接收到数据,解码得到方法名和序列化过后的req二进制流
  5. server根据方法名找到req的类型,反序列化得到req对象
  6. server调用本地方法得到res
  7. server序列化res
  8. server根据约定的协议编码并发送数据
  9. client接收到数据并解码
  10. client反序列化得到res

四、RPC与服务注册和服务发现

注意到,RPC中有关键的第一步就是要调用connect去建立TCP连接。既然要连接,那么要拿到对方的IP:port才行吧。如果对方是单机部署就很容易,直接用这个IP:PORT就行了。

然而,又是可恶的分布式再作怪,对端的服务通常是多机部署的。这时候怎么办,随便找一台机器?那肯定不行,要是这台机器故障了不就完了。再说你只指定一台机器,对方多机部署的意义何在?

那么这个时候有个第三者就出来了。有个叫服务中心的家伙,手下管理着大量服务的IP列表:

{
  OrderServer : {
    19.3.11.11:5000,
    19.4.11.11:5000, 
    19.5.11.11:5000, 
  },
  ...
}

这时候,要想获取某个服务的地址,只需要向服务中心发送请求:

hi,我要OrderServer的IP:PORT

那么服务中心就会返回一个地址给你。这个过程就叫做服务发现

这样,你就不需要担心不知道对端的地址是什么了,只要能在服务中心查到就行。

当然,还有服务注册,这就很简单了,每个服务启动的时候,需要将他的IP:PORT以及服务名称注册到服务中心。

不过,服务发现不止这么简单。通常,一个完整的服务发现中心还需要支持负载均衡容灾处理等功能。

负载均衡:简单数就是将请求分散给各个机器上,维持各个机器的请求的均衡态势。确保不会大量请求都涌入到某几个机器上导致机器过载。

容灾处理:就是即使剔除掉故障的机器。例如某次调用19.4.11.11上的OrderServer出现故障,那么服务中心会将这个地址剔除掉,防止下次再访问到这个有故障的地址。

五、RPC 与 HTTP

PS,文末补充一下,有些读者肯还是困惑 HTTP 和 RPC 有啥关系,其实没啥关系。从概念上来入手:

HTTP: 超文本传输协议。它只是一种协议,它代表的只是一种报文格式

HTTP服务:基于 HTTP 协议运行的 TCP 服务,它在通信过程中接收 HTTP 请求报文,处理后返回 HTTP 响应报文。例如 Apache.

RPC: 远程过程调用,它代表的是一种调用远端服务的思想或者形式。RPC 完全可以用任何协议来实现,包括 HTTP、基于 ProtoBUF的协议、或者是 基于JSON 等。所以可以说 HTTP 服务是一种 RPC 的实现方式。

从概念就能看出来,HTTP RPC 根本不是一个层级的东西。 RPC 可以把他当成一种思想,他强调的是调用远程方法的这个过程,实际上他把内部的建立连接、编码、传输数据、接收数据、解码这条路径都封装好了,让你感觉就像在调用本地方法一样。

RPC 可以基于 HTTP 协议来实现,当然也可能基于其他任何一个胡编乱造的协议,只要双方约定好协议格式,能编码解码就行。所以说可以认为 HTTP 其实是 RPC 中编、解码的一种方式。事实上,在内部服务的相互调用中,通常都会自己定义协议来实现 RPC, 而不是用 HTTP,毕竟 HTTP 还是略显笨重;反之,在提供到对外的接口的时候,通常是以 HTTP 协议为基准的, 因为 HTTP 是大家公认的一份标准。

六、一个异步 RPC 框架

最近有用 C++ 结合协程的思想手撸了一个异步的 RPC 框架 -- TinyRPC,开源在 GayHub 上:

GitHub - Gooddbird/tinyrpc: c++ rpc

TinyRPC 是多线程模型,基于主从 Reactor 的网络模型。因此它的效率还是十分不错的。

TinyRPC 支持两种协议:

  1. HTTP 协议:非常简单的实现了 HTTP 协议报文的解析,只是个 demo。
  2. TinyPB 协议:使用 Protobuf 自定义的序列化协议。

另外更重要的一点是由于引入了协程,通过良好的封装,在 TinyRPC 里面进行 RPC 调用是完全异步的,但是写法与同步的无异,即同步写法实现了异步的性能不用写任何的回调函数。

结语

文章很多概念都是泛泛而谈(因为有些我不太懂。。),读者若需要更深入了解,请自行查阅相关文档。

到此,RPC的简单介绍就结束了,在下一篇文章中,将会使用之前写的一个简单的RPC框架tinyrpc来实现RPC通信的具体例子。

只用 10 行代码搭建一个高性能 RPC 服务 -- TinyRPC 框架应用篇9 赞同 · 0 评论文章

如果你觉得这篇文章对你有帮助,麻烦来个点赞、关注、收藏三连吧,分享不易,感谢支持作者创作!

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值