理解GRPC,实现简易RPC框架

在这里插入图片描述

前言

在这学期即将结束之际,我想向中科大的孟宁老师表达我对您的敬意和感激之情。您的《网络程序设计》课程是我研究生阶段最喜欢的一门课程,也是我最受益匪浅的一门课程。
您的课堂风格生动有趣,您用幽默的语言和形象的比喻,让我们轻松地理解了网络体系结构的原理和演变,以及互联网架构设计的方法和技巧。您不仅教给我们知识,更教给我们思维,让我们能够从庖丁解牛的角度,把握网络编程的重点和难点,掌握网络通信协议的精髓和细节。
您的课程设计科学合理,您为我们安排了一系列的课程实验,让我们能够从基础到进阶,从简单到复杂,逐步探索网络编程的奥秘和乐趣。您的课程资料详实完备,您为我们提供了丰富的参考资料和示例代码,让我们能够自主地完成实验,解决问题,提高能力。
您的课程氛围自由轻松,您鼓励我们积极参与课堂讨论,分享我们的想法和经验,互相学习和交流。您对我们的评价公正客观,您对我们的建议及时有效,您对我们的关心温暖真诚。您不仅是我们的老师,更是我们的朋友和导师。
您的《网络程序设计》课程让我受益匪浅,让我对网络编程有了更深的理解和更强的兴趣,让我对未来的学习和工作有了更多的信心和动力。您的教诲和激励将永远铭记在我心中,您的风范和品格将永远值得我学习和敬仰。
在此,我再次向您表示衷心的感谢和崇高的敬意,祝您身体健康,工作顺利,幸福快乐!

实现简易RPC框架代码仓库地址

GRPC

gRPC是一个高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,支持多种开发语言。gRPC采用了Protocol Buffers(Protobuf)作为序列化和反序列化协议,它是一种轻便高效的结构化数据存储格式,基于二进制编码构建,能够减少CPU的复杂解析,保障了RPC调用的高性能。

Protobuf

Protobuf是一种跨语言、跨平台、可扩展的用于序列化数据协议,它通过定义.proto文件来描述数据结构和服务接口,然后通过protoc编译器生成对应语言的中间代码,实现数据的编解码和服务的调用。Protobuf的优点有:

有明确的类型,支持的类型有多种
每个字段有一个数字标号,用于在编解码时识别字段,也便于向后兼容
能表达数组、map映射等类型
通过嵌套message可以表达复杂的对象
序列化后的体积很小,通常是JSON编码后的十分之一以下
编解码的速度很快,时空复杂度远小于JSON

RPC方法

gRPC提供了四种类型的RPC方法,分别是:

simple RPC:应用于常见的典型的Request/Response模型,客户端通过stub请求RPC的服务端并等待服务端的响应
server-side streaming RPC:客户端给服务端发送一个请求并获取服务端返回的流,用以读取一连串的服务端响应
client-side streaming RPC:客户端发送的请求payload有一连串的信息,通过流给服务端发送请求
bidirectional streaming RPC:服务端和客户端之间都使用read-write stream进行通信

gRPC和Protobuf的结合使得RPC调用更加高效、简洁、稳定,适用于大数据量、高频率的数据交互场景,被广泛应用于分布式系统、微服务架构、云计算等领域。

gRPC支持的操作

设备在网络架构里支持Dial-in和Dial-out两种对接模式。

Dial-in模式:设备作为gRPC服务器,采集器作为gRPC客户端。由采集器主动向设备发起gRPC连接并获取需要采集的数据信息或下发配置。Dial-in模式适用于小规模网络和采集器需要向设备下发配置的场景。
Dial-in模式支持以下操作:
Subscribe操作:高速采集设备的接口流量统计、CPU和内存等数据信息。当前仅支持基于Telemetry技术的Subscribe操作。
Get操作:获取设备运行状态和运行配置。当前仅支持基于gNMI(gRPC Network Management Interface)规范的Get操作。
Capabilities操作:获取设备能力数据。当前仅支持基于gNMI规范的Capabilities操作。
Set操作:向设备下发配置。当前仅支持基于gNMI规范的Set操作。
Dial-out模式:设备作为gRPC客户端,采集器作为gRPC服务器。设备主动和采集器建立gRPC连接,将设备上配置的订阅数据推送给采集器。Dial-out模式适用于网络设备较多的情况下,由设备主动向采集器提供设备数据信息。Dial-out模式只支持基于Telemetry技术的Subscribe操作。

