RPC 版本的 “Hello, World”
Go 语言标准库提供了简单的 RPC 实现,包路径为 net/rpc
,我们实现的 RPC 方法必须要满足 Go 语言 RPC 要求:
- 方法只能有两个可序列化参数:
string
和*string
- 返回一个
error
类型 - 必须是公开方法
标准库提供的几个关键方法,服务端相关的:
rpc.RegisterName()
,将对象类型中所有满足 RPC 规则的对象方法注册为 RPC 函数rpc.ServeConn()
,在 TCP 连接上为客户端提供 RPC 服务
客户端相关的:
rpc.Dial()
,拨号 RPC 服务,返回客户端对象rpc.Client
和error
对象client.Call()
,调用具体的 RPC 方法
基于这几个方法,我们可以实现一个非常简单的 RPC 版本 "Hello, World":
服务端代码:
package main
import (
"fmt"
"net/rpc"
)
// 服务端代码
type HelloService struct {}
// 满足 RPC 规则的对象方法
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello: " + request
return nil
}
func main() {
rpc.RegisterName("HelloService", new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error: ", err)
}
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error: ", err)
}
rpc.ServeConn(conn)
}
客户端代码
package main
import (
"fmt"
"net/rpc"
)
func main() {
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing: ", err)
}
var reply string
err = client.Call("HelloService.Hello", "hello", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
更完善的 RPC
上面的示例简单演示了标准库 RPC 的几个基本接口的用法,但是服务端和客户端都关注了很多 RPC 的底层细节;在日常开发中,往往存在三方角色:服务端、客户端、RPC 框架设计者,其中 RPC 框架设计者需要为服务端和客户端提供 RPC SDK,我们可以将上面的示例进行改造,将服务端、客户端的业务逻辑和 RPC 相关代码做拆分。
服务端 SDK:
package rpcsdk
import "net/rpc"
// server sdk
const HelloServiceName = "HelloService"
type HelloServiceInterface = interface {
Hello(request string, reply *string) error
}
func RegisterHelloService(svc HelloServiceInterface) error {
return rpc.RegisterName(HelloServiceName, svc)
}
服务端业务代码:
package main
import (
"log"
"net"
"net/rpc"
"testRPC/rpcsdk"
)
type HelloService struct{}
// Go RPC 接口形式:两个可序列化参数,返回值 error,必须是公开方法
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello: " + request
return nil
}
func main() {
rpcsdk.RegisterHelloService(new(HelloService))
listener, err := net.Listen("tpc", ":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)
}
}
客户端 SDK
package rpcsdk
import "net/rpc"
// client sdk
type HelloServiceClient struct {
*rpc.Client
}
// 确保已经实现了 Hello 方法,否则编译报错
var _ HelloServiceInterface = (*HelloServiceClient)(nil)
func DialHelloService(network, address string) (*HelloServiceClient, error) {
c, err := rpc.Dial(network, address)
if err != nil {
return nil, err
}
return &HelloServiceClient{Client: c}, nil
}
func (p *HelloServiceClient) Hello(request string, reply *string) error {
return p.Client.Call(HelloServiceName+".Hello", request, reply)
}
客户端业务代码
package main
import (
"log"
"testRPC/rpcsdk"
)
func main() {
client, err := rpcsdk.DialHelloService("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing: ", err)
}
var reply string
err = client.Hello("hello", &reply)
if err != nil {
log.Fatal(err)
}
}
经过修改后,客户端和服务端的业务逻辑和 RPC 代码实现了一定程度的解耦:客户端用户不在需要担心 RPC 方法名或参数类型不匹配的问题;服务端避免了命名服务名称的工作。
跨语言 RPC
标准库 RPC 默认采用 Go 语言特有的 Gob 编码,因此其他语言嗲用 Go 语言实现的 RPC 服务会比较困难。在微服务时代,每个 RPC 及服务的使用者都可能采用不同的编程语言,因此支持跨语言的 RPC 服务是必要,这里简单演示基于net/rpc/jsonrpc
实现一个跨语言的 RPC 服务。
服务端代码:
package main
import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
func main() {
// ...
listener, err := net.Listen("tcp", "localhost: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(jsonrpc.NewServerCodec(conn))
}
}
最大的变化是使用 rpc.ServeCodec()
代替了 rpc.Conn()
,传入的参数是针对服务端的 JSON 编解码器。
客户端代码:
func main() {
//...
conn, err := new.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("net.Dial: ", err)
}
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
var reply string
//...
}
首先建立 TCP 连接,然后基于该连接建立针对客户端的 JSON 编解码器。
结果验证
首先需要清楚客户端请求发出的数据格式是什么,为此我们可以在 1234 端口启动一个 TCP 服务:
nc -l 1234
客户端执行依次 RPC 调用,得到以下信息:
{"method":"HelloService.Hello","params":["hello"],"id":0}
id
为调用方维护的唯一调用编号
得到了发送数据的形式,我们就可以向 RPC 服务端模拟发送数据:
echo -e '{"method":"HelloService.Hello","params":["hello"],"id":1}' | nc localhost 1234
得到返回结果:
{"id":1,"result":"hello:hello","error":null}
无论使用哪种语言,只要遵循同样的 JSON 结构,以同样的流程就可以和 Go 语言编写的 RPC 服务进行通信。
参考资料:
- 《Go语言高级编程》——柴树杉/曹春晖