Go语言之RPC实践学习笔记

net/rpc 是 Go 语言标准库中实现远程过程调用的利器,虽然现在 gRPC 更为流行,但 net/rpc 以其简洁、易用和“Go-way”的设计哲学,在很多纯 Go 项目的内部通信中仍然占有一席之地。

📖 Go语言之net/rpc:构建分布式系统的基石

1. 什么是 RPC?

RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议。它允许程序像调用本地函数(或方法)一样,去调用另一台计算机上(或另一个进程)的函数,而不需要显式地处理网络通信的复杂细节。关键:调用的函数的执行空间是发送方的内存空间,而不是提供者

RPC 的核心思想是“透明性”——开发者应该感觉不到自己是在进行网络调用。

2. RPC 的核心原理与规范

无论是 Go 的 net/rpc 还是 gRPC,它们都遵循一套相似的 RPC 工作流:

  1. 客户端(Client)
  • 调用一个本地的“存根”(Stub)函数。这个存根函数看起来和本地函数一样,但它是由 RPC 框架生成的。

  • 客户端存根(Client Stub) 接收到调用后,将方法名(例如 “UserService.GetUser”)和参数(例如 “UserID: 123”)进行 “序列化”(Serialization,也叫编组/Marshalling),即把它们转换成二进制或文本格式的数据包。

  1. 网络传输(Network)
  • 客户端存根通过底层的网络协议(如 TCP 或 HTTP)将这个数据包发送到服务端。
  1. 服务端(Server)
  • 服务端的 “骨架”(Skeleton) 负责监听网络端口,接收数据包。

  • 服务端骨架(Server Skeleton) 接收到数据后,进行 “反序列化”(Deserialization,也叫解组/Unmarshalling),解析出方法名和参数。

  • 骨架根据方法名,调用本地注册的实际服务函数(例如 server.GetUser(UserID: 123))。

  1. 执行与返回
  • 服务端本地函数执行完毕,得到一个返回值(或错误)。

  • 骨架将这个返回值 “序列化”,并通过网络发送回客户端。

  1. 客户端接收
  • 客户端存根接收返回的数据包,“反序列化” 出结果。

  • 最后,存根将这个结果返回给最初的调用方。

在这个过程中,有几个关键的 “规范” 或“要素”:

  • 序列化/编解码(Codec):数据如何在网络上传输?是 JSON、XML、Protocol Buffers(Protobuf)还是 Go 特有的 gob

  • 网络传输(Transport):数据是基于什么协议传输的?TCP、HTTP 还是 UDP?

  • 服务发现(Service Discovery):客户端如何知道服务端的地址和端口?(net/rpc 对此涉及不多,但 gRPC 很看重)

  • 接口定义(IDL):如何定义客户端和服务端都认可的服务接口?

3. RPC 的应用场景

RPC 是构建分布式系统微服务架构的基石。

  • 微服务通信:在一个微服务系统中,订单服务可能需要调用用户服务来获取用户信息,RPC 是实现这种跨服务通信最高效的方式之一。

  • 分布式系统:在数据库集群、分布式计算(如 MapReduce)中,主节点(Master)需要通过 RPC 来协调和控制工作节点(Worker)。

  • 内部工具:为公司内部系统提供一个高性能的 API 接口,供其他内部工具或服务调用。


4. 深度教程:Go 语言 “net/rpc”

Go 的 net/rpc 包在设计上非常简洁,并深度集成了 Go 的特性。

4.1 net/rpc 的强制规范

为了实现自动化的序列化和方法查找,net/rpc 对你暴露的服务(Service)有严格的“方法签名”要求:

net/rpc 规范

  1. 服务必须是一个 Go 对象(通常是结构体)。

  2. 方法必须是 可导出的(首字母大写)。

  3. 方法必须 有两个参数

  4. 第一个参数(args)是客户端传入的参数,必须是可导出类型或Go内置类型。

  5. 第二个参数(reply)是返回给客户端的结果,必须是指针类型

  6. 方法必须返回一个 error 类型的值。

标准签名func (t *T) MethodName(args *ArgType, reply *ReplyType) error

4.2 案例一:基于 TCP 的 RPC(最常用)

这是最简单、最高效的 net/rpc 使用方式。它使用 Go 独有的 gob 编解码,性能很高,但缺点是仅限 Go 语言之间的通信。