gRPC交互过程

在这里插入图片描述
以网络设备为gRPC客户端,采集器为gRPC服务器为:

1、设备在开启gRPC功能后作为gRPC客户端,采集器作为gRPC服务器。
2、设备会根据应用服务(如订阅的事件)构建对应数据的格式(GPB/JSON),通过ProtoBuf(Protocol Buffers)编写Proto文件。然后,设备与采集器建立gRPC通道,通过gRPC协议向采集器发送请求消息。
3、采集器收到请求消息后,会通过ProtoBuf解译Proto文件,还原出事先定义好的数据结构,进行业务处理。
4、采集器处理完数据后,需要使用ProtoBuf重新编译应答数据,通过gRPC协议向设备发送应答消息。
5、设备收到应答消息后,结束本次的gRPC交互。
简单地说,设备主动和采集器建立gRPC连接,将设备上配置的订阅数据推送给采集器。在整个gRPC交互的过程中,设备和采集器都需要使用ProtoBuf来定义Proto文件。

简易RPC框架(Golang 语言实现):

代码仓库地址

**可以被远程调用的函数模板:
func (t T) MethodName(argType T1, replyType T2) error

消息编解码

格式:
| Option{MagicNumber: xxx, CodecType: xxx} | Header{ServiceMethod …} | Body interface{} |
| <------ 固定 JSON 编码 ------> | <------- 编码方式由 CodeType 决定 ------->|

在一次连接中,Option 固定在报文的最开始,Header 和 Body 可以有多个,即报文可能是这样的。
| Option | Header1 | Body1 | Header2 | Body2 | …

Option被客户端用来和服务器 协商编码类型: CodecType 以及通过 MagicNumber来表明当前连接时RPC连接

type Header struct {
	ServiceMethod string //服务名和方法名
	Seq           uint64 //请求的序号
	Error         string
}

// 对消息体进行编解码的接口Codec
type Codec interface {
	io.Closer
	ReadHeader(*Header) error
	ReadBody(interface{}) error
	Write(*Header, interface{}) error
}

const MagicNumber = 0x3bef5c

type Option struct {
	MagicNumber    int           //标记RPC连接
	CodecType      codec.Type    //编解码种类
	ConnectTimeout time.Duration //连接超时时间
	HandleTimeout  time.Duration //处理超时时间
}

var DefaultOption = &Option{
	MagicNumber:    MagicNumber,
	CodecType:      codec.GobType,
	ConnectTimeout: time.Second * 10,
}

const (
	connected        = "200 connected to Gee RPC"
	defaultRPCPath   = "/_geerpc_"
	defaultDebugPath = "/debug/geerpc"
)

type Type string

const (
	GobType  Type = "application/gob"
	JsonType Type = "application/json" //not implemented
)



高性能客户端

type Call struct {
	Seq           uint64
	ServiceMethod string      //format "<service>.<method>"
	Args          interface{} //arguments to the function
	Reply         interface{} //reply from the function
	Error         error       //if error occurs,it will be set
	Done          chan *Call  //Strobes when call is complete
}
type Client struct {
	//消息的编解码器,和服务端类似,
	//序列化要发出去的请求,反序列化接收到的响应
	cc codec.Codec

	//配置字段
	opt *Option

	//和服务端类似,保证请求的有序发送,防止多个请求报文混淆
	sending sync.Mutex

	//消息头,只有在请求发送时才需要,而请求发送时互斥的,
	//因此每个客户端只需一个,可以复用
	header codec.Header

	//保护client的各属性
	mu sync.Mutex

	//每个请求的唯一编号
	seq uint64

	//存储注册但是没有发送的请求<seq,Call>
	pending map[uint64]*Call

	//closing和shutdown 任意一个值为true,则表示client不可用

	//主动关闭即调用Close()
	closing bool //user has called close
	//有错误关闭
	shutdown bool //user has told us to stop
}

客户端实现了服务注册、移除、异常终止、接受响应

// 注册:将参数 call 添加到 client.pending 中,并更新 client.seq。
func (client *Client) registerCall(call *Call) (uint64, error) {
	client.mu.Lock()
	defer client.mu.Unlock()
	if client.closing || client.shutdown {
		return 0, ErrShutdown
	}
	call.Seq = client.seq
	client.pending[call.Seq] = call
	//fmt.Println(len(client.pending))
	client.seq++
	return call.Seq, nil
}

