在涉及RPC的应用中,作为开发人员一般至少有3种角色:首先是服务器端实现RPC方法的开发人员,其次是客户端调用RPC方法的人员,最后也是最重要的是制定服务器端和客户端RPC接口规范的设计人员。为了简化将以上几种角色的工作全部放到一起,虽然看似实现简单,但是不利于后期的维护和工作的切割。
- 如果要重构HelloService服务,第一步需要明确服务的名字和接口:
我们将RPC服务的接口规范分为3部分:首先是服务的名字,然后是服务要实现的详细方法列表,最后是注册该类型服务的函数。为了避免名字冲突,我们在RPC服务的名字中增加了包路径前缀(这个是RPC服务抽象的包路径,并非完全等价于GO语言的包路径)。RegisterHelloService注册服务时,编译器会要求传入的对象满足HelloServiceInterface接口。 - 在定义了RPC服务接口实现规范之后,客户端就可以根据规范编写RPC调用的代码了:
服务端代码:
package main
import (
"log"
"net"
"net/rpc"
)
const HelloServiceName = "path/to/pkg.HelloService"
type HelloServiceInterface interface {
Hello(request string, reply *string) error
}
func RegisterHelloService(svc HelloServiceInterface) error{
return rpc.RegisterName(HelloServiceName, svc)
}
type HelloService struct {}
func (p *HelloService) Hello(request string, reply *string) error{
*reply = "hello:" + request
return nil
}
func main(){
RegisterHelloService(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"
"log"
"net/rpc"
)
const HelloServiceName = "path/to/pkg.HelloService"
func main(){
client, err := rpc.Dial("tcp", "localhost:1234")
if err!=nil{
log.Fatal("dialing:", err)
}
var reply string
err = client.Call(HelloServiceName+".Hello", "hello", &reply)
if err!=nil{
log.Fatal(err)
}
fmt.Println(reply)
}
由于两个文件我放在了同一个包下面,但是各自有main方法,所以需要分别编译,这就需要每个文件里面都有一个const HelloServiceName.
4. 为了简化客户端用户调用RPC函数,我们可以在接口规范部分增加对客户端的简单包装:
我们在接口规范中针对客户端新增加了HelloServiceClient类型,该类型也必须满足HelloServiceInterface接口,这样客户端用户就可以直接通过接口调用RPC函数,同时提供了一个DialHelloService()函数,直接拨号HelloService服务。
客户端最新代码:
package main
import (
"fmt"
"log"
"net/rpc"
)
const HelloServiceName = "path/to/pkg.HelloService"
type HelloServiceClient struct {
*rpc.Client
}
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)
}
func main(){
client, err := DialHelloService("tcp", "localhost:1234")
if err!=nil{
log.Fatal("dialing:", err)
}
var reply string
err = client.Call(HelloServiceName+".Hello", "hello", &reply)
if err!=nil{
log.Fatal(err)
}
fmt.Println(reply)
}
现在客户端用户不用担心RPC方法名字或参数类型不匹配等低级错误的发生。
5. 最后是基于RPC接口规范编写的服务器端代码:
package main
import (
"log"
"net"
"net/rpc"
)
const HelloServiceName = "path/to/pkg.HelloService"
type HelloServiceInterface interface {
Hello(request string, reply *string) error
}
func RegisterHelloService(svc HelloServiceInterface) error{
return rpc.RegisterName(HelloServiceName, svc)
}
type HelloService struct {}
func (p *HelloService) Hello(request string, reply *string) error{
*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)
}
}
- 运行结果
- 在新的RPC服务器端实现中,我们用RegisterHelloService()函数来注册函数,这样不仅可以避免命名服务名称的工作,同时也保证了传入的服务对象满足RPC接口的定义,最后新的服务改为支持多个TCP链接,然后为每个TCP链接提供RPC服务。