RPC是远程过程调用的简称,是分布式系统中不同节点间流行的通信方式。是互联网时代的基石技术,Go语言的标准库也提供了一个简单的RPC实现,Go语言的RPC包的路径为net/rpc
,本篇文章的目的是利用net/rpc
实现一个简单的RPC 接口,帮助我们拨开RPC 的迷雾。
对 net/rpc
而言,一个函数需要能够被远程调用,需要满足如下五个条件:
- the method’s type is exported.
- the method is exported.
- the method has two arguments, both exported (or builtin) types.
- the method’s second argument is a pointer.
- the method has return type error.
也就是说,必须满足。
1
|
func (t *T) MethodName(argType T1, replyType *T2) error
|
简单的RPC请求
基于这五点要求,我们可以构建一个简单的RPC接口
1
2
3
4
5
6
|
type HelloService struct{}
func (p *HelloService) Hello(request string, reply *string) error {
log.Println("HelloService Hello")
*reply = "hello:" + request
return nil
}
|
然后就可以将HelloService类型的对象注册为一个RPC服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func main() {
_ = rpc.RegisterName("HelloService", new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
go rpc.ServeConn(conn)
}
}
|
客户端的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func main() {
conn, err := net.Dial("tcp", ":1234")
if err != nil {
log.Fatal("net.Dial:", err)
}
client := rpc.NewClient(conn)
var reply string
err = client.Call("HelloService.Hello", "hello", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
|
首先是通过rpc.Dial
Dail a RPC服务,然后再通过client.Call()
调用具体的RPC方法。第一个参数是用点号链接的RPC服务名字和方法名字,第二个参数是入参,第三个为返回值,是一个指针。
由这个例子可以看出RPC的使用其实非常简单。
在 Server 与Client 的代码中,我们都需要去记忆 RPC 服务的名字HelloService
以及,接口名字Hello
。这在开发过程中,很容易出错,我们可以稍微封装一下。将公共的部分抽出来,完整的代码如下:
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
|
// server.go
const ServerName = "HelloService"
type HelloServiceInterface = interface {
Hello(request string, reply *string) error
}
func RegisterHelloService(srv HelloServiceInterface) error {
return rpc.RegisterName(ServerName, srv)
}
type HelloService struct{}
func (p *HelloService) Hello(request string, reply *string) error {
log.Println("HelloService Hello")
*reply = "hello:" + request
return nil
}
func main() {
_ = RegisterHelloService(new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
go rpc.ServeConn(conn)
}
}
|
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
|
// client.go
type HelloServiceClient struct {
*rpc.Client
}
var _ HelloServiceInterface = (*HelloServiceClient)(nil)
const ServerName = "HelloService"
func DialHelloService(network, address string) (*HelloServiceClient, error) {
conn, err := net.Dial(network, address)
client := rpc.NewClient(conn)
if err != nil {
return nil, err
}
return &HelloServiceClient{Client: client}, nil
}
func (p *HelloServiceClient) Hello(request string, reply *string) error {
return p.Client.Call(ServerName+".Hello", request, reply)
}
func main() {
client, err := DialHelloService("tcp", "localhost:1234")
if err != nil {
log.Fatal("net.Dial:", err)
}
var reply string
err = client.Hello("hello", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
|
是不是已经有点眼熟了?
codec
标准库的RPC默认采用Go语言特有的Gob编码,但是我们很容易在上面实现其他编码,比如Protobuf
,JSON
等。标准库中已经支持了jsonrpc
编码,我们只需要稍微改动一下服务端与客户端代码,就能实现JSON编码
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
|
// server.go
func main() {
_ = rpc.RegisterName("HelloService", new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
//go rpc.ServeConn(conn)
}
}
//client.go
func DialHelloService(network, address string) (*HelloServiceClient, error) {
conn, err := net.Dial(network, address)
//client := rpc.NewClient(conn)
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
if err != nil {
return nil, err
}
return &HelloServiceClient{Client: client}, nil
}
|
请求的JSON数据对象在内部对应两个结构体:客户端是clientRequest,服务器端是 serverRequest 。clientRequest和serverRequest结构体的内容基本是一致的:
1
2
3
4
5
6
7
8
9
10
|
type clientRequest struct {
Method string `json:"method"`
Params [1]any `json:"params"`
Id uint64 `json:"id"`
}
type serverRequest struct {
Method string `json:"method"`
Params *json.RawMessage `json:"params"`
Id *json.RawMessage `json:"id"`
}
|
其中,Method 表示服务名字,它是 serviceName +Method 组成。 params部分的第一个元素为参数,id是由调用方维护的唯一的调用编号。用于在并发场场景下区分请求。
我们可以用nc
来模拟服务端,然后运行客户端代码,看看使用json编码的客户端会给服务端发送什么信息,
nc
收到的数据为:
1
|
{"method":"HelloService.Hello","params":["hello"],"id":0}
|
跟 serverRequest 一致。
我们也可以运行 服务端代码,然后使用 nc 发送请求。
1
2
3
|
echo -e '{"method":"HelloService.Hello","params":["Hello"],"Id":1}' | nc localhost 1234
---
{"id":1,"result":"hello:Hello","error":null}
|
总结
本文介绍了 Go 标准库中的rpc
,它使用非常简单,性能异常强大。很多rpc
的第三方库都是对rpc
的封装。文章很简单,等于是给RPC研究系列开了一个头。下一篇文章,我们会将protobuf
跟RPC结合起来,最终,我们会实现一个自己的RPC框架。