// 移除:根据 seq,从 client.pending 中移除对应的 call,并返回
func (client *Client) removeCall(seq uint64) *Call {
	client.mu.Lock()
	defer client.mu.Unlock()
	call := client.pending[seq]
	delete(client.pending, seq)
	return call
}

// 终止:服务端或客户端发生错误时调用,将 shutdown 设置为 true,且将错误信息通知所有 pending 状态的 call。
func (client *Client) terminateCalls(err error) {
	client.sending.Lock()
	defer client.sending.Unlock()
	client.mu.Lock()
	defer client.mu.Unlock()
	client.shutdown = true
	for _, call := range client.pending {
		call.Error = err
		call.done()
	}
}
//对一个客户端端来说,接收响应、发送请求是最重要的 2 个功能。

/*
接受响应:即接受call的结果

三种情况:

call 不存在,可能是请求没有发送完整,或者因为其他原因被取消,但是服务端仍旧处理了。
call 存在,但服务端处理出错,即 h.Error 不为空。
call 存在,服务端处理正常,那么需要从 body 中读取 Reply 的值。
*/
func (client *Client) receive() {
	var err error
	for err == nil {
		var h codec.Header
		if err = client.cc.ReadHeader(&h); err != nil {
			break
		}
		call := client.removeCall(h.Seq)
		switch {
		case call == nil:
			// it usually means that Write partially failed
			// and call was already removed.
			err = client.cc.ReadBody(nil)
		case h.Error != "":
			call.Error = fmt.Errorf(h.Error)
			err = client.cc.ReadBody(nil)
			call.done()
		default:
			err = client.cc.ReadBody(call.Reply)
			if err != nil {
				call.Error = errors.New("reading body " + err.Error())
			}
			call.done()
		}
	}
	// error occurs, so terminateCalls pending calls
	client.terminateCalls(err)
}

客户端创建工作流程:
创建 Client 实例时,首先需要完成一开始的协议交换,即发送 Option 信息给服务端。
协商好消息的编解码方式之后,再创建一个子协程调用 receive() 接收响应

func NewClient(conn net.Conn, opt *Option) (*Client, error) {
	f := codec.NewCodeFuncMap[opt.CodecType]
	if f == nil {
		err := fmt.Errorf("invalid codec type %s", opt.CodecType)
		log.Println("rpc client: codec error:", err)
		return nil, err
	}
	// send options with server
	if err := json.NewEncoder(conn).Encode(opt); err != nil {
		log.Println("rpc client: options error: ", err)
		_ = conn.Close()
		return nil, err
	}
	return newClientCodec(f(conn), opt), nil
}

func newClientCodec(cc codec.Codec, opt *Option) *Client {
	client := &Client{
		seq:     1,
		cc:      cc,
		opt:     opt,
		pending: make(map[uint64]*Call),
	}
	go client.receive()
	return client
}

客户端发送RPC工作流程:
利用锁按序、完整发送

// 发送请求
func (client *Client) send(call *Call) {
	//按序发送,完整发送
	client.sending.Lock()
	defer client.sending.Unlock()

	//register this call
	seq, err := client.registerCall(call)
	if err != nil {
		call.Error = err
		call.done()
		return
	}

	//prepare request header
	client.header.ServiceMethod = call.ServiceMethod
	client.header.Seq = seq
	client.header.Error = ""

	//encode and send the request
	//利用gob.Write发送,发送失败移除Call
	if err := client.cc.Write(&client.header, call.Args); err != nil {
		call := client.removeCall(seq)
		// call may be nil, it usually means that Write partially failed,
		// client has received the response and handled
		if call != nil {
			call.Error = err
			call.done()
		}
	}

}

Go 和 Call 是客户端暴露给用户的两个 RPC 服务调用接口:
Go 是一个异步接口,返回 call 实例。
Call 是对 Go 的封装,阻塞 call.Done,等待响应返回,是一个同步接口。

func (client *Client) Go(ServiceMethod string, args, reply interface{}, done chan *Call) *Call {
	if done == nil {
		done = make(chan *Call, 10)
	} else if cap(done) == 0 {
		log.Panic("rpc client: done channel is unbuffered")
	}

	call := &Call{
		ServiceMethod: ServiceMethod,
		Args:          args,
		Reply:         reply,
		Done:          done,
	}

	client.send(call)
	return call
}

