RPC(Remote procedure Call)是分布式系统中不同节点之间广泛使用的通信方式,是互联网时代的基础技术。 Go的标准库在net/rpc包下提供了RPC的简单实现。本文旨在通过引导您使用 net/rpc 包实现一个简单的 RPC 接口来帮助您了解 RPC。
本文首发于Medium MPP计划。如果您是 Medium 用户,请在 Medium 上关注我。非常感谢。
要使net/rpc中的函数能够被远程调用,必须满足以下五个条件:
- 方法的类型已导出。
- 方法已导出。
- 该方法有两个参数,两者都是导出(或内置)类型。
- 该方法的第二个参数是一个指针。
- 该方法的返回类型为错误。
换句话说,函数签名必须是:
func (t *T) MethodName(argType T1, replyType *T2) error
基于这五个条件,我们可以构造一个简单的RPC接口:
type HelloService struct{} func (p *HelloService) Hello(request string, reply *string) error { log.Println("HelloService Hello") *reply = "hello:" + request return nil }
接下来,您可以将HelloService类型的对象注册为RPC服务:
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) } }
客户端实现如下:
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 拨打 RPC 服务,然后通过 client.Call() 调用特定的 RPC 方法。第一个参数是RPC服务名和方法名用点组合起来,第二个是输入,第三个是返回值,是一个指针。这个例子展示了使用 RPC 是多么容易。
在服务器和客户端代码中,我们都需要记住 RPC 服务名称 HelloService 和方法名称 Hello。这很容易导致开发过程中出现错误,因此我们可以通过抽象公共部分来稍微包装代码。完整代码如下:
// 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) } }
// 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) }
是不是很眼熟?
默认情况下,Go 的标准 RPC 库使用 Go 专有的 Gob 编码。但是,在其之上实现其他编码(例如 Protobuf 或 JSON)非常简单。标准库已经支持jsonrpc编码,我们可以通过对服务端和客户端代码进行小改动来实现JSON编码。
// 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 结构体的内容本质上是相同的:
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 -l 1234
nc 命令接收以下数据:
{"method":"HelloService.Hello","params":["hello"],"id":0}
这与serverRequest一致。
我们还可以运行服务器代码并使用 nc 发送请求:
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框架。
以上是RPC Action EP在Go中实现一个简单的RPC接口的详细内容。更多信息请关注PHP中文网其他相关文章!