步骤1:定义服务和参数

我们来创建一个简单的“数学”服务,提供一个乘法计算。

shared/common.go (这个文件客户端和服务端都需要)

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package shared

// 参数
type Args struct {
A, B int
}

// 服务方法
type MathService struct{}

// Multiply 方法
// (t *T) MethodName(args *ArgType, reply *ReplyType) error
func (t *MathService) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
步骤2:编写服务端 (Server)

server/main.go

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
package main

import (
"log"
"net"
"net/rpc"
"path/to/shared" // 替换为你的
)

func main() {
// 1. 创建一个 MathService 的实例
mathService := new(shared.MathService)

// 2. 注册服务
// rpc.Register 会使用反射来分析对象的所有可导出方法
err := rpc.Register(mathService)
if err != nil {
log.Fatalf("注册服务失败: %v", err)
}

// 3. 设置监听端口 (TCP)
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
defer listener.Close()

log.Println("RPC 服务器正在监听 :1234")

// 4. 接受连接并处理
// rpc.Accept 会在循环中接受连接,并为每个连接启动一个 goroutine 来处理
rpc.Accept(listener)
}
步骤3:编写客户端 (Client)

client/main.go

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
package main

import (
"log"
"net/rpc"
"path/to/shared" // 替换为你的
)

func main() {
// 1. 连接 RPC 服务器
// rpc.Dial(protocol, address)
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatalf("连接 RPC 服务器失败: %v", err)
}

// 2. 准备参数
args := &shared.Args{A: 10, B: 20}
var reply int

// 3. 调用远程方法
// client.Call("ServiceName.MethodName", args, &reply)
// ServiceName 默认是注册的结构体类型名,即 "MathService"
err = client.Call("MathService.Multiply", args, &reply)
if err != nil {
log.Fatalf("调用远程方法失败: %v", err)
}

// 4. 获得结果
log.Printf("MathService.Multiply(10, 20) = %d", reply)

// 演示:异步调用 (Async Call)
// 如果你不希望阻塞等待结果,可以使用 Go() 方法
var asyncReply int
call := client.Go("MathService.Multiply", &shared.Args{A: 5, B: 6}, &asyncReply, nil)

// ... 在这里可以做其他事情 ...

// 等待异步调用完成
replyCall := <-call.Done
if replyCall.Error != nil {
log.Fatalf("异步调用失败: %v", replyCall.Error)
}
log.Printf("异步调用 MathService.Multiply(5, 6) = %d", asyncReply)
}

4.3 案例二:基于 HTTP 的 RPC

net/rpc 也可以“寄宿”在 HTTP 协议上。这非常有用,因为它允许 RPC 数据穿过防火墙,并可以复用已有的 HTTP 基础设施(如 TLS、中间件等)。

服务端 (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
package main

import (
"log"
"net/http"
"net/rpc"
"path/to/shared"
)

func main() {
mathService := new(shared.MathService)
rpc.Register(mathService)

// 1. 将 RPC 服务绑定到 HTTP 处理器
// 这会暴露一个默认的 RPC 路径(_goRPC_)
rpc.HandleHTTP()

log.Println("RPC 服务器正在监听 HTTP :1234")

// 2. 启动 HTTP 服务器
if err := http.ListenAndServe(":1234", nil); err != nil {
log.Fatal(err)
}
}

客户端 (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
package main

import (
"log"
"net/rpc"
"path/to/shared"
)

func main() {
// 1. 使用 rpc.DialHTTP 连接
// 客户端会自动处理 HTTP POST 和路径
client, err := rpc.DialHTTP("tcp", "localhost:1234")
if err != nil {
log.Fatalf("连接 RPC 服务器失败: %v", err)
}

// 2. 调用(与 TCP 案例完全相同)
args := &shared.Args{A: 10, B: 20}
var reply int
err = client.Call("MathService.Multiply", args, &reply)
if err != nil {
log.Fatalf("调用远程方法失败: %v", err)
}

log.Printf("MathService.Multiply(10, 20) = %d", reply)
}

4.4 案例三:使用 JSON-RPC(实现跨语言)

net/rpc 默认的 gob 编码导致它只能 Go ↔ Go。但标准库提供了 net/rpc/jsonrpc 包,它允许我们使用 JSON 作为编解码器。

这意味着任何支持 JSON-RPC 的语言(如 Python, JavaScript)都可以调用你的 Go 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
package main

import (
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc" // 1. 导入 jsonrpc
"path/to/shared"
)

func main() {
rpc.Register(new(shared.MathService))

listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
log.Println("JSON-RPC 服务器正在监听 :1234")

for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
// 2. 使用 jsonrpc.ServeConn 替换 rpc.ServeConn
go jsonrpc.ServeConn(conn)
}
}

客户端 (Client - Go)

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
package main

import (
"log"
"net/rpc/jsonrpc" // 1. 导入 jsonrpc
"path/to/shared"
)

func main() {
// 2. 使用 jsonrpc.Dial 替换 rpc.Dial
client, err := jsonrpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal(err)
}

// 调用方式不变
args := &shared.Args{A: 10, B: 20}
var reply int
err = client.Call("MathService.Multiply", args, &reply)
if err != nil {
log.Fatal(err)
}
log.Printf("JSON-RPC 调用结果: %d", reply)
}