func (client *Client) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error {
	//创建call,注册call,移除call,等待发送call完成

	call := client.Go(serviceMethod, args, reply, make(chan *Call, 1))
	select {
	case <-ctx.Done():
		client.removeCall(call.Seq)
		return errors.New("rpc client: call failed: " + ctx.Err().Error())
	case call := <-call.Done:
		return call.Error
	}
}

客户端也可以借助HTTP协议发送:
向 TCP 连接写入一个 CONNECT 请求,请求的路径是默认的 RPC 路径,也就是 /geerpc,这是为了告诉服务器,这是一个 RPC 连接,而不是普通的 HTTP 连接。

从 TCP 连接读取一个 HTTP 响应,使用 http.ReadResponse 函数,它的参数是一个 bufio.Reader 和一个 http.Request,分别表示一个缓冲读取器和一个 HTTP 请求对象。

检查 HTTP 响应的状态是否是 connected,也就是 200 Connected to Gee RPC,如果是,就表示连接成功,然后调用 NewClient 函数,创建一个 RPC 客户端,返回给调用者。

如果 HTTP 响应的状态不是 connected,就表示连接失败,返回一个错误信息,包含 HTTP 响应的状态。

func NewHTTPClient(conn net.Conn, opt *Option) (*Client, error) {
	io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", defaultRPCPath))
	resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
	if err == nil && resp.Status == connected {
		return NewClient(conn, opt)
	}
	if err == nil {
		err = errors.New("unexpected HTTP response: " + resp.Status)
	}
	return nil, err

}

func DialHTTP(network, address string, opts ...*Option) (*Client, error) {
	return dialTimeout(NewHTTPClient, network, address, opts...)
}

func XDial(rpcAddr string, opts ...*Option) (*Client, error) {
	parts := strings.Split(rpcAddr, "@")
	if len(parts) != 2 {
		return nil, fmt.Errorf("rpc client err: wrong format '%s', expect protocol@addr", rpcAddr)
	}
	protocol, addr := parts[0], parts[1]
	switch protocol {
	case "http":
		return DialHTTP("tcp", addr, opts...)
	default:
		// tcp, unix or other transport protocol
		return Dial(protocol, addr, opts...)
	}
}

服务注册

通过反射客户端传来的结构体,解析服务和方法进行注册

type service struct {

	//映射的结构体的名称:T
	name string

	//结构体类型
	typ reflect.Type

	//结构体实例本身,用作调用的第0个参数
	rcvr reflect.Value

	//存储映射的结构体的所有符合条件的方法
	method map[string]*methodType
}

type methodType struct {

	//方法本身
	method reflect.Method

	//第一个参数类型
	ArgType reflect.Type

	//第二个参数的类型
	ReplyType reflect.Type

	//后续统计方法调用次数时会用到
	numCalls uint64
}

// 过滤除了符合条件的方法:
// 两个导出或内置类型的入参,(反射时为3个,第0个是自身)
// 输出参数的类型必须是error类型,表示方法执行的错误情况。
// 输入参数的第二个和第三个类型必须是导出类型或内置类型,表示方法的请求和响应的数据结构。
// 方法的名称必须是导出名称,即首字母大写的名称,表示方法对外可见。
// 如果方法满足这些条件,那么就将方法的名称作为键,
// 方法的类型信息作为值,存储到method映射中。
// 同时,打印一条日志,显示注册了哪个方法。
// 这样,这个结构体就可以作为一个RPC服务,提供远程调用的功能。
func (s *service) registerMethods() {
	s.method = make(map[string]*methodType)[添加链接描述](https://github.com/tomatodinosaur/GeeRPC)
	for i := 0; i < s.typ.NumMethod(); i++ {
		method := s.typ.Method(i)
		mType := method.Type
		if mType.NumIn() != 3 || mType.NumOut() != 1 {
			continue
		}
		if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() {
			continue
		}
		argType, replyType := mType.In(1), mType.In(2)
		if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) {
			continue
		}
		s.method[method.Name] = &methodType{
			method:    method,
			ArgType:   argType,
			ReplyType: replyType,
		}
		log.Printf("rpc server: register %s.%s\n", s.name, method.Name)
	}
}

