RPC是远程过程调用(remote procedure call),是用于分布式系统中节点之间互相通信。
本文章参考Go语言高级编程,对这本书进行了比较详细的演示。
我们先从Go原生的RPC讲起。
RPC
节点间通信,其中有一个节点是作为服务的提供方,我们称之为服务端,另外一个节点为服务的调用方,我们称之为客户端。我们通过一个打印"Hello World"的例子来说明native RPC是如何使用的。
Hello
下面是这个例子的文件目录结构:
.
├── client
│ └── main.go
└── server
└── main.go
服务端
服务端要完成几件事情:
- 实现服务提供的方法,这里就是将"Hello World"返回
- 启动服务器
- 将服务绑定到服务器上,注册服务
- 监听连接,并给连接提供服务
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)
}
客户端
客户端需要做的事情:
- 和服务端建立连接,比如TCP连接
- 调用服务端提供服务里的方法
- 输出返回值
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
}
- 对建立连接进行封装
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"
"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)
}
客户端
客户端做的事情:
- 启动一个goroutine去调用Watch方法
- 设置一个键值对
- 打印响应
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)
} ()
}
}