RPC是什么
RPC(Remote Procedure Call)是远程过程调用协议,一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。简言之,RPC使得程序能够像访问本地系统资源一样,去访问远端系统资源。
在OSI网络通信模型(应用层、表达层、会话层、传输层、网络层、数据链路层、物理层)中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
为何要有RPC
在我们业务不大的时候,大多数使用是 HTTP 协议,该协议简单,开发方便。随着系统业务增多,访问量的增大,服务的拆分和分布式部署,需要更加高效通信方式。HTTP 协议进行通信,需要每次三次握手建立连接,且不能不能建立长链接(HTTP 2 已支持),而 RPC 是长连接,没有 HTTP 协议的三次握手,减少网络开销。其次,RPC框架提供的是面向服务的封装,它针对服务的性能效率、可用性等都做了优化(比如提供了:注册中心、服务治理、负载均衡、二进制传输、熔断、服务降级等功能),是一套完整的解决方案;而HTTP调用缺少这些高级特性,它只是简单的数据通信,另外HTTP API受限于HTTP协议(要带HTTP请求头),传输效率及安全性不如RPC。
RPC 4个核心组件
客户端(client):服务调用者
客户端存根(client stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端
服务端存根(server stub):接收客户端请求的消息并进行解包,然后再调用本地服务进行处理
服务端(server):服务提供者
RPC 协议
协议是 rpc 重要一环,它主要解决客户端和服务端通信的问题,rpc协议示意图如下:
序列化协议
网络数据传输必须是二进制,执行过程是变成语言的对象方法,那么就涉及到如何将对象序列化成可传输消息(二进制),并可反序列化还原。常见的通用型协议如 XML、 JSON、Protobuf、Thrift 等,也有语言绑定的如Golang 原生支持的 Gob 协议等。
消息编码协议
它是一种客户端和服务端的调用约定,比如请求和参数如何组织,Header 放置什么内容。这部分每个框架设计均不同,有时也称这一层为狭义的 RPC 协议层。
网络传输协议
基于 TCP、UDP 还是 HTTP,UDP 要自己解决可靠性传输问题,而 HTTP 又太重,包含很多没必要的头信息,所以一般 RPC 框架会优先选择 TCP 协议(gRPC 基于 HTTP2)。
RPC 调用过程
rpc实现流程如下:
客户端(client)发起一个远程过程调用。
客户端存根(client stub)程序接收到客户端(client)调用请求,负责将调用方法名、参数等信息打包成可通过网路传输给数据包。
客户端存根(client stub)查找到远程服务器程序IP,将打包好的数据通过网络发送给服务端(Server)。
服务端存根(server stub)接收到客户端(client)的数据包信息,将数据信息解包。
服务端存根(server stub)根据解包结果调用本地服务进行相关处理。
服务端(server)执行结果返回给服务端存根(server stub)。
服务端存根(server stub)将执行结果打包成网络数据包,通过网络发送至客户端(Client)。
客户端存根(client stub)接收到消息,并进行解包。
客户端存根(client stub)将解包结果返回到客户端(client),得到最终执行结果,整个RPC调用过程结束。
Go RPC
Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。
Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:
函数必须是导出的(首字母大写)
必须有两个导出类型的参数,
第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
函数还要有一个返回值error
举个例子,正确的RPC函数格式如下:
func (t *T) MethodName(argType T1, replyType *T2) error
T、T1和T2类型必须能被encoding/gob包编解码。
HTTP RPC
服务端:
package main
import (
"log"
"net/http"
"net/rpc"
)
type Args struct {
A, B int
}
type Arith int
// Multiply RPC服务端方法,计算乘法
func (a *Arith) Multiply(args *Args, replay *int) error {
*replay = args.A * args.B
return nil
}
func main() {
// 1.注册服务
err := rpc.Register(new(Arith))
if err != nil {
return
}
// 2.服务处理绑定到http协议上
rpc.HandleHTTP()
// 3.监听服务
err = http.ListenAndServe(":9000", nil)
if err != nil {
log.Fatal(err)
}
}
客户端:
package main
import (
"fmt"
"log"
"net/rpc"
)
// Args 传的参数
type Args struct {
A, B int
}
func main() {
// 1.连接远程rpc服务
conn, err := rpc.DialHTTP("tcp", ":9000")
if err != nil {
log.Fatal(err)
}
// 2.调用方法
var area int
err = conn.Call("Arith.Multiply", Args{10, 5}, &area)
if err != nil {
log.Fatal(err)
}
fmt.Println("面积:", area)
}
TCP RPC
服务端:
package main
import (
"log"
"net"
"net/http"
"net/rpc"
)
type Args struct {
A, B int
}
type Reply struct {
C int
}
type Arith int
// Multiply RPC服务端方法,计算乘法
func (t *Arith) Multiply(args Args, reply *Reply) error {
reply.C = args.A * args.B
return nil
}
func main() {
tcpAddr, err := net.ResolveTCPAddr("tcp", ":9000")
if err != nil {
log.Fatal(err)
}
listener, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
log.Fatal(err)
}
err = rpc.Register(new(Arith))
if err != nil {
return
}
for {
conn, err := listener.Accept()
if err != nil {
continue
}
rpc.ServeConn(conn)
}
}
客户端:
package main
import (
"fmt"
"log"
"net/rpc"
)
// Args 传的参数
type Args struct {
A, B int
}
type Reply struct {
C int
}
func main() {
// 1.连接远程rpc服务
client, err := rpc.Dial("tcp", ":9000")
if err != nil {
log.Fatal("dialing", err)
}
defer client.Close()
// 2.调用方法
args := &Args{7, 8}
reply := new(Reply)
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal(err)
}
log.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply.C)
}
另外,net/rpc/jsonrpc库通过json格式编解码,支持跨语言调用
JSON RPC
服务端:
package main
import (
"log"
"net"
"net/http"
"net/rpc"
"net/rpc/jsonrpc"
)
type Args struct {
A, B int
}
type Reply struct {
C int
}
type Arith int
// Multiply RPC服务端方法,计算乘法
func (t *Arith) Multiply(args Args, reply *Reply) error {
reply.C = args.A * args.B
return nil
}
func main() {
// 1.实例化服务
listener, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatal(err)
}
// 2.注册服务
err = rpc.Register(new(Arith))
if err != nil {
return
}
// 3.启动服务
for {
conn, err := listener.Accept()
if err != nil {
continue
}
jsonrpc.ServeConn(conn)
}
}
客户端:
package main
import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
// Args 传的参数
type Args struct {
A, B int
}
type Reply struct {
C int
}
func main() {
// 1.连接远程rpc服务
conn, err := net.Dial("tcp", ":9000")
if err != nil {
log.Panicln(err)
}
// 2.调用方法
args := &Args{7, 8}
reply := new(Reply)
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal(err)
}
log.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply.C)
}
常见的 RPC 框架
gRPC:gRPC是谷歌开源的一款高性能的rpc框架 (https://grpc.io),是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。
Thrift:thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。