服务端

服务端存储一个服务池并提供Accept方法循环侦听

type Server struct {
	serviceMap sync.Map //服务池
}

// NewServer returns a new Server
func NewSever() *Server {
	return &Server{}
}

var DefaultSever = NewSever()

// Accept 接受侦听器上的连接并为每个传入连接提供请求。
func (sever *Server) Accept(lis net.Listener) {
	//for 循环等待 socket 连接建立,并开启子协程处理,
	//处理过程交给了 ServerConn 方法。
	for {
		conn, err := lis.Accept()
		if err != nil {
			log.Println("rpc sever:accept error:", err)
			return
		}
		go sever.ServeConn(conn)
	}
}

func Accept(lis net.Listener) { DefaultSever.Accept(lis) }

服务端的注册服务和发现服务

// 在Server端注册服务,加入Service池
func (server *Server) Register(rcvr interface{}) error {
	s := newService(rcvr)
	//LoadOrStore 返回键的现有值(如果存在)。 否则,它存储并返回给定值。
	//如果值已加载,则加载结果为 true;如果已存储,则加载结果为 false。
	if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup {
		return errors.New("rpc: service already defined: " + s.name)
	}
	return nil
}
因为 ServiceMethod 的构成是 “Service.Method”,
因此先将其分割成 2 部分,
第一部分是 Service 的名称,第二部分即方法名。
现在 serviceMap 中找到对应的 service 实例,
再从 service 实例的 method 中,找到对应的 methodType。

*/

// 发现服务:
// 1、在Server的服务池中查询服务
// 2、在Service的方法池中查询方法
func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) {
	dot := strings.LastIndex(serviceMethod, ".")
	if dot < 0 {
		err = errors.New("rpc server: service/method request ill-formed: " + serviceMethod)
		return
	}
	serviceName, methodName := serviceMethod[:dot], serviceMethod[dot+1:]
	svci, ok := server.serviceMap.Load(serviceName)
	if !ok {
		err = errors.New("rpc server: can't find service " + serviceName)
		return
	}
	svc = svci.(*service)
	mtype = svc.method[methodName]
	if mtype == nil {
		err = errors.New("rpc server: can't find method " + methodName)
	}
	return
}

客户端发来的请求格式

// request stores all information of a call
type request struct {
	h            *codec.Header //header :header of request:   <service.Method,seq,Error>
	argv, replyv reflect.Value //body :argv and replyv of request
	svc          *service      //服务
	mtype        *methodType   //方法
}

读请求头和读请求

func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) {
	var h codec.Header
	if err := cc.ReadHeader(&h); err != nil {
		if err != io.EOF && err != io.ErrUnexpectedEOF {
			log.Println("rpc server: read header error:", err)
		}
		return nil, err
	}
	return &h, nil
}

/*
通过 newArgv() 和 newReplyv() 两个方法创建出两个入参实例,
然后通过 cc.ReadBody() 将请求报文反序列化为第一个入参 argv,
在这里同样需要注意 argv 可能是值类型,也可能是指针类型,
所以处理方式有点差异。
*/

func (server *Server) readRequest(cc codec.Codec) (*request, error) {
	h, err := server.readRequestHeader(cc)
	if err != nil {
		return nil, err
	}
	req := &request{h: h}
	req.svc, req.mtype, err = server.findService(h.ServiceMethod)
	if err != nil {
		return req, err
	}
	req.argv = req.mtype.newArgv()
	req.replyv = req.mtype.newReplyv()

	//make sure that argvi is a pointer,
	//ReadBody need a pointer as parameter
	argvi := req.argv.Interface()
	if req.argv.Type().Kind() != reflect.Ptr {
		argvi = req.argv.Addr().Interface()
	}

	//将请求报文中的Body 反序列化为 argv
	if err = cc.ReadBody(argvi); err != nil {
		log.Println("rpc server: read body err:", err)
		return req, err
	}

	return req, nil
}

处理请求执行函数得到结果

