参考资料 GO高级编程
一 Rpc入门
RPC是远程过程调用的简称,是分布式系统中不同节点间流行的通信方式。在互联网时代,RPC已经和IPC一样成为一个不可或缺的基础构件。因此Go语言的标准库也提供了一个简单的RPC实现,我们将以此为入口学习RPC的各种用法。
Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。在第一章“Hello, World”革命一节最后,我们基于http实现了一个打印例子。下面我们尝试基于rpc实现一个类似的例子。
(1)server端
package main
import (
"net"
"net/rpc"
)
//其中Hello方法必须满足Go语言的RPC规则:
//1 方法只能有两个可序列化的参数
//2 其中第二个参数是指针类型,
//3 并且返回一个error类型,
//4 同时必须是公开的方法。
type HelloService struct {}
func (h *HelloService) Hello(request string, reply *string) error{
*reply = "hello:" + request
return nil
}
func main() {
//然后就可以将HelloService类型的对象注册为一个RPC服务:
//其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,
//所有注册的方法会放在“HelloService”服务空间之下
err := rpc.RegisterName("HelloService",new(HelloService))
if err != nil {
panic(err)
}
//然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
lis ,err := net.Listen("tcp",":1234")
if err != nil {
panic(err)
}
con,err := lis.Accept()
if err != nil {
panic(err)
}
rpc.ServeConn(con)
}
(1)client端
package main
import (
"fmt"
"net/rpc"
)
func main() {
//首先是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法
cli,err := rpc.Dial("tcp",":1234")
if err != nil {
panic(err)
}
var reply string
//在调用client.Call时,
//第一个参数是用点号链接的RPC服务名字和方法名字,
//第二和第三个参数分别我们定义RPC方法的两个参数。
err = cli.Call("HelloService.Hello","你好",&reply)
if err != nil {
panic(err)
}
fmt.Println(reply)
}
二 Http Rpc
Go语言内在的RPC框架已经支持在Http协议上提供RPC服务。但是框架的http服务同样采用了内置的gob协议,并且没有提供采用其它协议的接口,因此从其它语言依然无法访问的。在前面的例子中,我们已经实现了在TCP协议之上运行jsonrpc服务,并且通过nc命令行工具成功实现了RPC方法调用。现在我们尝试在http协议上提供jsonrpc服务。
新的RPC服务其实是一个类似REST规范的接口,接收请求并采用相应处理流程:
package main
import (
"io"
"net/http"
"net/rpc"
"net/rpc/jsonrpc"
)
type HelloService struct{}
func (s *HelloService) Hello(request string, reply *string) error {
//返回值是通过修改reply的值
*reply = "hello, " + request
return nil
}
func main() {
//1. 实例化一个server
_ = rpc.RegisterName("HelloService", &HelloService{})
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)
}
RPC的服务架设在“/jsonrpc”路径,在处理函数中基于http.ResponseWriter和http.Request类型的参数构造一个io.ReadWriteCloser类型的conn通道。然后基于conn构建针对服务端的json编码解码器。最后通过rpc.ServeRequest函数为每次请求处理一次RPC方法调用。
模拟一次RPC调用的过程就是向该链接发送一个json字符串:
curl localhost:1234/jsonrpc -X POST \
--data '{"method":"HelloService.Hello","params":["hello"],"id":0}'
三 rpc 实现watch
在很多系统中都提供了Watch监视功能的接口,当系统满足某种条件时Watch方法返回监控的结果。在这里我们可以尝试通过RPC框架实现一个基本的Watch功能。如前文所描述,因为client.send是线程安全的,我们也可以通过在不同的Goroutine中同时并发阻塞调用RPC方法。通过在一个独立的Goroutine中调用Watch函数进行监控。
为了便于演示,我们计划通过RPC构造一个简单的内存KV数据库。首先定义服务如下:
//server.go
package main
import (
"fmt"
"math/rand"
"net"
"net/rpc"
"sync"
"time"
)
type KVStoreService struct {
m map[string]string
filter map[string]func(key string)
mu sync.Mutex
}
/*
其中m成员是一个map类型,用于存储KV数据。filter成员对应每个Watch调用时定义的过滤器函数列表。而mu成员为互斥锁,用于在多个Goroutine访问或修改时对其它成员提供保护。
然后就是Get和Set方法:
*/
func NewKVStoreService() *KVStoreService {
return &KVStoreService{
m: make(map[string]string),
filter: make(map[string]func(key string)),
}
}
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")
}
func (p *KVStoreService) Set(kv [2]string, reply *struct{}) error {
if p.m == nil {
p.m = make(map[string]string)
}
p.mu.Lock()
defer p.mu.Unlock()
key, value := kv[0], kv[1]
if oldValue := p.m[key]; oldValue != value {
for _, fn := range p.filter {
fn(key)
}
}
p.m[key] = value
return nil
}
/*
在Set方法中,输入参数是key和value组成的数组,用一个匿名的空结构体表示忽略了输出参数。当修改某个key对应的值时会调用每一个过滤器函数。
而过滤器列表在Watch方法中提供:
*/
func (p *KVStoreService) Watch(timeoutSecond int, keyChanged *string) error {
if p.filter == nil {
p.filter = make(map[string]func(key string))
}
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
}
return nil
}
func main() {
err := rpc.RegisterName("KVStoreService",new(KVStoreService))
if err != nil {
panic(err)
}
//然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
lis ,err := net.Listen("tcp",":1234")
if err != nil {
panic(err)
}
con,err := lis.Accept()
if err != nil {
panic(err)
}
rpc.ServeConn(con)
}
Watch方法的输入参数是超时的秒数。当有key变化时将key作为返回值返回。如果超过时间后依然没有key被修改,则返回超时的错误。Watch的实现中,用唯一的id表示每个Watch调用,然后根据id将自身对应的过滤器函数注册到p.filter列表。
KVStoreService服务的注册和启动过程我们不再赘述。下面我们看看如何从客户端使用Watch方法:
//client.go
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)
} ()
err := client.Call(
"KVStoreService.Set", [2]string{"abc", "abc-value"},
new(struct{}),
)
if err != nil {
log.Fatal(err)
}
var t string
err = client.Call(
"KVStoreService.Get","abc",&t)
fmt.Println("t",t)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second*3)
}
func main() {
cli,err := rpc.Dial("tcp",":1234")
if err != nil {
panic(err)
}
doClientWork(cli)
}
首先启动一个独立的Goroutine监控key的变化。同步的watch调用会阻塞,直到有key发生变化或者超时。然后在通过Set方法修改KV值时,服务器会将变化的key通过Watch方法返回。这样我们就可以实现对某些状态的监控。
四 需要登陆的rpc认证
//server.go
package main
import (
"fmt"
"log"
"net"
"net/rpc"
)
type HelloService struct {
conn net.Conn
isLogin bool
}
//基于上下文信息,我们可以方便地为RPC服务增加简单的登陆状态的验证:
//这样可以要求在客户端链接RPC服务时,首先要执行登陆操作,登陆成功后才能正常执行其他的服务。
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
}
//Hello方法中就可以根据conn成员识别不同链接的RPC调用:
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)
}
//然后为每个链接启动独立的RPC服务:
go func() {
defer conn.Close()
p := rpc.NewServer()
p.Register(&HelloService{conn: conn})
p.ServeConn(conn)
} ()
}
}
//client.go
package main
import (
"fmt"
"net/rpc"
)
func main() {
//首先是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法
cli,err := rpc.Dial("tcp",":1234")
if err != nil {
panic(err)
}
var reply string
//因为server 添加了登录认证 这里不登录就会
//panic: please login
err = cli.Call("HelloService.Login","user:password",nil)
if err != nil {
panic(err)
}
err = cli.Call("HelloService.Hello","你好",&reply)
if err != nil {
panic(err)
}
fmt.Println(reply)
}