在涉及RPC的应用中,作为开发人员一般至少有3种角色:首先是服务器端实现RPC方法的开发人员,其次是客户端调用RPC方法的人员,最后也是最重要的是制定服务器端和客户端RPC接口规范的设计人员。为了简化将以上几种角色的工作全部放到一起,虽然看似实现简单,但是不利于后期的维护和工作的切割。

  1. 如果要重构HelloService服务,第一步需要明确服务的名字和接口:

    我们将RPC服务的接口规范分为3部分:首先是服务的名字,然后是服务要实现的详细方法列表,最后是注册该类型服务的函数。为了避免名字冲突,我们在RPC服务的名字中增加了包路径前缀(这个是RPC服务抽象的包路径,并非完全等价于GO语言的包路径)。RegisterHelloService注册服务时,编译器会要求传入的对象满足HelloServiceInterface接口。
  2. 在定义了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函数,我们可以在接口规范部分增加对客户端的简单包装:

rpc 流式接口_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)
	}



}
  1. 运行结果
  2. 在新的RPC服务器端实现中,我们用RegisterHelloService()函数来注册函数,这样不仅可以避免命名服务名称的工作,同时也保证了传入的服务对象满足RPC接口的定义,最后新的服务改为支持多个TCP链接,然后为每个TCP链接提供RPC服务。