func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup, timeout time.Duration) {
	defer wg.Done()
	called := make(chan struct{})
	sent := make(chan struct{})

	go func() {
		err := req.svc.call(req.mtype, req.argv, req.replyv)
		called <- struct{}{}
		if err != nil {
			req.h.Error = err.Error()
			server.sendResponse(cc, req.h, invalidRequest, sending)
			sent <- struct{}{}
			return
		}
		server.sendResponse(cc, req.h, req.replyv.Interface(), sending)
		sent <- struct{}{}
	}()

	if timeout == 0 {
		<-called
		<-sent
		return
	}
	select {
	case <-time.After(timeout):
		req.h.Error = fmt.Sprintf("rpc server: request handle timeout: expect within %s", timeout)
		server.sendResponse(cc, req.h, invalidRequest, sending)
	case <-called:
		<-sent
	}

}
func (s *service) call(m *methodType, argv, replyv reflect.Value) error {
	atomic.AddUint64(&m.numCalls, 1)
	f := m.method.Func
	returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv})
	if errInter := returnValues[0].Interface(); errInter != nil {
		return errInter.(error)
	}
	return nil
}

发送响应

func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) {
	sending.Lock()
	defer sending.Unlock()
	if err := cc.Write(h, body); err != nil {
		log.Println("rpc server: write response error:", err)
	}

}

负载均衡

提供两种负载均衡策略:
随机选择策略 - 从服务列表中随机选择一个。
轮询算法(Round Robin) - 依次调度不同的服务器,每次调度执行 i = (i + 1) mode n。

// 负载均衡策略
type SelectMode int

const (
	RandomSelect SelectMode = iota
	RoundRobinSelect
)

// 服务发现接口
type Discovery interface {
	//从注册中心更新服务列表
	Refresh() error

	//手动更新服务列表
	Update(severs []string) error

	//根据负载均衡策略,选择一个服务实例
	Get(mode SelectMode) (string, error)

	//返回所有的服务实例
	GetAll() ([]string, error)
}

// NoRegister
type MultiServerDiscovery struct {
	r       *rand.Rand
	mu      sync.RWMutex
	servers []string
	index   int
}

func NewMultiServerDiscovery(servers []string) *MultiServerDiscovery {
	d := &MultiServerDiscovery{
		servers: servers,
		r:       rand.New(rand.NewSource(time.Now().UnixNano())),
	}
	d.index = d.r.Intn(math.MaxInt32 - 1)
	return d
}

// 从注册中心更新服务列表
func (d *MultiServerDiscovery) Refresh() error {
	return nil
}

// 手动更新服务列表
func (d *MultiServerDiscovery) Update(severs []string) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.servers = severs
	return nil
}

// 根据负载均衡策略,选择一个服务实例
func (d *MultiServerDiscovery) Get(mode SelectMode) (string, error) {
	d.mu.Lock()
	defer d.mu.Unlock()
	n := len(d.servers)
	if n == 0 {
		return "", errors.New("rpc discovery: no available servers")
	}

	switch mode {
	case RandomSelect:
		return d.servers[d.r.Intn(n)], nil
	case RoundRobinSelect:
		s := d.servers[d.index]
		d.index = (d.index + 1) % n
		return s, nil
	default:
		return "", errors.New("rpc discovery: not supported select mode")
	}

}

// 返回所有的服务实例
func (d *MultiServerDiscovery) GetAll() ([]string, error) {
	d.mu.RLock()
	defer d.mu.RUnlock()
	servers := make([]string, len(d.servers))
	copy(servers, d.servers)
	return servers, nil
}

简单的支持心跳保活的注册中心

1、服务端启动后,向注册中心发送注册消息,注册中心得知该服务已经启动,处于可用状态。一般来说,服务端还需要定期向注册中心发送心跳,证明自己还活着。
2、客户端向注册中心询问,当前哪天服务是可用的,注册中心将可用的服务列表返回客户端。
3、客户端根据注册中心得到的服务列表,选择其中一个发起调用。

任何注册的服务超过 5 min,即视为不可用状态。

type GeeRegistry struct {
	timeout time.Duration
	mu      sync.Mutex // protect following
	servers map[string]*ServerItem
}

type ServerItem struct {
	Addr  string
	start time.Time
}

const (
	defaultPath    = "/_geerpc_/registry"
	defaultTimeout = time.Minute * 5
)

// New create a registry instance with timeout setting
func New(timeout time.Duration) *GeeRegistry {
	return &GeeRegistry{
		servers: make(map[string]*ServerItem),
		timeout: timeout,
	}
}

var DefaultGeeRegister = New(defaultTimeout)

