RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
上面就是关于RPC定义的介绍,其实RPC就是想实现函数调用模式的网络化。客户端就可以像调用本地函数一样,然后客户端把这些参数打包后通过网络传递到服务器端,服务器端解包到处理过程中执行,然后执行的结果反馈给客户端。
具体来说,在运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:
1.调用客户端句柄;执行传送参数
2.调用本地系统内核发送网络消息
3.消息传送到远程主机
4.服务器句柄得到消息并取得参数
5.执行远程过程
6.执行的过程将结果返回服务器句柄
7.服务器句柄返回结果,调用远程系统内核
8.消息传回本地主机
9.客户句柄由内核接收消息
10.客户接收句柄返回的数据**
Go RPC
Go在标准库中已经支持了RPC,而且支持三个级别的RPC:TCP,HTTP,JSONRPC
。但Go中的RPC又都是通过Gob
来统一编码的,因此它只能支持在Go语言开发的服务器和客户端之间的交互。
在被远程访问之前,还需要满足一些条件:
1)函数必须是导出的(首字母大写)
2)必须有两个导出类型的参数
3)第一个参数用来接收,第二个参数是返回给客户端的参数,并且第二个参数必须是指针类型
4)函数还要有一个返回值error
格式如下:
func (t *T) MethodName(arg1 argType, arg2 *argType) error
HTTP RPC
我们来看一个简单的HTTP的服务器端代码:
package main
import (
"errors"
"log"
"net/http"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("The divisor cannot be zero.")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Fatalln(err.Error())
os.Exit(1)
}
}
我们首先注册了一个Arith的RPC的服务,然后通过rpc.HandleHTTP
函数把该服务注册到HTTP协议上,最后就可以利用HTTP的方式来传递数据。
我们再来看看客户端代码:
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server")
os.Exit(1)
}
serverAddress := os.Args[1]
client, err := rpc.DialHTTP("tcp", serverAddress+":8000")
if err != nil {
log.Fatal("Dial Wrong: ",err)
}
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("Arith Wrong: ", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, &reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("Arith Wrong: ", err)
}
fmt.Printf("Arith: %d/%d=%d and %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
程序中的关键就在于这个Call
函数,它有三个参数,第一个是要调用的函数的名字,第二个要传递的参数,第三个是要返回的参数(必须是指针类型)
TCP RPC
我们仍使用前面的内容来构建基于TCP的RPC的服务器端:
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", "8000")
checkError(err)
listener, err := net.Listen("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
rpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Wrong: ", err.Error())
os.Exit(1)
}
}
在这里使用了TCP协议,然后需要自己连接控制,当有客户端连接上来后,需要把这个连接交给RPC来处理。
相应的RPC客户端代码如下:
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
os.Exit(1)
}
server := os.Args[1]
client, err := rpc.Dial("tcp", server)
if err != nil {
log.Fatal("Dial Wrong: ", err)
}
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("Arith Wrong: ", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, &reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("Arith Wrong: ", err)
}
fmt.Printf("Arith: %d/%d=%d and %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
和HTTP的客户端代码相比,唯一的区别就在于这里使用的是Dial(tcp)
函数,而前者使用的是DialHTTP
。
JSON RPC
JSON RPC采用的是json
编码的方式,但其操作方式是与上面介绍的RPC方式是基本一致的,和TCP所创建的客户端和服务器端唯一的不同在于,JSON RPC在这里是用的连接方式是jsonrpc.ServerConn(conn)
其余的内容完全一致。
代码在这里不再赘述。
RPC接口
Go语言的net/rpc
很灵活,它在传输前后实现了编码解码器的接口定义。
RPC提供的编码解码器接口如下:
type ClientCodec interface {
WriteRequest(*Request, interface{}) error
ReadResponseHeader(*Response) error
ReadResponseBody(interface{}) errror
Close() error
}
type ServerCodes interface {
ReadRequestHeader(*Request) error
ReadRequestBody(interface{}) error
WriteResponse(*Request, interface{}) error
Close() error
}
我们可以通过定义很容易的看出来两个编码器的运行过程。
接口ClientCodec
定义了RPC客户端如何在一个RPC会话中发送请求和读取响应。
接口ServerCodec
定义了RPC服务器端如何在一个RPC会话中接收请求并发送响应。