Go语言高并发与微服务实战专题精讲——远程过程调用 RPC——实践案例:Go 语言 RPC 过程调用实践
远程过程调用 RPC —— 实践案例:Go语言RPC过程调用实践
Go语言的官方RPC库net/rpc为开发者提供了实现远程过程调用的强大功能,使得通过网络访问对象的方法成为可能。这种机制极大地促进了分布式系统的构建,让不同的服务能够轻松地进行相互通信和协作。
在使用Go的RPC库时,服务器需要注册相应的对象,并通过对象的类型名来公开其服务。对象一旦注册成功,其公开的方法便可进行远程调用。该库精心封装了底层实现的细节,例如序列化、网络传输和反射调用等,从而极大地简化了RPC的开发流程。
服务器可以注册多个不同类型的对象以提供丰富的服务,但需注意,不能同时注册多个相同类型的对象。此外,为了让对象的方法能够被远程调用,它们必须满足以下条件:
-
- 方法的类型必须是可导出的(即首字母大写),以确保外部能够访问。
- 方法本身也必须是可导出的。
- 方法必须接受两个参数。第一个参数是从客户端接收的数据,第二个参数是用于返回数据的结构体指针。这两个参数的类型必须是可导出的或者是Go语言的内建类型。
- 方法的第二个参数必须是指针类型,以便能够修改其指向的数据并将修改后的数据返回给客户端。
-
方法的返回类型必须是
error,用于指示方法调用过程中是否发生了错误。
满足上述条件的能够进行远程调用的方法格式通常如下所示:
1 2 3 4 5 6 7 8 | func (t *YourType) YourRemoteProcedure(args *ArgsType, reply *ReplyType) error { // 实现远程过程的逻辑 // ... // 如果有错误发生,返回一个非nil的错误 // 如果没有错误,将结果填充到reply中,并返回nil return nil } |
YourType是你希望暴露给RPC服务的类型,ArgsType是传递给远程过程的参数类型,而ReplyType是用于存储过程返回结果的类型。YourRemoteProcedure是具体的远程过程名称,它会被RPC
客户端调用。
(1)首先,我们定义远程过程调用的请求和响应数据结构。例如,对于字符串操作的请求,我们可以定义如下结构,调用字符串操作的请求包括两个参数:字符串A和字符串B。
1 2 3 4 5 6 7 8 | type StringRequest struct { A string B string } type StringResponse struct { Value string } |
(2)接下来,我们定义一个服务接口,比如Service,并给出两个方法
Concat
和Diff:
1 2 3 4 | 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
接口:
1 2 3 4 5 6 7 8 9 10 11 12 | 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服务器设置示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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 的完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | 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 的完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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 的的代码:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具