putServer:添加服务实例,如果服务已经存在,则更新 start。
aliveServers:返回可用的服务列表,如果存在超时的服务,则删除。

func (r *GeeRegistry) putServer(addr string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	s := r.servers[addr]
	if s == nil {
		r.servers[addr] = &ServerItem{Addr: addr, start: time.Now()}
	} else {
		s.start = time.Now() // if exists, update start time to keep alive
	}
}

func (r *GeeRegistry) aliveServers() []string {
	r.mu.Lock()
	defer r.mu.Unlock()
	var alive []string
	for addr, s := range r.servers {
		if r.timeout == 0 || s.start.Add(r.timeout).After(time.Now()) {
			alive = append(alive, addr)
		} else {
			delete(r.servers, addr)
		}
	}
	sort.Strings(alive)
	return alive
}

Heartbeat 方法,便于服务启动时定时向注册中心发送心跳,默认周期比注册中心设置的过期时间少 1 min。

func Heartbeat(registry, addr string, duration time.Duration) {
	if duration == 0 {
		// make sure there is enough time to send heart beat
		// before it's removed from registry
		duration = defaultTimeout - time.Duration(1)*time.Minute
	}
	var err error
	err = sendHeartbeat(registry, addr)
	go func() {
		t := time.NewTicker(duration)
		for err == nil {
			<-t.C
			err = sendHeartbeat(registry, addr)
		}
	}()
}

func sendHeartbeat(registry, addr string) error {
	log.Println(addr, "send heart beat to registry", registry)
	httpClient := &http.Client{}
	req, _ := http.NewRequest("POST", registry, nil)
	req.Header.Set("X-Geerpc-Server", addr)
	if _, err := httpClient.Do(req); err != nil {
		log.Println("rpc server: heart beat err:", err)
		return err
	}
	return nil
}

运行Demo

type Foo int

type Args struct{ Num1, Num2 int }

func (f Foo) Sum(args Args, reply *int) error {
	*reply = args.Num1 + args.Num2
	return nil
}

func (f Foo) Sleep(args Args, reply *int) error {
	time.Sleep(time.Second * time.Duration(args.Num1))
	*reply = args.Num1 + args.Num2
	return nil
}

func startServer(addrCh chan string) {
	var foo Foo
	l, _ := net.Listen("tcp", ":0")
	server := geerpc.NewSever()
	_ = server.Register(&foo)
	addrCh <- l.Addr().String()
	server.Accept(l)
}

func foo(xc *xclient.XClient, ctx context.Context, typ, serviceMethod string, args *Args) {
	var reply int
	var err error
	switch typ {
	case "call":
		err = xc.Call(ctx, serviceMethod, args, &reply)
	case "broadcast":
		err = xc.Broadcast(ctx, serviceMethod, args, &reply)
	}
	if err != nil {
		log.Printf("%s %s error: %v", typ, serviceMethod, err)
	} else {
		log.Printf("%s %s success: %d + %d = %d", typ, serviceMethod, args.Num1, args.Num2, reply)
	}
}

func startRegistry(wg *sync.WaitGroup) {
	l, _ := net.Listen("tcp", ":9999")
	registry.HandleHTTP()
	wg.Done()
	_ = http.Serve(l, nil)
}

func startSever(registryAddr string, wg *sync.WaitGroup) {
	var foo Foo
	l, _ := net.Listen("tcp", ":0")
	server := geerpc.NewSever()
	_ = server.Register(&foo)
	registry.Heartbeat(registryAddr, "tcp@"+l.Addr().String(), 0)
	wg.Done()
	server.Accept(l)
}

func call(registry string) {
	d := xclient.NewGeeRegistryDiscovery(registry, 0)
	xc := xclient.NewXClient(d, xclient.RandomSelect, nil)
	defer func() { _ = xc.Close() }()
	// send request & receive response
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			foo(xc, context.Background(), "call", "Foo.Sum", &Args{Num1: i, Num2: i * i})
		}(i)
	}
	wg.Wait()
}

