Go语言高并发与微服务实战专题精讲——远程过程调用 RPC——实践案例:Go 语言 RPC 过程调用实践
远程过程调用 RPC —— 实践案例:Go语言RPC过程调用实践
Go语言的官方RPC库net/rpc为开发者提供了实现远程过程调用的强大功能,使得通过网络访问对象的方法成为可能。这种机制极大地促进了分布式系统的构建,让不同的服务能够轻松地进行相互通信和协作。
在使用Go的RPC库时,服务器需要注册相应的对象,并通过对象的类型名来公开其服务。对象一旦注册成功,其公开的方法便可进行远程调用。该库精心封装了底层实现的细节,例如序列化、网络传输和反射调用等,从而极大地简化了RPC的开发流程。
服务器可以注册多个不同类型的对象以提供丰富的服务,但需注意,不能同时注册多个相同类型的对象。此外,为了让对象的方法能够被远程调用,它们必须满足以下条件:
-
- 方法的类型必须是可导出的(即首字母大写),以确保外部能够访问。
- 方法本身也必须是可导出的。
- 方法必须接受两个参数。第一个参数是从客户端接收的数据,第二个参数是用于返回数据的结构体指针。这两个参数的类型必须是可导出的或者是Go语言的内建类型。
- 方法的第二个参数必须是指针类型,以便能够修改其指向的数据并将修改后的数据返回给客户端。
-
方法的返回类型必须是
error,用于指示方法调用过程中是否发生了错误。
满足上述条件的能够进行远程调用的方法格式通常如下所示:
func (t *YourType) YourRemoteProcedure(args *ArgsType, reply *ReplyType) error {
// 实现远程过程的逻辑
// ...
// 如果有错误发生,返回一个非nil的错误
// 如果没有错误,将结果填充到reply中,并返回nil
return nil
}
YourType是你希望暴露给RPC服务的类型,ArgsType是传递给远程过程的参数类型,而ReplyType是用于存储过程返回结果的类型。YourRemoteProcedure是具体的远程过程名称,它会被RPC
客户端调用。
(1)首先,我们定义远程过程调用的请求和响应数据结构。例如,对于字符串操作的请求,我们可以定义如下结构,调用字符串操作的请求包括两个参数:字符串A和字符串B。
type StringRequest struct { A string B string } type StringResponse struct { Value string }
(2)接下来,我们定义一个服务接口,比如Service,并给出两个方法
Concat
和Diff:
type Service interface { Concat(req *StringRequest, resp *StringResponse) error Diff(req *StringRequest, resp *StringResponse) error }
在 Go 语言的 RPC(远程过程调用)框架中,将response定义为指针类型有几个重要的原因:
-
-
性能优化: 在
RPC
调用中, 请求和响应的数据通常需要经过网络传输。如果response
不是指针类型, 那么每次函数调用时都会创建一个新的response
对象, 并且需要将整个对象在网络上发送和接收。而如果response
是指针类型, 那么只需要发送和接收指针所指向的数据的内存地址,这可以减少网络传输的数据量,从而提高性能。当然,在实际的网络通信中,指针本身并不会被直接传输,而是传输指针指向的数据内容;但使用指针意味着服务端可以直接修改传入的响应对象的字段,而不需要复制整个对象。 -
能够修改传入的对象: 当
response
是指针类型时,函数可以直接修改指针所指向的数据,这样调用方可以得到修改后的结果。在RPC
调用中,服务端通常需要填充response
对象以返回结果给客户端。如果>response
不是指针类型,服务端将只能修改其本地副本,而无法修改调用方传入的实际对象。 -
内存效率: 在函数调用过程中,如果
response
是值类型(非指针类型),那么当函数被调用时,response
将会被复制到函数的栈上。对于大的数据结构,这可能会导致不必要的内存分配和复制开销。而使用指针可以避免这种开销,因为只需要传递指向数据的引用。
-
综上所述,将response
定义为指针类型在 RPC
调用中是常见的做法,它有助于优化性能、减少内存使用,并允许服务端直接修改并返回结果给客户端。这种做法符合
Go
语言的设计哲学,即尽量减少不必要的内存分配和复制操作,提高程序的效率和性能。
(3)然后,我们实现这个Service
接口:
type StringService struct{} func (s *StringService) Concat(req *StringRequest, resp *StringResponse) error { resp.Value = req.A + req.B return nil } func (s *StringService) Diff(req *StringRequest, resp *StringResponse) error { // Diff方法是返回两个字符串的不同部分 resp.Value = strings.Replace(req.A, req.B, "", -1) return nil }
(4)之后,我们需要将服务注册到RPC服务中。这通常涉及到创建一个RPC服务器,注册服务,并开始监听请求。以下是一个简单的RPC服务器设置示例:
import ( "log" "net" "net/rpc" ) func main() { // 实例化服务对象 service := new(StringService) // 注册服务 rpc.Register(service) // 设置RPC监听 l, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("Listen error:", err) } defer l.Close() // 接受并处理RPC请求 for { conn, err := l.Accept() if err != nil { log.Fatal("Accept error:", err) } go rpc.ServeConn(conn) } }
现在,服务端已经准备好接受RPC请求了。客户端可以通过RPC调用Concat和
Diff
方法。
让我们看看 rpc-server.go 的完整代码:
package main import ( "log" "net" "net/rpc" ) // StringRequest 表示字符串操作的 RPC 请求结构。 type StringRequest struct { A string B string } // StringResponse 表示字符串操作的 RPC 响应结构。 type StringResponse struct { Value string } // Service 接口定义了 RPC 服务器将暴露的方法。 type Service interface { Concat(req *StringRequest, resp *StringResponse) error Diff(req *StringRequest, resp *StringResponse) error } // StringService 是 Service 接口的实现。 type StringService struct{} func (s *StringService) Concat(req *StringRequest, resp *StringResponse) error { resp.Value = req.A + req.B // 拼接字符串 return nil } func (s *StringService) Diff(req *StringRequest, resp *StringResponse) error { /*参数说明: s:源字符串,即需要进行替换操作的字符串。 old:需要被替换的子串。 new:替换后的新子串。 n:指定替换操作的次数。如果n为-1,则表示替换所有old子串;如果n为一个非负整数,则替换前n个old子串。 */ resp.Value = strings.Replace(req.A, req.B, "", -1) // 函数返回一个新的字符串,其中源字符串s中前n个old子串被new替换。 return nil } func main() { // 实例化服务对象。 service := new(StringService) // 注册服务到 RPC 服务器。 rpc.Register(service) // 设置 RPC 监听在指定端口。 l, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("监听错误:", err) } defer l.Close() // 接受并处理 RPC 请求。 for { conn, err := l.Accept() if err != nil { log.Fatal("接受错误:", err) } go rpc.ServeConn(conn) } }
运行 rpc-server.go 的的代码:
让我们看看 rpc-client.go 的完整代码:
package main import ( "fmt" "log" "net/rpc" ) // StringRequest 和 StringResponse 与服务器代码共享。 // 在实际场景中,可能会将它们放在一个公共包中。 type StringRequest struct { A string B string } type StringResponse struct { Value string } func main() { // 连接到 RPC 服务器。 client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { log.Fatal("拨号错误:", err) } // 准备 Concat 和 Diff 方法的请求。 reqConcat := &StringRequest{A: "Hello, ", B: "World!"} reqDiff := &StringRequest{A: "Hello, World!", B: "World!"} // 准备响应容器。 var respConcat StringResponse var respDiff StringResponse // 调用远程 Concat 方法。 err = client.Call("StringService.Concat", reqConcat, &respConcat) if err != nil { log.Fatal("Concat RPC 调用错误:", err) } fmt.Println("Concat 结果:", respConcat.Value) // 调用远程 Diff 方法。 err = client.Call("StringService.Diff", reqDiff, &respDiff) if err != nil { log.Fatal("Diff RPC 调用错误:", err) } fmt.Println("Diff 结果:", respDiff.Value) }
先运行了 rpc-server.go 的代码后,再运行 rpc-client.go 的的代码: