Go的RPC

652 阅读7分钟

RPC是远程过程调用(remote procedure call),是用于分布式系统中节点之间互相通信。
本文章参考Go语言高级编程,对这本书进行了比较详细的演示。

我们先从Go原生的RPC讲起。

RPC

节点间通信,其中有一个节点是作为服务的提供方,我们称之为服务端,另外一个节点为服务的调用方,我们称之为客户端。我们通过一个打印"Hello World"的例子来说明native RPC是如何使用的。

Hello

下面是这个例子的文件目录结构:

.
├── client
│   └── main.go
└── server
    └── main.go

服务端

服务端要完成几件事情:

  1. 实现服务提供的方法,这里就是将"Hello World"返回
  2. 启动服务器
  3. 将服务绑定到服务器上,注册服务
  4. 监听连接,并给连接提供服务

main.go

// HelloService 定义服务结构体,目前都是一个空的
type HelloService struct{}

// Hello 服务提供的方法实现
// request是请求字符串
// reply是返回的字符串指针
func (h *HelloService) Hello(request string, reply *string) error {
	*reply = "Hello:" + request
	return nil
}

func main() {
	// 注册服务,第一个参数服务名,第二个是服务对应的结构体
	rpc.RegisterName("HelloService", new(HelloService))

	lis, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal("ListenTCP error:", err)
	}

	conn, err := lis.Accept()
	if err != nil {
		log.Fatal("Accept error:", err)
	}
	// 给对应的连接提供服务
	rpc.ServeConn(conn)
}

客户端

客户端需要做的事情:

  1. 和服务端建立连接,比如TCP连接
  2. 调用服务端提供服务里的方法
  3. 输出返回值

main.go

func main() {
	// 建立连接
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
		log.Fatal("dialing:", err)
	}
	var reply string
	// 方法调用
	// 第一个参数:服务名.方法名
	// request参数:发送给方法的参数
	// reply参数:响应回来的字符串
	err = client.Call("HelloService.Hello", "world", &reply)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(reply)
}

运行结果

完善

上面的代码看起来十分简单,但是其实不利于团队的沟通和协作,包括后期的维护。所以我们需要制定一套规范(接口)来完善它们。
首先确定服务名:

const HelloServiceName = "HelloService"

第二步定义接口:

type HelloServiceInterface interface {
    Hello(reply string, reply *string)error
}

在服务端和客户端都需要放上上面的代码
接着我们分别对服务端和客户端进行进一步的封装完善

服务端

服务端主要是对注册的方法的进行进一步的封装

func RegisterHelloService(svc HelloServiceInterface) error {
    return rpc.RegisterName(HelloServiceName, svc)
}

客户端

定义一个结构体

type HelloServiceClient struct {
    *rpc.Client
}
  1. 对建立连接进行封装
func DialHelloService(network, address string) (*HelloServiceClient, error) {
    c, err := rpc.Dial(network, address)
    if err != nil {
        return nil, err
    }
    return &HelloServiceClient{Client: c}, nil
}
  1. 对调用进行封装
func (p *HelloServiceClient) Hello(request string, reply *string) error {
    return p.Client.Call(HelloServiceName+".Hello", request, reply)
}

完整代码

服务端

package main

import (
	"log"
	"net"
	"net/rpc"
)

// HelloServiceName 服务名
const HelloServiceName = "HelloService"

// HelloServiceInterface HelloService的接口
type HelloServiceInterface interface {
	Hello(request string, reply *string) error
}

// HelloService 定义服务结构体,目前都是一个空的
type HelloService struct{}

// Hello 服务提供的方法实现
// request是请求字符串
// reply是返回的字符串指针
func (h *HelloService) Hello(request string, reply *string) error {
	*reply = "Hello:" + request
	return nil
}

func RegisterHelloService(svc HelloServiceInterface) error {
	return rpc.RegisterName(HelloServiceName, svc)
}

func main() {
	// 注册服务,第一个参数服务名,第二个是服务对应的结构体
	// rpc.RegisterName("HelloService", new(HelloService))
	RegisterHelloService(new(HelloService))

	lis, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal("ListenTCP error:", err)
	}

	conn, err := lis.Accept()
	if err != nil {
		log.Fatal("Accept error:", err)
	}
	// 给对应的连接提供服务
	rpc.ServeConn(conn)
}

客户端

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

// HelloServiceName 服务名
const HelloServiceName = "HelloService"

// HelloServiceInterface HelloService的接口
type HelloServiceInterface interface {
	Hello(request string, reply *string) error
}

// HelloServiceClient 对client进行封装
type HelloServiceClient struct {
    *rpc.Client
}

// 判断是否是interface类型
var _ HelloServiceInterface = (*HelloServiceClient)(nil)

// DialHelloService 建立连接封装
func DialHelloService(network, address string) (*HelloServiceClient, error) {
	c, err := rpc.Dial(network, address)
	if err != nil {
		return nil, err
	}
	return &HelloServiceClient{Client: c}, nil
}

// Hello 函数调用的封装
func (p *HelloServiceClient) Hello(request string, reply *string) error {
	return p.Client.Call(HelloServiceName+".Hello", request, reply)
}

func main() {
	// 建立连接
	// client, err := rpc.Dial("tcp", "localhost:1234")
	client, err := DialHelloService("tcp", "localhost:1234")
	if err != nil {
		log.Fatal("dialing:", err)
	}
	var reply string
	// 方法调用
	// 第一个参数:服务名.方法名
	// request参数:发送给方法的参数
	// reply参数:响应回来的字符串
	// err = client.Call("HelloService.Hello", "world", &reply)
	err = client.Hello("world", &reply)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(reply)
}

RPC原理

这部分主要讲一下RPC的源代码

客户端原理

// Call源码
func (client *Client) Call(
    serviceMethod string, args interface{},
    reply interface{},
) error {
    call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
    return call.Error
}

首先通过Client.Go方法进行一次异步调用,返回一个表示这次调用的Call结构体。然后等待Call结构体的Done管道返回调用结果。

// Go源码
func (client *Client) Go(
    serviceMethod string, args interface{},
    reply interface{},
    done chan *Call,
) *Call {
    call := new(Call)
    call.ServiceMethod = serviceMethod
    call.Args = args
    call.Reply = reply
    call.Done = make(chan *Call, 10) // buffered.

    client.send(call)
    return call
}

Go()主要任务就是构造一个Call结构体并将其返回,然后调用client.send(call)call发给服务端。
当调用结果返回,完成的时候会调用call.done()方法:

func (call *Call) done() {
    select {
    case call.Done <- call:
        // ok
    default:
        // We don't want to block here. It is the caller's responsibility to make
        // sure the channel has enough buffer space. See comment in Go().
    }
}

通过阅读上面的源码,我们在客户端可以有另外一种方法去完成client.Call()的工作

func doClientWork(client *rpc.Client) {
    helloCall := client.Go("HelloService.Hello", "hello", new(string), nil)

    // do some thing

    helloCall = <-helloCall.Done
    if err := helloCall.Error; err != nil {
        log.Fatal(err)
    }

    args := helloCall.Args.(string)
    reply := helloCall.Reply.(string)
    fmt.Println(args, reply)
}

服务端原理

//TODO

内存KV数据库

这是第二个例子,实现一个基于内存的KV数据库,是用来演示基于RPC实现Watch功能。
文件目录:

.
├── client
│   └── main.go
└── server
    └── main.go

服务端

第一步:定义服务结构体。m用来存储数据的,filter是每个Watch调用时定义的过滤器函数列表

// KVStoreService 内存KV存储服务
type KVStoreService struct {
	m      map[string]string
	filter map[string]func(key string)
	mu     sync.Mutex
}

Get和Set方法

// Get 获取值
func (p *KVStoreService) Get(key string, value *string) error {
	p.mu.Lock()
	defer p.mu.Unlock()

	if v, ok := p.m[key]; ok {
		*value = v
		return nil
	}

	return fmt.Errorf("not found")
}

// Set 设置值
func (p *KVStoreService) Set(kv [2]string, reply *struct{}) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	key, value := kv[0], kv[1]

	// 通知发生变化
	if oldValue := p.m[key]; oldValue != value {
		fmt.Println(key)
		for _, fn := range p.filter {
			fn(key)
		}
	}

	p.m[key] = value
	return nil
}

当值发生变化的时候,Set方法就会调用p.filter的函数列表
下面是Watch方法:主要是定义filter的方法,然后监控是否发生变化。
这里filter的函数是将值发生变化的键放入管道中。在Watch中取出管道中的值并将其返回

// Watch 实现一个监控
func (p *KVStoreService) Watch(timeoutSecond int, keyChanged *string) error {
	id := fmt.Sprintf("watch-%s-%03d", time.Now(), rand.Int())
	ch := make(chan string, 10) // buffered

	p.mu.Lock()
	p.filter[id] = func(key string) { ch <- key }
	p.mu.Unlock()
    
	select {
	case <-time.After(time.Duration(timeoutSecond) * time.Second):
		return fmt.Errorf("timeout")
	case key := <-ch:
		*keyChanged = key
		return nil
	}
}

main函数

func main() {
	rpc.RegisterName("KVStoreService", NewKVStoreService())
	lis, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal("net error")
	}
	conn, err := lis.Accept()
	if err != nil {
		log.Fatal("conn error")
	}
	rpc.ServeConn(conn)
}

客户端

客户端做的事情:

  1. 启动一个goroutine去调用Watch方法
  2. 设置一个键值对
  3. 打印响应
package main

import (
	"fmt"
	"log"
	"net/rpc"
	"time"
)

func doClientWork(client *rpc.Client) {
	go func() {
		var keyChanged string
		err := client.Call("KVStoreService.Watch", 30, &keyChanged)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("watch:", keyChanged)
	}()
	// 让上面的goroutine启动
	time.Sleep(time.Second * 1)

	err := client.Call(
		"KVStoreService.Set", [2]string{"abc", "abc-value"},
		new(struct{}),
	)
	if err != nil {
		log.Fatal(err)
	}

	time.Sleep(time.Second * 3)
}

func main() {
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
		log.Fatal("connection error")
	}
	doClientWork(client)
}

反向RPC

这是第三个例子,反向RPC也就是将内网的RPC服务提供给外网访问。
其实本质就是:一个可以被外网访问的服务器作为客户端,接收外网的请求,做为服务的客户端调用服务的服务端,内网的服务器作为服务端,内网的服务器主动与外网的服务器建立连接
这是一个大概的拓扑图:

内网服务器的代码:

func main() {
    rpc.Register(new(HelloService))

    for {
        conn, _ := net.Dial("tcp", "localhost:1234")
        if conn == nil {
            time.Sleep(time.Second)
            continue
        }

        rpc.ServeConn(conn)
        conn.Close()
    }
}

可以注意到Listen变为Dial。反向RPC的内网服务将不再主动提供TCP监听服务,而是首先主动链接到对方的TCP服务器。然后基于每个建立的TCP链接向对方提供RPC服务。
下面是外网客户端的代码:

func doClientWork(clientChan <-chan *rpc.Client) {
    client := <-clientChan
    defer client.Close()

    var reply string
    err = client.Call("HelloService.Hello", "hello", &reply)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(reply)
}

func main() {
    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        log.Fatal("ListenTCP error:", err)
    }

    clientChan := make(chan *rpc.Client)

    go func() {
        for {
            conn, err := listener.Accept()
            if err != nil {
                log.Fatal("Accept error:", err)
            }

            clientChan <- rpc.NewClient(conn)
        }
    }()

    doClientWork(clientChan)
}

Listen监听TCP连接,如果有连接就新建一个rpc.Client然后调用doClientWork()

根据上下文信息提供对应的RPC服务

我们可以根据不同的连接提供不同的RPC服务 比如根据登陆状态:

type HelloService struct {
    conn    net.Conn
    isLogin bool
}

func (p *HelloService) Login(request string, reply *string) error {
    if request != "user:password" {
        return fmt.Errorf("auth failed")
    }
    log.Println("login ok")
    p.isLogin = true
    return nil
}

func (p *HelloService) Hello(request string, reply *string) error {
    if !p.isLogin {
        return fmt.Errorf("please login")
    }
    *reply = "hello:" + request + ", from" + p.conn.RemoteAddr().String()
    return nil
}

func main() {
    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 func() {
            defer conn.Close()

            p := rpc.NewServer()
            p.Register(&HelloService{conn: conn})
            p.ServeConn(conn)
        } ()
    }
}