RPC与restful
什么是restful
REST就是一种设计API的模式。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,以JSON格式编写的REST风格的API具有简单、易读、易用的特点。
- 每一个URI代表一种资源;
- 客户端和服务器之间,传递这种资源的某种表现层;
- 客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
什么是RPC
RPC指远程过程调用(Remote Procedure Call),它的调用包含传输协议和编码(对象序列)胁议等,允许运行于一台计算机上的程序调用另一台计算机上的子程序,而开发入员无须额外为这个交互作用编程,就像对本地函数进行调用一样方便。
有关RPC的想法至少可以追溯到1976年以“信使报”(Courier)的名义使用。RPC首次在UNIX平台上普及的执行工具程序是SUN公司的RPC(现在叫ONC RPC)。它被用作SUN的NFC的主要部件。ONC RPC今天仍在服务器上被广泛使用。 另一个早期UNIX平台的工具是“阿波罗”计算机网络计算系统(NCS),它很快就用做OSF的分布计算环境(DCE)中的DCE/RPC的基础,并补充了DCOM。
远程过程调用是一个分布式计算的客户端-服务器(Client/Server)的例子,它简单而又广受欢迎。 远程过程调用总是由客户端对服务器发出一个执行若干过程请求,并用客户端提供的参数。执行结果将返回给客户端。 由于存在各式各样的变体和细节差异,对应地派生了各式远程过程调用协议,而且它们并不互相兼容。
为了允许不同的客户端均能访问服务器,许多标准化的 RPC 系统应运而生了。其中大部分采用接口描述语言(Interface Description Language,IDL),方便跨平台的远程过程调用。
它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想
** 是一种技术思想,或者说框架而非一种规范或协议**
类似于gin是一种web框架的感觉
简单解释RPC
- 调用者(客户端Client)以本地调用的方式发起调用;
- Client stub(客户端存根)收到调用后,负责将被调用的方法名、参数等打包编码成特定格式的能进行网络传输的消息体;
- Client stub将消息体通过网络发送给服务端;
- Server stub(服务端存根)收到通过网络接收到消息后按照相应格式进行拆包解码,获取方法名和参数;
- Server stub根据方法名和参数进行本地调用;
- 被调用者(Server)本地调用执行后将结果返回给server stub;
- Server stub将返回值打包编码成消息,并通过网络发送给客户端;
- Client stub收到消息后,进行拆包解码,返回给Client;
- Client得到本次RPC调用的最终结果
举个例子,你在家里,要拿一本书,你直接去拿,这就像是你在本地调用函数。在本地调用中,函数体是直接通过函数指针来指定的,我们调用函数时,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。
如果你不在家,你要拿书,怎么办,你要打电话告诉别人什么书,书在哪,而这就类似于调运远程函数。
而RPC就是要像调用本地的函数一样去调远程函数。
而在这个过程中,很明显,会有很多问题
你怎么找到你要用的函数
像之前说得,在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
客户端怎么把参数值传给远程的函数呢?
在本地的话,你可以直接传参。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
数据准备好了之后,如何进行传输?
远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC就用了HTTP2。
只要做到了这三点,这就是一个符合标准的rpc框架。
或者说简单一点来说:
- 解决分布式系统中,服务之间的调用问题。
- 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。
具体一点,分开解释(前端后端)
Client端
Student student = Call(ServerAddr, addAge, student)
- 将这个调用映射为Call ID。
- 将Call ID,student(params)序列化,以二进制形式打包
- 把2中得到的数据包发送给ServerAddr,这需要使用网络传输层
- 等待服务器返回结果
- 如果服务器调用成功,那么就将结果反序列化,并赋给student,年龄更新
// Server端
- 在本地维护一个Call ID到函数指针的映射call_id_map,可以用Map<String, Method> callIdMap
- 等待服务端请求
- 得到一个请求后,将其数据包反序列化,得到Call ID
- 通过在callIdMap中查找,得到相应的函数指针
- 将student(params)反序列化后,在本地调用addAge()函数,得到结果
- 将student结果序列化后通过网络返回给Client
gRPC
RPC只是描绘了 Client 与 Server 之间的点对点调用流程,包括 stub、通信、RPC 消息解析等部分,在实际应用中,还需要考虑服务的高可用、负载均衡等问题,所以产品级的 RPC 框架除了点对点的 RPC 协议的具体实现外,还应包括服务的发现与注销、提供服务的多台 Server 的负载均衡、服务的高可用等更多的功能。 目前的 RPC 框架大致有两种不同的侧重方向,一种偏重于服务治理,另一种偏重于跨语言调用。
服务治理型的 RPC 框架有Alibab Dubbo、Motan 等,这类的 RPC 框架的特点是功能丰富,提供高性能的远程调用以及服务发现和治理功能,适用于大型服务的微服务化拆分以及管理,对于特定语言(Java)的项目可以十分友好的透明化接入。但缺点是语言耦合度较高,跨语言支持难度较大。
跨语言调用型的 RPC 框架有 Thrift、gRPC、Hessian、Finagle 等,这一类的 RPC 框架重点关注于服务的跨语言调用,能够支持大部分的语言进行语言无关的调用,非常适合于为不同语言提供通用远程服务的场景。但这类框架没有服务发现相关机制,实际使用时一般需要代理层进行请求转发和负载均衡策略控制。
gRPC是Google开发的高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。 它的目标的跨语言开发,支持多种语言, 服务治理方面需要自己去实现,所以要实现一个综合的产品级的分布式RPC平台还需要扩展开发。Google内部使用的也不是gRPC,而是Stubby,gRPC是在Stubby基础上修改并开源的。
但是 Stubby 并不基于任何标准,并且与谷歌内部基础设施紧密耦合,不适合公开发布。随着 SPDY、HTTP/2 和 QUIC 的出现,公共标准中出现了许多Stubby 没有提供的其他特性。面对新标准和新特性的出现,谷歌决定对Stubby进行改造,以利用新标准化,并将其适用性扩展到移动、物联网和云,成为一个外界可用的通用远程过程调用框架。
ProtoBuf
Protobuf ( Protocol Buffers)是一种与语言、平台无关,且可扩展的序列化结构化数据的数据描述语言,通常称其为IDL,常用于通信协议、数据存储等,与JSON、XML相比,它更小、更快,因此也更受开发人员的青睐。
基础语法
syntax = "proto3";
package helloworld;
service Greeter{
rpc SayHello (HelloRequest) returns (HelloReply) ()
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
- 第一行(非空的非注释行)声明使用的是proto3语法。如果不声明,则默认使用proto2语法。
- 定义名为Greeter的RPC服务(Service) ,其中,RPC 方法为SayHello入参为HelloRequest消息体(message) ,出参为HelloReply消息体。
定义HelloRequest HelloReply消息体,每一个消息体的字段均包含三个属性: 类想字段名称和字段编号在定义消息体时,除类型外均不可重复。
在编写完.proto文件后,会生成对应语言的.proto文件,这时Protobuf 编译器会根据选择的语言和调用的插件情况,生成相应语言的Service Interface Code和Stubs.
举个例子
把他用于grpc
首先,明确一下接口的详情:
syntax = "proto3";
option java multiple files = true;
option java_ package = "io.grpc. examples . helloworld";
option java_ outer classname = "HelloWorldProto";
package helloworld ;
serviceGreete {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1 ;
}
message HelloReply {
string message= 1;
}
生成框架代码
protoc proto/service.proto --go_out=plugins=grpc:service
我们就可以在 service 目录下发现生成的 Go 语言代码,然后我们看到文件:service/proto/service.pb.go,会发现已经生成了我们的函数:
func (c *greeterClient) SayHello(ctx context.Context, in *HellowRequest, opt ...grpc.CallOption) (*HelloReply, error) {
out := new(HellowReply)
err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello",in,out,c.cc,opts...)
if err != nil {
return nil,err
}
return out, nil
}
重新根据我们自己的逻辑编辑它即可,以及编写一段服务器的代码,用来驱动这个 service
grpc优点
性能好
grpc用的IDL是ProtoBuf.protobuf在客户端和服务端都能快速进行序列化,并且序列化结果较小,能够有效节省传输占用的数据大小。
而且,它是基于http2协议进行设计的。
- 二进制
- http2是公开的协议
- 有实践,支持手机浏览器
- 支持stream流控
......
为什么用ProtoBuf
A、性能号,效率高
序列化后字节占用空间比XML少3-10倍,序列化的时间效率比XML快20-100倍。
B、有代码生成机制
将对结构化数据的操作封装成一个类,便于使用。
C、支持向后和向前兼容
当客户端和服务器同时使用一块协议的时候, 当客户端在协议中增加一个字节,并不会影响客户端的使用
D、支持多种编程语言
Protobuf目前已经支持Java,C++,Python、Go、Ruby等多种语言。
E、在开发过程中,开发者不必太过在意其内容
代码生成方便
在代码生成上,只需用一个proto文件就能够定义gRPC服务和消息体的约定。gRPC及其生态圈提供了大量的工具,可从proto文件中生成服务基类、消息体、客户端等代码,也就是说,客户端和服务端共用一个proto 文件就可以了,保证了IDL的一致性,且减少了重复工作。
流传输。
gRPC通过HTTP2对流传输提供了大量的支持。
)超时和取消。
gRPC允许客户端设置截止时间,若超出截止时间,则本RPC请求会被取消,与此同时,服务端也会收到取消动作的事件,因此客户端和服务端都可以在到截至时间后对取消事件进行相关处理。
根据Go语言的上下文(context) 特性,截止时间是可以一层层传递下去的。也就是说,我们可以通过一层层gRPC调用来进行上下文的传播截止日期和取消事件,这有助于我们处理一些上下游的连锁等问题。
grpc缺点
- 可读性差
- 对浏览器支持有限
- 外部组件支持较差