大家好,欢迎来到IT知识分享网。
RPC 意为远程过程调用或者远程方法调用,这里说的远程可能是本机的另外一个进程,但大多场景是远程的一台 tcp 服务器,Web HTTP Api 访问虽然方便,但是面对复杂的业务的时候封装查询参数往往就很复杂了,RPC 调用在调用方生成动态代理接口对象,调用远程的方法就就像是调用本地方法一样,提高了易用性。
动态代理接口对象的主要工作是:
1. 识别要访问的远程方法的 IP 和端口
2. 将调用方法名、参数进行序列化
3. 将通讯的请求发送给远端服务器
4. 接受远程服务器返回的调用结果
这个过程比较重要的是通讯协议和序列化协议,通讯就是 tcp 连接通讯,而序列化是将对象的状态信息转换为可传输和可存储的过程,反序化就是从这些可传输、可存储的数据中恢复对象实例和它的状态,甚至是方法定义。在 go 语言中,这个过程使用 encoding/gob 完成,gob 对 go 就像 Serialization 对 java、pickle 对 python,这些序列化方案都是语言内部的,需要跨语言的描述就需要 xml、json、protocol buffers 序列化方案了。
下面的例子把 person 结构体实例序列化到终端显示,由于序列化的结果并不全是文本,所以显示不规整:
type person struct { Name string Age int } p1 := person{"zhangsan", 20} enc1 := gob.NewEncoder(os.Stdout) enc1.Encode(p1)
当然也能存成文件:
file1, _ := os.OpenFile("/tmp/person.gob", os.O_CREATE|os.O_WRONLY, 0644) enc2 := gob.NewEncoder(file1) enc2.Encode(p1) file1.Close()
这时候如果把 /tmp/person.gob 通过 传给好友,那么好友也能读取这个文件恢复成一个 person 结构体对象:
var p2 person file2, _ := os.Open("/tmp/person.gob") defer file2.Close() dec := gob.NewDecoder(file2) dec.Decode(&p2)
在 PRC 远程调用的过程中,对象也是通过被序列化后通过网络传输给服务方,服务方把序列化的数据恢复成对应的结构体对象,用传过来的参数调用它的方法后返回结果,调用方只需要知道方法的签名就行了,不必知道它的具体实现过程,这个叫存根(stub)。
go 语言的 net/rpc 包提供了编写 RPC 调用的支持,首先定义一下 rpc 的服务端,它是真正提供实现的一方:
type HelloService struct{} func (hs *HelloService) Say(name string, reply *string) error { *reply = "hi, " + name return nil } func startHelloRpcServer() { rpc.RegisterName("HelloService", new(HelloService)) listener, _ := net.Listen("tcp", ":3000") //使用 for 循环服务多个客户端 conn, _ := listener.Accept() rpc.ServeConn(conn) }
新的方法是 rpc.RegisterName 和 rpc.ServeConn,先注册 rpc 的服务名(RegisterName 用于指定名称,Register 函数利用反射获得名称),然后使用 conn 连接构造 rpc 服务,事实上 ServeConn 函数接受的参数类型是 io.ReadWriteCloser,F12 查看 ServeConn 的内部实现,可以看到 gob 编解码:
func ServeConn(conn io.ReadWriteCloser) { DefaultServer.ServeConn(conn) } func (server *Server) ServeConn(conn io.ReadWriteCloser) { buf := bufio.NewWriter(conn) srv := &gobServerCodec{ rwc: conn, dec: gob.NewDecoder(conn), enc: gob.NewEncoder(buf), encBuf: buf, } server.ServeCodec(srv) }
在内部它调用了 ServeCodec 来实现,这个函数接受一个 ServerCodec 对象,表示具体使用哪一种序列化的编解码方案,这里默认的是 gob,而 gob 是 go 语言内部的,它不能跨语言,如果需要以 json 序列化传输对象和参数,就可以直接使用这个函数:
// rpc.ServeConn(conn) // 使用 json 编码 rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
而 json 序列化的实现是:
func NewServerCodec(conn io.ReadWriteCloser) rpc.ServerCodec { return &serverCodec{ dec: json.NewDecoder(conn), enc: json.NewEncoder(conn), c: conn, pending: make(map[uint64]*json.RawMessage), } }
有了 rpc 的服务端,客户端就可以调用了,很显然就像网络编程一样,客户端需要先 dial 到服务方建一个连接,然后把调用的方法名和参数都序列化后传过去:
var reply string client, _ := rpc.Dial("tcp", "127.0.0.1:3000") client.Call("HelloService.Say", "zhangsan", &reply) fmt.Println(reply)
client.Call 真正调用了远程方法,第一参数是方法名,第二个是调用参数,它对应着服务方的第一个参数,第三个参数是调用结果,对应服务方的第二个参数,F12 查看 Call 的实现:
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error { call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done return call.Error }
它在内部通过 Go 函数实现,返回了一个通道一直等待,所以 Call 是一个同步调用,如果希望异步就可直接调用 Go 方法自己拿到通道进行处理:
done := client.Go("HelloService.Say", "zhangsan", &reply, nil).Done // 继续做其它的事情 <-done
这个例子只有一个参数,如果调用需要多个参数则么办? 直接在 Say 方法中增加参数是行不通的,go 语言对 rpc 远程方法做了规定:
1. 只允许有 2 个参数,第二个参数必须是指针类型
2. 必须返回 error 类型
所有要支持多个参数,第一个参数必须修改成结构体类型,看下面的 math 的例子:
type MathService struct{} func (ms *MathService) Calc(expr Expr, reply *int) error { switch expr.Method { case "add": *reply = expr.Left + expr.Right case "mul": *reply = expr.Left * expr.Right } return nil } func startMathRpcServer() { rpc.RegisterName("MathService", new(MathService)) listener, _ := net.Listen("tcp", ":3000") //使用 for 循环服务多个客户端 conn, _ := listener.Accept() rpc.ServeConn(conn) }
这里的 Calc 是暴露的远程方法,它的第一个参数 expr 是结构体类型,定义了运算符号和操作数:
type Expr struct { Method string Left int Right int }
客户端的调用示例:
expr := Expr{"add", 1, 2} client.Call("MathService.Calc", expr, &reply) fmt.Println(reply)
这两个例子的服务方都是 tcp 的监听方,其实远程方法的提供者只是基于 io.ReadWriteCloser 实例,这里是 tcp.Conn 对象,所以其实 rpc 的服务方也可以是 tcp 的客户端对象,比如:
func startProxyRpcServer() { rpc.Register(new(HelloService)) for { // 反过来拨号到外网的 ip 地址上 conn, err := net.Dial("tcp", "127.0.0.1:3000") // 外网客户端还未监听连接失败 if err != nil { time.Sleep(1 * time.Second) continue } rpc.ServeConn(conn) conn.Close() } }
这个 tcp 一直尝试去连接本机的 3000 端口,它是 tcp 客户端,但是当连接上了,它也使用 conn 对象来提供 rpc 调用服务,完成后关闭 conn 连接,这时候客户端 rpc 的调用其实是开启 tcp 监听,它是调用方但是它不主动,等待被连接,它也不知道哪个 rpc 提供方会来服务,这个过程相当于反向代理:
func startProxyRpcClient() { var reply string // 外网的客户端主动提供 tcp 服务等待连接 listener, _ := net.Listen("tcp", ":3000") conn, _ := listener.Accept() // 构建 rpc 客户端对象 client := rpc.NewClient(conn) defer client.Close() client.Call("HelloService.Say", "zhangsan", &reply) fmt.Println(reply) }
和前面 rpc 调用方的区别是,首先使用 rpc.NewClient 构建一个 rpc 客户端对象,使用 Call 发起调用。
如果你已经写了一个 rpc 服务,现在需要提供 http 的版本怎么办? 是不是在 http 内部再调用一下 rpc 服务方呢,显然有点麻烦。能不能把在 http 的方法内直接嫁接到 rpc 服务方呢? 可以,不过这个前提是使用 xml、json 的序列化方案,go rpc 提供 ServeRequest 函数来嫁接:
func startHttpJsonRpcServer() { rpc.RegisterName("HelloService", new(HelloService)) http.HandleFunc("/say", func(writer http.ResponseWriter, request *http.Request) { var conn io.ReadWriteCloser = struct { io.Writer io.ReadCloser }{ writer, request.Body, } rpc.ServeRequest(jsonrpc.NewServerCodec(conn)) }) // curl localhost:3000/say -X POST --data '{"method":"HelloService.Say","params":["zhangsan"],"id":0}' http.ListenAndServe(":3000", nil) }
调用这个 http 方法的时候,需要传递序列化的调用语义:
{"method":"HelloService.Say","params":["zhangsan"],"id":0}
这里的 id 是调用的表示符,因为网络传输的原因,先发起的调用很可能后返回结果,需要一个 id 来鉴别是本次调用。
go rpc 提供了简洁的实现方案,在正式项目中常常需要跨语言的 rpc 调用,而 json 序列化的结果是全文本,传输效果过低,因此都大多使用 Protobuf 的方案,它是一个中间的描述语言,定义了调用的数据结构和消息(方法),使用编号来绑定数据,序列化后的数据字节数更少,调用方和服务方靠这个中间文件各自生成对应语言的代码,这样即实现了高效调用也实现了跨语言,具体参考 github.com/golang/protobuf/protoc-gen-go。基于 Protobuf 谷歌开发了 gRPC 开源框架,基于 http/2 协议提供服务。请注意如果不需要跨语言调用,go 自带的 net/rpc 是非常好的方案。
本章节的代码 https://github.com/developdeveloper/go-demo/tree/master/20-rpc-distibuted-os
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/72081.html