RPC和Protobuf(二)
跨语言的RPC
标准库的RPC默认采用Go语言特有的Gob编码,因此从其他语言调用Go语言实现RPC服务将比较困难,在互联网的为服务时代,每个RPC以及服务的使用都可能采用不同的编码语言,因此跨语言是互联网时代RPC的一个首要
条件。Go语言的RPC框架有两个比较有特色的设计: 一个是RPC数据包可以通过插件实现自定义的编码和解码;另一个是RPC建立在抽象的io.ReadWriteCloser接口之上,我们可以将RPC架设在不同的通信之上,这里我们尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的RPC。
service.go代码如下:
package main import ( "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" ) // 定义服务结构体 type HelloService struct { } // 定义服务空间名,之后要与客户端一致绑定 const HelloServiceName = "grpc.Server" // 服务端接口要求方法实现 type HelloServiceInterface = interface { Hellos(request string, reply *string) error } // 将结构体注册到rpc的命名空间下,注意这里的命名空间可以是一个路径片段,隐式约束实现接口中的方法后才能注册 func RegisterHelloService(svc HelloServiceInterface) error { // 将注册一这个名字为空间的,可以通过映射到这个结构体下,在客户端通过.调用对应的方法 return rpc.RegisterName(HelloServiceName, svc) } // 服务端实现方法,注意这个函数名要和client端的.名字保持一致 func (p *HelloService) Hellos(request string, reply *string) error { *reply = "hello :" + request return nil } func main() { // 将结构体注册到rpc中,前提式实现了方法 RegisterHelloService(new(HelloService)) listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("ListenTCP error: ", err) } for { fmt.Println("listenning...") // 监听服务 conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:", err) } // 将监听到的服务挂在在rpc上,这不过这里使用rpc.ServeCodec(),传入参数是针对服务端的JSON编解码器 go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) } }
client.go代码如下:
package main import ( "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" ) // 自定义命名空间,与服务注册的名字一致 const HelloServiceName = "grpc.Server" // 使用一个接口来约束注册的结构体 type HelloServiceInterface = interface { Hello (request string, reply *string) error } // 将结构体注册到rpc的命名空间下,注意这里的命名空间可以是一个路径片段 func RegisterHelloService(svc HelloServiceInterface) error { return rpc.RegisterName(HelloServiceName, svc) } func main() { // 首先通过net建立tcp拨号,而不是rpc创建客户端了 conn, err := net.Dial("tcp","localhost:1234") if err != nil { log.Fatal("dialing: ",err) } // 区别于之前,这里基于tcp连接,建立JSON的编解码器 client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) var reply // 通过client.Call()时,第一个参数是用点号连接的RPC服务名字和方法名字,第二个和第三个参数分别是定义rpc方法的两个参数 err = client.Call(HelloServiceName+".Hellos","hello",&reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
有了以上代码,我们可以测试哈,分别运行服务端和客户端代码,可以看到相应的结果。那和之前有什么差别?我们停止服务端的运行,运行一个监听tcp端口的命令: nc -l 1234 ;之后我们再次调用客户端时会出现如下结果:
{"method":"grpc.Server.Hellos","params":["hello"],"id":0}。 这里我们可以看到客户端通过json数据传输给服务端(也就是说只要能发送json就可以调用服务端的函数了),这里需要说明的是Json数据内部对应了两个结构体:客户端是clientRequest、服务端是serverRequest:
type clientRequest struct { Method string `json:"method"` Params [1]interface `json:"params"` Id uint64 `json:"id"` } // 服务端类似
在获取到RPC调用的Json数据后,我们来核实下service是否可以根据json数据返回相应的结果,启动服务端,在终端中数据:echo -e '{"method":"grpc.Server.Hellos","params":["hello"],"id":0}' | nc localhost 1234, 返回的结果是{"id":0,"result":"hello :hello","error":null}。说明可以通过json来实现跨语言的交流了,这里忽略了服务端的返回结构体。
之前的版本使用的是Gob编码,并比支持http协议,并没有完全实现跨语言调用,下面的版本可以实现:
package main import ( "io" "net/http" "net/rpc" "net/rpc/jsonrpc" ) // 创建一个函数的承载体 type HelloServer struct { } // 定义服务函数 // rpc规则: 必须含有两个参数,一个请求、一个相应,返回值为error类型,且方法名必须大写 func (p *HelloServer) Hello(request string, reply *string) error { *reply = "hello" + request return nil } func main() { // 将承载体的所有满足rpc规则的方法注册到HelloServer服务空间中 rpc.RegisterName("HelloServer", new(HelloServer)) http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) { var conn io.ReadWriteCloser = struct { io.Writer io.ReadCloser }{ ReadCloser: r.Body, Writer: w, } rpc.ServeRequest(jsonrpc.NewServerCodec(conn)) }) http.ListenAndServe(":1234", nil) }
在终端数据如下命令: curl localhost:1234/jsonrpc -X POST --data '{"method":"HelloServer.Hello","params":["hello"],"id":0}' 就可以完成rpc调用