Go语言的标准库提供了 RPC 框架和不同的 RPC 实现。
什么是 RPC
远程过程调用(Remote Procedure Call,简称 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而开发人员无需额外地为这个交互作用编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用。
通俗的来讲就是,RPC 允许跨机器、跨语言调用计算机程序。例如我们用Go语言写了一个获取用户信息的方法 getUserInfo,并把Go语言程序部署在阿里云服务器上面,另外我们还有一个部署在腾讯云上面的 php 项目,需要调用Go语言的 getUserInfo 方法获取用户信息,php 跨机器调用 Go 方法的过程就是 RPC 调用。
RPC 的工作流程如下图所示:
图:远程过程调用流程图
流程说明如下:
- (1) 调用客户端句柄,执行传送参数;
- (2) 调用本地系统内核发送网络消息;
- (3) 消息传送到远程主机;
- (4) 服务器句柄得到消息并取得参数;
- (5) 执行远程过程;
- (6) 执行的过程将结果返回服务器句柄;
- (7) 服务器句柄返回结果,调用远程系统内核;
- (8) 消息传回本地主机;
- (9) 客户句柄由内核接收消息;
- (10) 客户接收句柄返回的数据。
Go语言中如何实现 RPC 的
在Go语言中实现 RPC 非常简单,有封装好的官方包和一些第三方包提供支持。Go语言中 RPC 可以利用 tcp 或 http 来传递数据,可以对要传递的数据使用多种类型的编解码方式。
Go语言的 net/rpc 包使用 encoding/gob 进行编解码,支持 tcp 或 http 数据传输方式,由于其他语言不支持 gob 编解码方式,所以使用 net/rpc 包实现的 RPC 方法没办法进行跨语言调用。
此外,Go语言还提供了 net/rpc/jsonrpc 包实现 RPC 方法,JSON RPC 采用 JSON 进行数据编解码,因而支持跨语言调用。但目前的 jsonrpc 包是基于 tcp 协议实现的,暂时不支持使用 http 进行数据传输。
除了Go语言官方提供的 rpc 包,还有许多第三方包为在Go语言中实现 RPC 提供支持,大部分第三方 rpc 包的实现都是使用 protobuf 进行数据编解码,根据 protobuf 声明文件自动生成 rpc 方法定义与服务注册代码,所以在Go语言中可以很方便的进行 rpc 服务调用。
net/rpc 包
rpc 包提供了通过网络或其他 I/O 连接对一个对象的导出方法的访问。服务端注册一个对象,使它作为一个服务被暴露,服务的名字是该对象的类型名。注册之后,对象的导出方法就可以被远程访问。服务端可以注册多个不同类型的对象(服务),但注册具有相同类型的多个对象是错误的。
只有满足如下标准的方法才能用于远程访问,其余方法会被忽略:
- 方法是可导出的;
- 方法有两个参数,都是导出类型或内建类型;
- 方法的第二个参数是指针类型;
- 方法只有一个 error 接口类型的返回值。
下面的示例演示了Go语言 net/rpc 包实现 RPC 方法,使用 http 作为 RPC 的载体,通过 net/http 包监听客户端连接请求。
服务端代码如下:
package main import ( "errors" "fmt" "log" "net" "net/http" "net/rpc" "os" ) // 算数运算结构体 type Arith struct { } // 算数运算请求结构体 type ArithRequest struct { A int B int } // 算数运算响应结构体 type ArithResponse struct { Pro int // 乘积 Quo int // 商 Rem int // 余数 } // 乘法运算方法 func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error { res.Pro = req.A * req.B return nil } // 除法运算方法 func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error { if req.B == 0 { return errors.New("除以零") } res.Quo = req.A / req.B res.Rem = req.A % req.B return nil } func main() { rpc.Register(new(Arith)) // 注册rpc服务 rpc.HandleHTTP() // 采用http协议作为rpc载体 lis, err := net.Listen("tcp", "127.0.0.1:8080") if err != nil { log.Fatalln("致命错误: ", err) } fmt.Fprintf(os.Stdout, "%s", "开始连接") http.Serve(lis, nil) }
服务端程序运行之后将会监听本地的 8080 端口,下面我们再来看一下客户端程序,用于连接服务端并实现 RPC 方法调用,完整代码如下:
package main import ( "fmt" "log" "net/rpc" ) // 算数运算请求结构体 type ArithRequest struct { A int B int } // 算数运算响应结构体 type ArithResponse struct { Pro int // 乘积 Quo int // 商 Rem int // 余数 } func main() { conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8080") if err != nil { log.Fatalln("连接错误: ", err) } req := ArithRequest{11, 2} var res ArithResponse err = conn.Call("Arith.Multiply", req, &res) // 乘法运算 if err != nil { log.Fatalln("算术误差: ", err) } fmt.Printf("%d * %d = %d/n", req.A, req.B, res.Pro) err = conn.Call("Arith.Divide", req, &res) if err != nil { log.Fatalln("算术误差: ", err) } fmt.Printf("%d / %d, 商是 %d, 余数是 %d/n", req.A, req.B, res.Quo, res.Rem) }
运行结果如下:
11 * 2 = 22
11 / 2, 商是 5, 余数是 1
net/rpc/jsonrpc 库
上面的例子演示了使用 net/rpc 包实现 RPC 的过程,但是没办法在其他语言中调用上面例子所实现的 RPC 方法。
Go语言提供了 net/rpc/jsonrpc 包,用于提供基于 json 编码的 RPC 支持。在不指定编码协议时,默认采用 Go 特有的 gob 编码协议。但是其他语言一般不支持 Go 的 gob 协议,所以如果需要跨语言的 RPC 调用,需要采用通用的编码协议。
服务端部分的代码如下所示:
package main import ( "errors" "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" "os" ) // 算数运算结构体 type Arith struct { } // 算数运算请求结构体 type ArithRequest struct { A int B int } // 算数运算响应结构体 type ArithResponse struct { Pro int // 乘积 Quo int // 商 Rem int // 余数 } // 乘法运算方法 func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error { res.Pro = req.A * req.B return nil } // 除法运算方法 func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error { if req.B == 0 { return errors.New("除以零") } res.Quo = req.A / req.B res.Rem = req.A % req.B return nil } func main() { rpc.Register(new(Arith)) // 注册rpc服务 lis, err := net.Listen("tcp", "127.0.0.1:8080") if err != nil { log.Fatalln("致命错误: ", err) } fmt.Fprintf(os.Stdout, "%s", "开始连接") for { conn, err := lis.Accept() // 接收客户端连接请求 if err != nil { continue } go func(conn net.Conn) { // 并发处理客户端请求 fmt.Fprintf(os.Stdout, "%s", "新连接接入/n") jsonrpc.ServeConn(conn) }(conn) } }
上述服务端程序启动后,将会监听本地的 8080 端口,并处理客户端的 tcp 连接请求。下面我们再来实现一个客户端程序来连接上述服务端并进行 RPC 调用,完整代码如下:
package main import ( "fmt" "log" "net/rpc/jsonrpc" ) // 算数运算请求结构体 type ArithRequest struct { A int B int } // 算数运算响应结构体 type ArithResponse struct { Pro int // 乘积 Quo int // 商 Rem int // 余数 } func main() { conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8080") if err != nil { log.Fatalln("连接错误: ", err) } req := ArithRequest{11, 3} var res ArithResponse err = conn.Call("Arith.Multiply", req, &res) // 乘法运算 if err != nil { log.Fatalln("算术误差: ", err) } fmt.Printf("%d * %d = %d/n", req.A, req.B, res.Pro) err = conn.Call("Arith.Divide", req, &res) if err != nil { log.Fatalln("算术误差: ", err) } fmt.Printf("%d / %d, 商是 %d, 余数是 %d/n", req.A, req.B, res.Quo, res.Rem) }
运行结果如下:
11 * 3 = 33
11 / 3, 商是 3, 余数是 2
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/21373.html