5. 展望未来:gRPC

虽然 net/rpc 非常轻巧易用,但它也存在一些局限性:

  1. Go 绑定:默认的 gob 编解码导致其生态封闭。

  2. 性能:JSON 编解码性能较差;gob 虽快,但不如 Protobuf。

  3. 功能限制:不支持流式(Streaming) 调用、没有连接复用、没有内置的服务发现、负载均衡或认证机制。

gRPC (Google Remote Procedure Call) 是 Google 推出的现代 RPC 框架,它解决了上述所有问题,已成为云原生时代微服务的首选。

gRPC 的核心优势

  1. Protocol Buffers (Protobuf)
  • gRPC 默认使用 Protobuf 作为其 IDL(接口定义语言) 和序列化工具。

  • Protobuf 是一种语言无关、平台无关、可扩展的序列化结构数据的方法(类似 JSON,但更小、更快、更强)。

  • 你只需要在一个 .proto 文件中定义服务和消息,gRPC 的工具链 (protoc) 就能自动生成所有语言(Go, Java, Python, C++, Node.js 等)的客户端和服务端代码。

  1. 基于 HTTP/2
  • net/rpc 通常基于 TCP 或 HTTP/1.1。

  • gRPC 建立在 HTTP/2 之上,带来了巨大优势:

    • 多路复用(Multiplexing):允许在单个 TCP 连接上同时处理多个双向请求和响应,解决了“队头阻塞”问题,极大提升了并发性能。

    • 头部压缩(Header Compression):减少了请求的开销。

    • 服务端推送(Server Push)

  1. 支持流式(Streaming)
  • 这是 gRPC 的杀手级特性。net/rpc 只能“一问一答”(Unary RPC)。

  • gRPC 支持四种通信模式:

    1. Unary RPC(一元RPC):同 net/rpc

    2. Server-streaming RPC(服务端流):客户端发一个请求,服务端可以像数据流一样持续返回多个响应(例如订阅通知)。

    3. Client-streaming RPC(客户端流):客户端可以像数据流一样持续发送多个消息,服务端最后返回一个响应(例如上传大文件)。

    4. Bidirectional-streaming RPC(双向流):客户端和服务端可以同时、独立地向对方发送数据流(例如实时聊天)。

net/rpc vs gRPC

特性 net/rpc (标准库) gRPC (Google)
序列化 默认 gob (Go-only),可选 JSON Protocol Buffers (高性能, 二进制)
传输协议 TCP 或 HTTP/1.1 HTTP/2 (高性能, 多路复用)
跨语言 较差 (需使用 jsonrpc) 极强 (自动生成多语言代码)
流处理 不支持 (仅 一问一答) 支持 (一元、客户端流、服务端流、双向流)
生态系统 基础 (标准库) 非常丰富 (服务发现、负载均衡、认证、Trace)
易用性 非常简单 (纯 Go 项目的快速选择) 学习曲线稍高 (需定义 .proto 文件)

总结

net/rpc 是 Go 语言“小而美”哲学的体现。如果你正在构建一个纯 Go 语言的内部系统,不希望引入复杂的 .proto 文件和 gRPC 依赖,net/rpc 及其 jsonrpc 变体仍然是一个优秀、轻量且高效的选择。

然而,如果你在构建面向未来的、高性能、跨语言的微服务系统,那么 gRPC 毫无疑问是当今的标准答案。