Go之rpc入门

参考资料 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)
}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

来自万古的忧伤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值