func broadcast(registry string) {
	d := xclient.NewGeeRegistryDiscovery(registry, 0)
	xc := xclient.NewXClient(d, xclient.RandomSelect, nil)
	defer func() { _ = xc.Close() }()
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			foo(xc, context.Background(), "broadcast", "Foo.Sum", &Args{Num1: i, Num2: i * i})
			// expect 2 - 5 timeout
			ctx, _ := context.WithTimeout(context.Background(), time.Second*2)
			foo(xc, ctx, "broadcast", "Foo.Sleep", &Args{Num1: i, Num2: i * i})
		}(i)
	}
	wg.Wait()
}
func main() {
	log.SetFlags(0)
	registryAddr := "http://localhost:9999/_geerpc_/registry"
	var wg sync.WaitGroup
	wg.Add(1)
	go startRegistry(&wg)
	wg.Wait()

	time.Sleep(time.Second)
	wg.Add(2)
	go startSever(registryAddr, &wg)
	go startSever(registryAddr, &wg)
	wg.Wait()

	time.Sleep(time.Second)
	call(registryAddr)
	broadcast(registryAddr)
}
**结果:**
rpc registry path: /_geerpc_/registry
rpc server: register Foo.Sleep
rpc server: register Foo.Sum
tcp@[::]:37125 send heart beat to registry http://localhost:9999/_geerpc_/registry
rpc server: register Foo.Sleep
rpc server: register Foo.Sum
tcp@[::]:44607 send heart beat to registry http://localhost:9999/_geerpc_/registry
rpc registry: refresh servers from registry http://localhost:9999/_geerpc_/registry
call Foo.Sum success: 0 + 0 = 0
call Foo.Sum success: 4 + 16 = 20
call Foo.Sum success: 1 + 1 = 2
call Foo.Sum success: 2 + 4 = 6
call Foo.Sum success: 3 + 9 = 12
rpc registry: refresh servers from registry http://localhost:9999/_geerpc_/registry
broadcast Foo.Sum success: 3 + 9 = 12
broadcast Foo.Sum success: 4 + 16 = 20
broadcast Foo.Sum success: 1 + 1 = 2
broadcast Foo.Sum success: 2 + 4 = 6
broadcast Foo.Sum success: 0 + 0 = 0
broadcast Foo.Sleep success: 0 + 0 = 0
broadcast Foo.Sleep success: 1 + 1 = 2
broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded
broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded
broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded

分析
启动了一个注册中心,它监听了 9999 端口,提供了一个 HTTP 服务,用于接收和返回 RPC 服务器的地址信息。
rpc registry path: /geerpc/registry

启动了两个 RPC 服务器,它们分别监听了随机的端口,注册了一个 Foo 类型的实例,提供了两个远程方法:Sum 和 Sleep。它们还向注册中心发送了心跳信息,告诉注册中心自己的地址和端口。
rpc server: register Foo.Sleep
rpc server: register Foo.Sum
tcp@[::]:37125 send heart beat to registry http://localhost:9999/geerpc/registry
rpc server: register Foo.Sleep
rpc server: register Foo.Sum
tcp@[::]:44607 send heart beat to registry http://localhost:9999/geerpc/registry
rpc registry: refresh servers from registry http://localhost:9999/geerpc/registry

调用了 call 函数,它创建了一个 RPC 客户端,使用注册中心提供的服务发现机制,随机选择一个 RPC 服务器,向其发送 Foo.Sum 的请求,传递了不同的参数,得到了不同的返回值。
call Foo.Sum success: 0 + 0 = 0
call Foo.Sum success: 4 + 16 = 20
call Foo.Sum success: 1 + 1 = 2
call Foo.Sum success: 2 + 4 = 6
call Foo.Sum success: 3 + 9 = 12

最后,调用了 broadcast 函数,它也创建了一个 RPC 客户端,使用注册中心提供的服务发现机制,向所有 RPC 服务器发送 Foo.Sum 和 Foo.Sleep 的请求,传递了不同的参数,得到了不同的返回值或错误信息,并打印了日志信息。其中,Foo.Sleep 的请求设置了 2 秒的超时时间,所以当参数大于 2 时,就会出现超时错误。
broadcast Foo.Sum success: 3 + 9 = 12
broadcast Foo.Sum success: 4 + 16 = 20
broadcast Foo.Sum success: 1 + 1 = 2
broadcast Foo.Sum success: 2 + 4 = 6
broadcast Foo.Sum success: 0 + 0 = 0
broadcast Foo.Sleep success: 0 + 0 = 0
broadcast Foo.Sleep success: 1 + 1 = 2
broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded
broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded
broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值