Featured image of post 认识RPC

认识RPC

 

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编码,但是我们很容易在上面实现其他编码,比如ProtobufJSON 等。标准库中已经支持了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编码的客户端会给服务端发送什么信息,

1
 nc -l 1234

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框架。

Licensed under CC BY-NC-SA 4.0
最后更新于 Aug 13, 2024 18:24 CST
使用 Hugo 构建
主题 StackJimmy 设计