RPC(Remote Procedure Call)远程过程调用,是在分布式系统中,不同节点之间的一种调用方式,可以理解为,在 A 服务器上,调用 B 服务器上应用提供的函数/方法,RPC 由客户端发起,调用服务端的方法进行通信,然后服务端把结果再返回给客户端。
RPC 的核心点有两个:「通信协议」和「序列化」。序列化和反序列化是一种把传输数据进行编码和解码的方式,常见的编解码方式有 JSON、Protobuf 等。
RPC 调用的流程:(图片来自百度百科)
网络通信一般是通过 Socket 通信。
在 Go SDK 中,内置了 net/rpc 包来实现 RPC。net/rpc 包提供了通过网络访问服务端对象方法的能力。
我们通过一个加法运行来展示RPC的调用,服务端示例:
server/math_server.go
package server
type MathService struct {
}
type Args struct {
A, B int
}
func (m *MathService) Add(args Args, reply *int) error {
*reply = args.A + args.B
return nil
}
上面代码中:
有了这个服务对象,就可以把它注册到暴露的服务列表中,来提供其他客户端的使用。注册 RPC 服务对象,通过 RegisterName 方法即可,代码如下:
server_main.go
package main
import (
"log"
"net"
"net/rpc"
"rpctest/server"
)
func main() {
rpc.RegisterName("MathService", new(server.MathService))
l, err := net.Listen("tcp", ":8088") //注意 “:” 不要忘了写
if err != nil {
log.Fatal("listen error", err)
}
rpc.Accept(l)
}
上面代码中:
net/rpc 提供的RPC 框架,要想把一个对象注册为 RPC 服务,可以让客户端远程访问,那么该对象(类型)的方法必须满足如下条件:
总结就是:
func (t *T) MethodName(argType T1, replyType *T2) error
此处 T1、T2都是可以被 encoding/gob 序列化的。
我们完成了 RPC 服务,接下来继续完成客户端的调用:
client_main.go
package main
import (
"fmt"
"log"
"net/rpc"
"rpctest/server"
)
func main() {
client, err := rpc.Dial("tcp", "localhost:8088")
if err != nil {
log.Fatal("dialing")
}
args := server.Args{A: 1, B: 2}
var reply int
err = client.Call("MathService.Add", args, &reply)
if err != nil {
log.Fatal("MathService.Add error", err)
}
fmt.Printf("MathService.Add: %d+%d=%d", args.A, args.B, reply)
}
❝上面代码中:
❞
服务端和客户端我们已经完成,整体目录结构:
接下来开始运行:
go run server_main.go
go run client_main.go
运行结果:
MathService.Add: 1+2=3
看到如上结果,说明 RPC 调用成功!✿✿ヽ(°▽°)ノ✿
RPC 除了可以通过 TCP 协议调用之外,还可以通过HTTP 协议进行调用,还是通过内置的 net/rpc 包便可调用,我们将上门的代码改成 HTTP 协议的调用,服务端代码:
server_main.go
func main() {
rpc.RegisterName("MathService", new(server.MathService))
rpc.HandleHTTP()//新增的
l, err := net.Listen("tcp", ":8088")
if err != nil {
log.Fatal("listen error:", err)
}
http.Serve(l, nil)//换成http的服务
}
客户端部分代码修改:
client_main.go
func main() {
client, err := rpc.DialHTTP("tcp", "localhost:8088")
//此处省略其他没有修改的代码
}
修改完成后,我们分别运行服务端和客户端,结果和 tcp 连接时是一样的。
net/rpc 包提供的 HTTP 协议的 RPC 还有一个调试的 URL,运行服务端代码后,在浏览器中访问 http://localhost:8088/debug/rpc 回车,即可看到服务端注册的RPC 服务,以及每个服务的方法,如图:
如图,注册的 RPC 服务、方法的签名、已经被调用的次数都可以看到。
上面实现的 RPC 服务是基于 gob 编码的,这种编码在跨语言调用的时候比较困难,但是在 RPC 服务的实现者和调用者往往都可能是不同的编程语言,因此我们实现的 RPC 服务要支持多语言的调用。
要实现跨语言的 RPC 服务,核心在于选择一个通用的编码,比如常用的 JSON。在 Go 语言中,只需要使用 net/rpc/jsonrpc
包便可实现一个 JSON RPC 服务。
我把上面的示例改造成支持 JSON的RPC 服务,服务端代码如下:
server_main.go
func main() {
rpc.RegisterName("MathService", new(server.MathService))
l, err := net.Listen("tcp", ":8088")
if err != nil {
log.Fatal("listen error:", err)
}
for {
conn, err := l.Accept()
if err != nil {
log.Println("jsonrpc.Serve: accept:", err.Error())
return
}
//json rpc
go jsonrpc.ServeConn(conn)
}
}
❝上面的代码中,对比 gob 编码的RPC 服务,JSON 的 RPC 服务是把链接交给了
jsonrpc.ServeConn
这个函数处理,达到了基于 JSON 进行 RPC 调用的目的。 ❞
JSON RPC 的客户端代码修改的部分如下所示:
client_main.go
func main() {
client, err := jsonrpc.Dial("tcp", "localhost:8088")
//省略了其他没有修改的代码
}
只需要把建立链接的 Dial方法换成 jsonrpc 包中的即可。
Go 语言内置的jsonrpc 并没有实现基于 HTTP的传输,这里参考 gob 编码的HTTP RPC 实现方式,来实现基于 HTTP的JSON RPC 服务。
server_main.go
func main() {
rpc.RegisterName("MathService", new(server.MathService))
//注册一个path,用于提供基于http的json rpc服务
http.HandleFunc(rpc.DefaultRPCPath, func(rw http.ResponseWriter, r *http.Request) {
conn, _, err := rw.(http.Hijacker).Hijack()
if err != nil {
log.Print("rpc hijacking ", r.RemoteAddr, ": ", err.Error())
return
}
var connected = "200 Connected to JSON RPC"
io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")
jsonrpc.ServeConn(conn)
})
l, err := net.Listen("tcp", ":8088")
if err != nil {
log.Fatal("listen error:", err)
}
http.Serve(l, nil)//换成http的服务
}
❝上面代码实现基于 HTTP 协议的核心,使用 http.HandleFunc 注册了一个 path,对外提供基于 HTTP 的 JSON RPC 服务。在这个 HTTP 服务的实现中,通过 Hijack 方法劫持链接,然后转交给 jsonrpc 处理,这样就实现了基于 HTTP 协议的 JSON RPC 服务。 ❞
客户端调用代码:
func main() {
client, err := DialHTTP("tcp", "localhost:8088")
if err != nil {
log.Fatal("dialing:", err)
}
args := server.Args{A:1,B:2}
var reply int
err = client.Call("MathService.Add", args, &reply)
if err != nil {
log.Fatal("MathService.Add error:", err)
}
fmt.Printf("MathService.Add: %d+%d=%d", args.A, args.B, reply)
}
// DialHTTP connects to an HTTP RPC server at the specified network address
// listening on the default HTTP RPC path.
func DialHTTP(network, address string) (*rpc.Client, error) {
return DialHTTPPath(network, address, rpc.DefaultRPCPath)
}
// DialHTTPPath connects to an HTTP RPC server
// at the specified network address and path.
func DialHTTPPath(network, address, path string) (*rpc.Client, error) {
var err error
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
io.WriteString(conn, "GET "+path+" HTTP/1.0\n\n")
// Require successful HTTP response
// before switching to RPC protocol.
resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "GET"})
connected := "200 Connected to JSON RPC"
if err == nil && resp.Status == connected {
return jsonrpc.NewClient(conn), nil
}
if err == nil {
err = errors.New("unexpected HTTP response: " + resp.Status)
}
conn.Close()
return nil, &net.OpError{
Op: "dial-http",
Net: network + " " + address,
Addr: nil,
Err: err,
}
}
❝上面代码的核心在于通过建立好的 TCP 链接,发送 HTTP 请求调用远程的 HTTP JSON RPC 服务,这里使用的是 HTTP GET 方法。 分别运行服务端和客户端,就可以看到正确的 HTTP JSON RPC 调用结果了。 ❞
❝上面我们使用的是 Go 语言自带的 RPC 框架,但实际开发中,使用它的情况并不多,比较常用的是 Google 的 gRPC 框架,它是通过 Protobuf 序列化的,基于 HTTP/2 协议的二进制传输,且支持很多编程语言,效率也比较高。我们通过 RPC 的学习,再来学习 gRPC 是很容易入门的。 ❞