基于go语言的rpc框架【万字拆解】

文章目录

  • RPC框架
    • 简介
    • 需要解决的问题
  • 一、编码
    • 1、JSON和GOB
    • 2、JSON和GOB的编解码过程
      • JSON编码解码过程描述
      • GOB编码解码过程描述
    • 3、RPC编解码器的实现
      • 定义RPC编解码器接口
      • 基于JSON格式的RPC编解码器
      • 基于GOB格式的RPC编解码器
  • 二、服务注册
    • 1、go的反射机制
    • 2、定义存储方法类型的结构体
    • 3、服务注册
    • 4、服务调用
  • 三、服务注册中心
    • 1、服务注册中心实现原理
    • 2、定义注册中心结构体以及常量
    • 3、添加服务实例以及返回列表
    • 4、处理HTTP请求
    • 5、发送心跳
  • 四、支持异步和并发的客户端实现
    • 1、客户端结构体以及call结构体
    • 2、客户端连接以及接收
      • a、解析配置文件函数
      • b、客户端的关闭以及接收信息
      • c、超时处理以及客户端的创建
    • 3、客户端并发以及异步发送
      • 具体实现
      • 并发以及超时处理解析
  • 五、客户端实现服务发现以及负载均衡
    • 1、支持服务发现与负载均衡
      • a、结构体
      • b、服务发现
      • c、根据负载均衡算法获取一个服务地址
    • 2、新客户端
  • 六、服务器
  • 七、更完善的注册中心
  • 总结


RPC框架

简介

rpc(远程过程调用)是计算机通信协议,允许在不同进程空间的程序之间进行调用。其客户端和服务器可以在同一台机器上,也可以在不同的机器上,并且使用时就像调用本地程序一样,无需关注实现细节,因此相比其他通信方式具有更高的灵活性与易扩展性。
相较于传输层以 http 为基础的 restful api,rpc 更接近直接调用,且自定义协议格式减少了冗余报文,提供更高效的序列化协议并使得扩展与集成例如注册中心、负载均衡等功能更加容易。

需要解决的问题

rpc框架需要解决很多问题,其中包括:确定采用的传输协议、编码格式、超时处理、异步请求和并发、负载平衡和注册中心等一系列可用性问题,都需要被解决。而如服务提供方不统一各需自行实现连接池,超时处理等技术劳动,牵涉到“业务之外”的公共能力重复实现,因此这些能力也是rpc框架需要具备的。

一、编码

1、JSON和GOB

a、JSON和GOB都是序列化和反序列化数据的标准库。它们之间的主要区别在于:
JSON是一种文本格式,可读性好,但体积较大;GOB是二进制格式,通常更紧凑,但可读性差。
JSON可以轻松地与其他语言进行交互,而GOB仅能用于Go程序间的通信。
JSON支持动态类型,可以处理任意类型的数据,而GOB仅支持Go类型。
因此,在选择使用JSON还是GOB时,需要根据具体应用场景进行权衡。例如,如果要与其他语言进行交互,则需要选择JSON;如果需要更高效的序列化和反序列化,可以选择GOB。

2、JSON和GOB的编解码过程

JSON编码解码过程描述

  1. JSON编码过程: 在JSON格式中,需要将数据结构序列化为字符串。编码过程如下:

    • 将GO语言的各种基本类型与struct/map等组成的数据结构转换为JSON对象。
    • 对象中的字段名作为JSON对象的键,对应的值序列化后作为JSON对象的值。
    • 最终将JSON对象序列化为一个字符串。
  2. JSON解码过程:

    • 将序列化的JSON字符串反序列化为JSON对象。
    • 遍历JSON对象,针对不同的数据类型反序列化为GO语言对应的数据类型。

GOB编码解码过程描述

  1. GOB编码过程: GOB是GO语言内置的二进制编码格式,它可以将任意GO语言的值序列化为字节流。编码过程如下:

    • 通过GOB编码器将GO语言的各种基本类型与struct/map等组成的数据结构序列化为字节流。序列化的结果包括了类型信息以及值信息。
  2. GOB解码过程:

    • 通过GOB解码器将序列化的字节流反序列化为GO语言的值。解码器会根据字节流中包含的类型信息将其反序列化为对应的GO语言类型,并返回相应的值。

注意:GOB编解码通常用于在GO语言程序之间进行通信,而JSON编解码更适合于不同语言之间的数据交互。

3、RPC编解码器的实现

定义RPC编解码器接口

Header定义了RPC请求和响应的头部信息,包括服务方法名、请求序列号和错误信息。Codec定义了RPC编解码器的接口,包括读取请求和响应的头部和消息体,以及向网络连接中写入响应。NewCodecFunc定义了创建RPC编解码器的函数类型,它把传入的参数(即网络连接)转换成一个Codec对象。Type定义了RPC编解码器的类型,包括基于GOB格式和JSON格式的编解码器。const关键字用于声明常量,分别设置了基于GOB格式和JSON格式的RPC编解码器的类型。var关键字用于声明全局变量,将基于GOB格式和JSON格式的RPC编解码器类型与创建函数对应起来,在初始化时将其存储在一个映射表中。
通过定义HeaderCodec等类型来规范RPC请求和响应的格式,同时提供了可扩展的基于不同编解码格式的RPC编解码器。用户可以通过修改NewCodecFuncMap映射表中的值,以支持新的RPC编解码器类型。


type Header struct {
	ServiceMethod string // format "Service.Method"
	Seq           uint64 // sequence number chosen by client
	Error         string
}

type Codec interface {
	io.Closer
	ReadHeader(*Header) error
	ReadBody(interface{}) error
	Write(*Header, interface{}) error
}
type NewCodecFunc func(io.ReadWriteCloser) 	Codec
type Type string
const (
	GobType  Type = "application/gob"
	JsonType Type = "application/json" // not implemented
)

var NewCodecFuncMap map[Type]NewCodecFunc

func init() {
	NewCodecFuncMap = make(map[Type]NewCodecFunc)
	NewCodecFuncMap[GobType] = NewGobCodec
	NewCodecFuncMap[JsonType] = NewJsonCodec
}

基于JSON格式的RPC编解码器

通过使用JSON编码格式实现了RPC的编解码,同时利用带缓冲区的I/O流提高了效率。此处采用的是json.NewDecoder和json.NewEncoder对JSON数据进行编解码。
JsonCodec是一个结构体类型,包括一个读写器、一个缓冲器、一个JSON解码器和一个JSON编码器。
NewJsonCodec()函数用于创建JsonCodec对象,它将给定的读写器封装为带缓冲区的I/O流,并创建了一个JSON解码器和一个JSON编码器。
ReadHeader()方法用于从网络连接中读取一个RPC请求的头部信息,并将其解析成Header对象;如果当前的输入流还没有可用的数据,那么调用 c.dec.Decode(h) 就会阻塞等待,
ReadBody()方法用于从网络连接中读取一个RPC请求的消息体,并将其解析成相应的数据结构;
Write()方法用于将一个RPC响应写入到网络连接中,它先将响应的头部信息编码成JSON格式并写入到缓冲器中,然后将响应的消息体编码成JSON格式并写入到缓冲器中,最后调用缓冲器的Flush()方法将缓冲器中的内容刷新到网络连接中;Close()方法用于关闭当前的网络连接。

type JsonCodec struct {
	conn io.ReadWriteCloser
	buf  *bufio.Writer
	dec  *json.Decoder
	enc  *json.Encoder
}

var _ Codec = (*JsonCodec)(nil)

func NewJsonCodec(conn io.ReadWriteCloser) Codec {
	//变成bufio操作 效率高
	buf := bufio.NewWriter(conn)
	return &JsonCodec{
		conn: conn,
		buf:  buf,
		dec:  json.NewDecoder(conn),
		enc:  json.NewEncoder(buf),
	}
}
func (c *JsonCodec) ReadHeader(h *Header) error {
	err:= c.dec.Decode(h)
	// fmt.Println("json ReadHeader",h)
	return err 
}

func (c *JsonCodec) ReadBody(body interface{}) error {
	err:=c.dec.Decode(body)
	// fmt.Println("json ReadBody",body)
	return err
}

// 相当于重新构造了这个函数  _,err:=this.conn.Write([]byte(sendMsg))
func (c *JsonCodec) Write(h *Header, body interface{}) (err error) {
	defer func() {
		// 一是调用缓冲区的 Flush() 方法将缓冲区的内容输出
		_ = c.buf.Flush()
		if err != nil {
			_ = c.Close()
		}
	}()

	if err = c.enc.Encode(h); err != nil {
		log.Println("rpc: json error encoding header:", err)
		return
	}
	if err = c.enc.Encode(body); err != nil {
		log.Println("rpc: json error encoding body:", err)
		return
	}
	// fmt.Println("json Encode",h)
	// fmt.Println("json Encode",body)

	return
}

func (c *JsonCodec) Close() error {
	return c.conn.Close()
}

基于GOB格式的RPC编解码器

其原理与实现JSON格式的一样。

二、服务注册

调用者发送服务名以及方法名如何找到对应的方法名呢,通过反射,我们能够非常容易地获取某个结构体的所有方法,并且能够通过方法,获取到该方法所有的参数类型与返回值
具体逻辑
通过发射机制把服务的方法都注册到map中,一个string对应一个方法结构体。然后服务器server接受到请求通过这个map找到服务方法结构体,对请求进行处理。

1、go的反射机制

Go语言的反射机制(reflection)是指在运行时对程序本身进行访问和修改的能力。通过反射,程序可以动态获取变量的类型和数值,也可以动态调用函数和方法。
反射机制是由 reflect 包提供的。其中,reflect.Type 表示类型的元数据,reflect.Value 表示值的元数据。在 Go 语言中,每个变量都有一个类型,反射的核心是将变量转换为 reflect.Type 和 reflect.Value 类型,并在这两种类型之间进行转换和操作。
例子说明,创建了一个 Person 类型的对象 p,然后使用反射获取了 p 对象的类型 t。接着,我们使用 MethodByName 方法查找类型中名为 “SayHello” 的方法,并返回对应的 reflect.Method 类型的对象 method。如果找到了该方法,则我们使用 MethodByName 方法创建一个 reflect.Value 类型的对象 methodValue,然后调用该方法。 注意,当我们调用 methodValue.Call(nil) 时,传递了一个空切片作为参数。这是因为 SayHello 方法不接受任何参数。如果该方法接受参数,我们需要传递一个包含对应参数值的切片作为参数。

type Person struct {
	Name string
}

func (p *Person) SayHello() {
	fmt.Println("Hello, my name is", p.Name)
}

func main() {
	p := &Person{Name: "Alice"}
	t := reflect.TypeOf(p)
	method, _ := t.Elem().MethodByName("SayHello")
	methodValue := reflect.ValueOf(method.Func)
	methodValue.Call([]reflect.Value{reflect.ValueOf(p)})
}

2、定义存储方法类型的结构体

定义了一个名为service的结构体类型,用于封装一个服务对象及其所有方法。该结构体包含了服务的名称、类型、值以及存储服务方法的字典。

在newService函数中,我们首先通过反射机制获取传入参数rcvr的类型和值,并将其作为服务对象。接着,我们可以通过reflect.Indirect方法来获取服务对象的实际类型名称(去除指针类型),并判断该名称是否是导出的(即首字母大写)。如果不是,则输出错误信息并退出程序。最后,调用registerMethods方法注册该服务的所有方法,并返回服务对象。

在registerMethods方法中,我们会遍历服务对象的所有方法,并将符合条件的方法存储到服务的method字典中。具体来说,对于每个方法,我们需要满足以下条件才能进行注册:方法必须有且仅有三个参数,第一个参数必须为指向服务对象的指针,第二个参数为请求参数,第三个参数为返回值,同时返回值类型必须为error类型;请求参数和返回值必须为导出类型或内置类型。如果满足以上条件,则将方法的名称、参数类型和返回类型等信息存储到服务的method字典中,并输出注册成功的信息。

// 定义翻翻 类型 用来表示 服务的方法 的名字 和 需要的承诺书 和返回的类型 还有 被调用了几次
type methodType struct {
	method    reflect.Method
	ArgType   reflect.Type
	ReplyType reflect.Type
	numCalls  uint64
}

func (m *methodType) NumCalls() uint64 {
	// 原子操作 可以记录被调用了几次
	return atomic.LoadUint64(&m.numCalls)
}

// 动态地创建方法的参数值,以便于通过反射调用该方法。由于方法的参数类型可能是一个值类型,
// 也可能是一个指针类型,因此需要根据参数类型的不同,
// 分别使用 reflect.New 和 Elem() 方法来创建方法的参数值。这个方法就是实现这个功能的关键之一。
//、、、 因为传入反射的  call函数 需要 reflect.Value 类型的  并且要符合这个方法的类型
func (m *methodType) newArgv() reflect.Value {
	var argv reflect.Value
	// arg may be a pointer type, or a value type
	if m.ArgType.Kind() == reflect.Ptr {
		argv = reflect.New(m.ArgType.Elem())
	} else {
		argv = reflect.New(m.ArgType).Elem()
	}
	return argv
}
//返回值如果是指针和或者数组的话 要另外处理
func (m *methodType) newReplyv() reflect.Value {
	// reply must be a pointer type
	replyv := reflect.New(m.ReplyType.Elem())
	switch m.ReplyType.Elem().Kind() {
	case reflect.Map:
		replyv.Elem().Set(reflect.MakeMap(m.ReplyType.Elem()))
	case reflect.Slice:
		replyv.Elem().Set(reflect.MakeSlice(m.ReplyType.Elem(), 0, 0))
	}
	return replyv
}

3、服务注册

newService函数接收一个参数rcvr,该参数为任意类型的值,并将其作为服务的类型和值。在函数中,我们通过反射机制获取服务对象的类型和名称。
然后交给registerMethods 该方法用于遍历服务对象的所有方法,并将其存储到服务的method字典中。对于每个方法,我们需要满足以下条件才能进行注册:方法必须有且仅有三个参数,第一个参数必须为指向服务对象的指针,第二个参数为请求参数,第三个参数为返回值,同时返回值类型必须为error类型;请求参数和返回值必须为导出类型或内置类型。如果满足以上条件,则将方法的名称、参数类型和返回类型等信息存储到服务的method字典中,并输出注册成功的信息。

// 定义服务  一个服务有多个方法  并且存储在method中 
//   注册服务的时候就是传入一个结构体 然后把结构体实现的方法作为这个服务的方法 存储在map中
type service struct {
	name   string
	typ    reflect.Type
	rcvr   reflect.Value
	method map[string]*methodType
}
// 把传入的结构的类型 和作为值作为 这个服务的类型和值    名字也是  并且查看名字时候暴露  然后队长和这个服务的方法进行注册
func newService(rcvr interface{}) *service {
	s := new(service)
	s.rcvr = reflect.ValueOf(rcvr)
	s.name = reflect.Indirect(s.rcvr).Type().Name()
	s.typ = reflect.TypeOf(rcvr)
	if !ast.IsExported(s.name) {
		log.Fatalf("rpc server: %s is not a valid service name", s.name)
	}
	s.registerMethods()
	return s
}
// 注册服务的方法  首先遍历这个结构体所有的方法  把方法类型 方法体存储这个服务的字典中去
func (s *service) registerMethods() {
	s.method = make(map[string]*methodType)
	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)
	}
}

4、服务调用

service结构体的call方法,用于调用服务的一个具体方法。该方法接收三个参数:methodType类型的m表示要调用的方法对象,reflect.Value类型的argv表示请求参数的值,reflect.Value类型的replyv表示返回值的值。
在方法内部,我们首先通过原子操作将该方法被调用的次数加1,并获取到该方法对应的函数对象f。接着,我们可以通过f.Call方法来动态地调用该函数,传入参数s.rcvr, argv和replyv,并获取返回结果。如果返回结果中包含错误类型,则将其转化为error类型并返回。否则,直接返回nil,表示调用成功。

// 这里是调用这个服务的具体犯某个 已经打包好成methodType的方法的 传入 参数 replyv就是返回
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
}

三、服务注册中心

1、服务注册中心实现原理

注册中心的位置如下图所示。注册中心的好处在于,客户端和服务端都只需要感知注册中心的存在,而无需感知对方的存在。更具体一些:
服务端启动后,向注册中心发送注册消息,注册中心得知该服务已经启动,处于可用状态。一般来说,服务端还需要定期向注册中心发送心跳,证明自己还活着。
客户端向注册中心询问,当前哪天服务是可用的,注册中心将可用的服务列表返回客户端。
客户端根据注册中心得到的服务列表,选择其中一个发起调用。
基于go语言的rpc框架【万字拆解】_第1张图片
注册中心使用HTTP通信,并通过自定义的Header字段来传递信息。
服务提供者通过发送POST请求向注册中心注册服务,或者通过定时发送POST请求来发送心跳信号以保持在线。注册中心收到请求后会将服务地址和启动时间加入到存储列表中。
服务消费者通过发送GET请求向注册中心获取所有可用的服务地址。注册中心返回服务地址列表,服务消费者可以根据地址调用服务提供者的接口。如果出现服务提供者宕机或超时,则其地址会在注册中心的存储列表中被删除。
服务消费者就可以通过注册中心获取可用的服务列表,从而实现服务发现。服务提供者也可以通过定时发送心跳信号来保持在线,从而实现服务治理。

2、定义注册中心结构体以及常量

GeeRegistry结构体是整个注册中心的主要模块。里面包含了一个超时时间timeout和一个map类型的servers,用来存储服务实例的地址和启动时间。默认超时时间是5分钟,并默认设置了一个常量defaultPath作为注册中心的路径。
New函数用来创建一个GeeRegistry实例,可以传入超时时间的设置。DefaultGeeRegister是一个默认的GeeRegistry实例,超时时间使用默认值。

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

type ServerItem struct {
	Addr  string
	start time.Time
}

// 默认五分钟  并且注册中心是http通信的所以给一个路径
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,
	}
}

3、添加服务实例以及返回列表

putServer函数用来添加服务实例或更新已有实例的启动时间戳。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
}

4、处理HTTP请求

实现了ServeHTTP接口用来处理HTTP请求。其中
GET:返回可用的服务列表,将它们写入响应头部的 “X-Geerpc-Servers” 自定义字段中。这个字段是注册中心定义的,客户端可以通过访问这个字段获取到可用的服务列表。
POST:添加服务或发送心跳信号,从请求头部的 “X-Geerpc-Server” 自定义字段中获取服务实例地址。如果没有获取到地址,则返回错误信息;否则将该服务添加到服务映射表中。
HandleHTTP 方法注册了 GeeRegistry 消息处理器,使之在指定路径下面得以处理 HTTP 请求。参数 registryPath 是注册中心的路径。具体地,它调用了 HTTP 包的 Handle 函数,将路径和 GeeRegistry 实例 r 作为参数传递给 Handle 函数,以注册 HTTP 请求处理器。

// Get:返回所有可用的服务列表,通过自定义字段 X-Geerpc-Servers 承载。
// Post:添加服务实例或发送心跳,通过自定义字段 X-Geerpc-Server 承载。
// Runs at /_geerpc_/registry
func (r *GeeRegistry) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	switch req.Method {
	case "GET":
		// keep it simple, server is in req.Header
		w.Header().Set("X-Geerpc-Servers", strings.Join(r.aliveServers(), ","))
	case "POST":
		// keep it simple, server is in req.Header
		// 从响应头 拿到这个字段对应的value
		addr := req.Header.Get("X-Geerpc-Server")
		if addr == "" {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		r.putServer(addr)
	default:
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
}

// HandleHTTP registers an HTTP handler for GeeRegistry messages on registryPath
func (r *GeeRegistry) HandleHTTP(registryPath string) {
	http.Handle(registryPath, r)
	log.Println("rpc registry path:", registryPath)
}
var DefaultGeeRegister = New(defaultTimeout)
func HandleHTTP() {
	DefaultGeeRegister.HandleHTTP(defaultPath)
}

5、发送心跳

Heartbeat 函数:这个函数用于启动一个 goroutine,定时向服务注册中心发送心跳信号。参数 registry 是注册中心的地址,addr 是本地服务实例的地址,duration 是心跳信号发送的时间间隔。它在发送第一次心跳信号后,开启定时器等待 duration 时间间隔后再次发送心跳信号。在定时器周期内,如果发生错误(比如连不上注册中心),则停止发送心跳信号。
sendHeartbeat 函数:这个函数负责实际的心跳信号的发送。它构造一个 http POST 请求,将服务实例地址保存在请求头部的 “X-Geerpc-Server” 自定义字段中,并发送给注册中心。如果发送过程中出现错误,则返回错误信息,否则返回 nil。同时,会打印日志记录心跳信号的发送情况。


func Heartbeat(registry, addr string, duration time.Duration) {
	if duration == 0 {
		// 比默认的少一分钟
		duration = defaultTimeout - time.Duration(1)*time.Minute
	}
	var err error
	err = sendHeartbeat(registry, addr)
	go func() {
		t := time.NewTicker(duration)
		// 定时器 一旦时间到了 channel就会收到消息 变为不阻塞状态
		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
}

四、支持异步和并发的客户端实现

1、客户端结构体以及call结构体

lient结构体表示一个RPC客户端对象。它包含以下字段:
cc:表示编解码器codec.Codec,用于对请求和响应进行编解码。
opt: 表示客户端的选项Option,包括编码类型、超时时间等。
sending:是一个互斥锁,用来保证请求的有序发送,避免多个请求混淆。
header:表示消息头,用于对请求和响应进行标识。
mu:是一个互斥锁,保护以下字段。
seq:表示当前请求的序号,每次请求发送前会自增。
pending:表示未处理完的请求,以序号为键,以Call实例为值。
closing:表示用户是否主动关闭了客户端。
shutdown:表示服务端是否要求关闭连接或发生异常而终止。
Call结构体表示一个RPC调用。它包含以下字段:
Seq:表示此次调用的序号,与Client中的seq一致。
ServiceMethod:表示服务名和方法名,格式为 “service.method”。
Args:表示需要传递给服务端的参数。
Reply:表示服务端返回的结果。
Error:表示调用过程中发生的错误。
Done:表示调用完成后的通知管道,用于通知调用方响应已返回。

type Client struct {
	cc       codec.Codec
	//客户端设置
	opt      *Option
	// sending 是一个互斥锁,和服务端类似,为了保证请求的有序发送,即防止出现多个请求报文混淆
	sending  sync.Mutex // protect following
	header   codec.Header
	mu       sync.Mutex // protect following
	seq      uint64
	//存储未处理完的请求,键是编号,值是 Call 实例。
	pending  map[uint64]*Call
	//手动关闭  与服务器关闭
	closing  bool // user has called Close
	shutdown bool // server has told us to stop
}

// Call represents an active RPC.
type Call struct {
	Seq           uint64
	ServiceMethod string      // format "."
	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.
}

func (call *Call) done() {
	call.Done <- call
}

Client结构体的两个辅助方法,用于处理客户端关闭时的未处理完的请求。
removeCall方法通过序号seq从pending字段中获取并移除对应的Call实例,并返回这个实例。该方法使用了互斥锁client.mu来保证并发安全。
terminateCalls方法会在客户端关闭连接时被调用,它的参数err表示关闭原因。该方法先使用client.sending锁来确保所有还未发送的请求都被发送完成,然后使用client.mu锁遍历所有未处理完成的请求,并将它们的Error字段设置为err,表示这些请求已经被关闭,最后调用Call实例的done方法通知请求方。


// 把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
}
// b不是人微的关闭   
func (client *Client) terminateCalls(err error) {
	client.sending.Lock()
	defer client.sending.Unlock()
	client.mu.Lock()
	defer client.mu.Unlock()
	client.shutdown = true
	// 把队列中的请求都 加入err  表示关闭了
	for _, call := range client.pending {
		call.Error = err
		call.done()
	}
}

查看客户端是否还在

func (client *Client) IsAvailable() bool {
	client.mu.Lock()
	defer client.mu.Unlock()
	return !client.shutdown && !client.closing
}

2、客户端连接以及接收

a、解析配置文件函数

函数 parseOptions(opts …*Option) 主要是用于解析传入的 Option 参数,如果 opts 为空,则返回默认参数 DefaultOption;如果 opts 数组的长度超过1,则返回错误;如果 opts 的长度为1,则将其中的选项设置到 opt 中,并将 MagicNumber 字段设置为默认值,最后返回 opt。

func parseOptions(opts ...*Option) (*Option, error) {
	// if opts is nil or pass nil as parameter
	if len(opts) == 0 || opts[0] == nil {
		return DefaultOption, nil
	}
	if len(opts) != 1 {
		return nil, errors.New("number of options is more than 1")
	}
	opt := opts[0]
	opt.MagicNumber = DefaultOption.MagicNumber
	if opt.CodecType == "" {
		opt.CodecType = DefaultOption.CodecType
	}
	return opt, nil
}

b、客户端的关闭以及接收信息

receive() 函数会不断从客户端的编解码器(codec)中读取消息头和消息体,直到出现错误为止。在读取消息头时,如果发现有错误,则退出循环。否则,根据消息头中的调用序列号 seq,从客户端的 calls 中查找对应的 Call 对象。如果没找到,则说明出现了异常情况,客户端接收到了无效的响应数据,此时忽略掉消息体并继续读取下一条消息;
如果找到了对应的 Call 对象,则根据消息头中的 Error 字段判断调用是否出现了错误:如果 Error 不为空,则表示出现了错误,此时将错误信息保存到 Call 对象的 Error 字段中,并忽略掉消息体;否则,表示调用正常,那么就从消息体中读取调用结果,并将其保存到 Call 对象的 Reply 字段中。

terminateCalls() 方法,用于终止正在等待响应的所有 Call 对象。当客户端关闭连接时,或者出现严重的编解码错误时,都需要调用该方法清理掉所有等待响应的 Call 对象,避免资源泄漏,由于 receive() 函数是一个无限循环,并且是在独立的 goroutine 中运行的,因此 terminateCalls() 方法也需要保证它的线程安全性。


// Close the connection
func (client *Client) Close() error {
	client.mu.Lock()
	defer client.mu.Unlock()
	if client.closing {
		return errors.New("connection is shut down")
	}
	client.closing = true
	return client.cc.Close()
}
//接受响应
func (client *Client) receive() {
	var err error
	for err == nil {
		var h codec.Header
		// 从编解码器读出 h和body 
		if err = client.cc.ReadHeader(&h); err != nil {
			break
		}
		fmt.Println("/",h.Seq)
		call := client.removeCall(h.Seq)
		// 根据seq移除调用   call分为3种
		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)
}

c、超时处理以及客户端的创建

NewClient 函数接受两个参数:conn net.Conn 和 opt *Option,其中 conn 表示已经建立的与RPC服务器之间的网络连接,之后马上传opt给服务器。opt 则为客户端的配置选项。该函数的返回值为一个包含所有必要状态信息的 Client 实例和错误信息。

func NewClient(conn net.Conn, opt *Option) (*Client, error) {
	f := codec.NewCodecFuncMap[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
	}

	client := &Client{
		seq:     1, // seq starts with 1, 0 means invalid call
		cc:      f(conn),
		opt:     opt,
		pending: make(map[uint64]*Call),
	}
	go client.receive()
	return client, nil

}

代码通过在 net.DialTimeout 中设置连接超时时间,实现了连接超时机制。同时,还使用了 Go 语言中的 select 语句和 time.After 函数来实现客户端 Dial 方法的超时处理。

具体来说,当调用 XDial 函数建立连接时,会首先创建一个名为 ch 的无缓冲信道。然后通过启动一个新的协程来执行 NewClient 函数,如果该函数执行成功,则将 client 和 nil 作为一个结构体 clientResult 的参数,通过信道 ch 进行发送。
在此期间,我们使用 time.After(opt.ConnectTimeout) 来创建一个计时器,该计时器会在 opt.ConnectTimeout 秒后向其返回的信道发送一个值。在 select 语句中,我们使用 case result := <-ch: 来等待 NewClient 函数执行完毕,并尝试从信道 ch 中接收计算结果。另外,如果时间限制超时,则在 case <-time.After(opt.ConnectTimeout): 中直接返回错误信息,否则等待 case result := <-ch: 的结果并返回。

type clientResult struct {
	client *Client
	err    error
}

// 这是同一调用接口 
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)
	}
	// rpcAddr获取协议  以及地址
	protocol, addr := parts[0], parts[1]
	opt, err := parseOptions(opts...)
	if err != nil {
		return nil, err
	}

	conn, err := net.DialTimeout(protocol, addr, opt.ConnectTimeout)
	if err != nil {
		return nil, err
	}
	// close the connection if client is nil
	defer func() {
		if err != nil {
			_ = conn.Close()
		}
	}()
	ch := make(chan clientResult)
	go func() {
		// 启动一个协程 进行创建
		client, err := NewClient(conn, opt)
		ch <- clientResult{client: client, err: err}
	}()
	if opt.ConnectTimeout == 0 {
		result := <-ch
		return result.client, result.err
	}
	select {
	case <-time.After(opt.ConnectTimeout):
		return nil, fmt.Errorf("rpc client: connect timeout: expect within %s", opt.ConnectTimeout)
	case result := <-ch:
		return result.client, result.err
	}
}

3、客户端并发以及异步发送

具体实现

在 Go 方法中,它接收服务方法名、参数、返回对象和完成通道为参数,然后创建一个 Call 结构并向服务端发送请求。完成通道用于标识请求是否完成,并且 Call 结构体会存储服务方法名、参数和返回对象。最后,Go 方法返回 Call 结构体。

在 Call 方法中,它接收上下文、服务方法名、参数和返回对象为参数,并使用 Go 方法发起异步调用。然后它等待完成通道中响应并返回错误信息或者超时或者上下文取消。如果完成通道中响应,则返回 Call 结构体中的错误信息。如果超时或者上下文取消,则需要删除请求并返回错误信息。

在 send 方法中,它会对 Call 结构体进行注册,在 pending 字段中存储它,并将其发送到服务端。序列号 Seq 是唯一的,是通过加锁的 map 来实现的。然后,它会准备相应的请求头,并将其编码后发送给服务端。如果发送请求时出错,则需要从 pending 字段中删除请求并返回错误信息。

func (client *Client) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error {
	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
	}
}

// Go invokes the function asynchronously.
// It returns the Call structure representing the invocation.
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) send(call *Call) {
	// make sure that the client will send a complete request
	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
	// // 通过解码器来发送
	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()
		}
	}
}
//发送
//把请求放在pending里面 然后seq加  共享资源map要加锁
func (client *Client) registerCall(call *Call) (uint64, error) {
	client.mu.Lock()
	defer client.mu.Unlock()
	if client.closing || client.shutdown {
		return 0, errors.New("connection is shut down")
	}
	call.Seq = client.seq
	client.pending[call.Seq] = call
	client.seq++
	return call.Seq, nil
}

并发以及超时处理解析

整个过程是并发的,多个 goroutine 可以同时进行 RPC 请求。并且使用了 sync.Mutex 来保证共享资源访问的并发安全,即在对共享资源进行修改时需要先获取锁,避免多个 goroutine 同时去读写共享资源,造成数据竞争的问题。

在超时处理方面,使用了 select 语句和 Done 通道来实现。在 Call 方法中调用 Go 方法时,会传递一个 context.Context 对象,其中包含了本次请求所需的上下文信息,包括超时时间和取消信号等。在 select 语句中等待 Done 通道或者 ctx.Done() 信号,如果 Done 通道返回结果,则返回响应信息;如果 ctx.Done() 信号被触发,则取消请求并返回错误信息。

五、客户端实现服务发现以及负载均衡

在基础客户端的基础上实现一个可以支持服务发现以及负载均衡的客户端XClient。

1、支持服务发现与负载均衡

需要做的就是通过注册中心获取可用的服务地址,并提供了负载均衡的选择策略。

a、结构体

首先定义 SelectMode 枚举类型,表示不同的服务选择方式,包括随机选择和轮询选择两种。然后定义 Discovery 接口,包含 RefreshGet 两个方法。Refresh 方法用于从注册中心刷新服务列表,Get 方法用于返回一个可用的服务地址。
定义了 ServerDiscovery 结构体,它实现了 Discovery 接口,通过注册中心获取可用的服务地址。registry 字段保存注册中心的地址;timeout 表示服务列表的过期时间,默认是10秒;lastUpdate 表示最后一次从注册中心更新服务列表的时间;
r 是一个随机数生成器,保证每次生成的随机数不同;mu 用于保护共享变量的读写安全;servers 是可用的服务地址列表;index 是记录已选定服务器下标的变量;
NewServerDiscovery 函数用于创建 ServerDiscovery 实例,需要传入注册中心地址和过期时间两个参数。如果传入的时间为0,则使用默认过期时间。


type SelectMode int
const (
	RandomSelect     SelectMode = iota // select randomly
	RoundRobinSelect                   // select using Robbin algorithm
)
type Discovery interface {
	// 初始化时使用时间戳设定随机数种子,避免每次产生相同的随机数序列。
	Refresh() error // refresh from remote registry
	Get(mode SelectMode) (string, error)
}
type ServerDiscovery struct {
	registry   string
	timeout    time.Duration
	// astUpdate 是代表最后从注册中心更新服务列表的时间,默认 10s 过期,
	// 即 10s 之后,需要从注册中心更新新的列表。
	lastUpdate time.Time
///
	r       *rand.Rand   // generate random number
	mu      sync.RWMutex // protect following
	servers []string
	index   int // record the selected posi

}
const defaultUpdateTimeout = time.Second * 10

func NewServerDiscovery(registerAddr string, timeout time.Duration) *ServerDiscovery {
	if timeout == 0 {
		timeout = defaultUpdateTimeout
	}
	d := &ServerDiscovery{
		registry:              registerAddr,
		timeout:               timeout,
		servers: make([]string, 0),
		r:       rand.New(rand.NewSource(time.Now().UnixNano())),
	}
	return d
}


b、服务发现

通过隔一段时间通过注册中心获取可用的服务地址刷新服务列表。Refresh 方法用于刷新服务列表,当服务列表超时后需要重新向注册中心获取可用的服务地址。在获取到服务地址列表之后,将其保存到 servers 变量中,并更新 lastUpdate 变量的值。

// 刷新 服务列表
func (d *ServerDiscovery) Refresh() error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.lastUpdate.Add(d.timeout).After(time.Now()) {
		return nil
	}
	log.Println("rpc registry: refresh servers from registry", d.registry)
	resp, err := http.Get(d.registry)
	if err != nil {
		log.Println("rpc registry refresh err:", err)
		return err
	}
	// 从返回header中 resp.Header获取
	servers := strings.Split(resp.Header.Get("X-Geerpc-Servers"), ",")
	d.servers = make([]string, 0, len(servers))
	for _, server := range servers {
		if strings.TrimSpace(server) != "" {
			d.servers = append(d.servers, strings.TrimSpace(server))
		}
	}
	d.lastUpdate = time.Now()
	return nil
}

c、根据负载均衡算法获取一个服务地址

获取到刷新之后的服务列表在根据负载均衡算法策略对列表中的服务器进行选择,然后返回一个选择的服务地址。
对于随机选择模式,通过调用rand.Intn(n)方法生成一个位于0~n-1之间的随机数,将该随机数作为d.servers的下标,从可用服务器地址列表中随机选择一个服务器,并返回其地址。
对于轮询选择模式,首先通过取模运算计算出当前已选定服务器的下标。然后从可用的服务器地址列表中选择该下标对应的服务器,并将下标递增一。如果递增后的下标大于等于可用服务器地址列表的长度,就将其设置为0。最后返回已选定服务器的地址,并更新下一个需要选择的服务器下标。



func (d *ServerDiscovery) Get(mode SelectMode) (string, error) {
	// 先调用 Refresh 确保服务列表没有过期
	if err := d.Refresh(); err != nil {
		return "", err
	}
	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%n] // servers could be updated, so mode n to ensure safety
		d.index = (d.index + 1) % n
		fmt.Println("MultiServersDiscovery push ",s)
		return s, nil
	default:
		return "", errors.New("rpc discovery: not supported select mode")
	}
}

2、新客户端

为了尽量复用已经建立好的 Socket 连接,减少因为频繁创建和关闭连接所引起的性能损耗。将 Client 实例缓存到 XClient 的 clients 字典中,可以在下次调用服务时,直接使用缓存中的 Client 实例,而不必再重新创建连接,从而提高性能。
所以新客户端主要从服务发现以及负载均衡器中获取服务地址,然后根据给定的 rpcAddr 获取或者生成一个 Client 实例,并把它缓存到 XClient 的 clients 字典中,以便下次使用。

type XClient struct {
	d       Discovery
	mode    SelectMode
	opt     *Option
	mu      sync.Mutex // protect following
	// 为了尽量地复用已经创建好的 Socket 连接,使用 clients 保存创建成功的 Client 实例
	clients map[string]*Client
}
// 它的意思是将 *XClient 类型转换为实现了 io.Closer 接口的类型。
var _ io.Closer = (*XClient)(nil)

func NewXClient(d Discovery, mode SelectMode, opt *Option) *XClient {
	return &XClient{d: d, mode: mode, opt: opt, clients: make(map[string]*Client)}
}

//关闭所有的
func (xc *XClient) Close() error {
	xc.mu.Lock()
	defer xc.mu.Unlock()
	// 关闭所有的 客户端
	for key, client := range xc.clients {
		// I have no idea how to deal with error, just ignore it.
		_ = client.Close()
		delete(xc.clients, key)
	}
	return nil
}


// 通过rpcAddr 获取或者生成client  就不用每次连接
func (xc *XClient) dial(rpcAddr string) (*Client, error) {
	xc.mu.Lock()
	defer xc.mu.Unlock()
	client, ok := xc.clients[rpcAddr]
	// 检查 xc.clients 是否有缓存的 Client,
	// 如果有,检查是否是可用状态,如果是则返回缓存的 Client,
	// 如果不可用,则从缓存中删除。
	if ok && !client.IsAvailable() {
		_ = client.Close()
		delete(xc.clients, rpcAddr)
		client = nil
	}
	// 如果不存在就新建一个  并且加入缓存
	if client == nil {
		var err error
		client, err = XDial(rpcAddr, xc.opt)
		fmt.Println("new client:",rpcAddr)
		if err != nil {
			return nil, err
		}
		xc.clients[rpcAddr] = client
	}else{
		fmt.Println("old client:",rpcAddr)
	}

	return client, nil
}

// Call invokes the named function, waits for it to complete,
// and returns its error status.
// xc will choose a proper server.
// 实现负载均衡 就要每次call 调用服务都需要获取服务地址  然后根据服务地址的客户端 
// 请求服务 
func (xc *XClient) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error {
	//从负载均衡中获取地址
	rpcAddr, err := xc.d.Get(xc.mode)
	if err != nil {
		return err
	}
	client, err := xc.dial(rpcAddr)
	if err != nil {
		return err
	}
	return client.Call(ctx, serviceMethod, args, reply)
}

六、服务器

服务器基本就是把service服务的存储在 serviceMap,然后读取响应获取响应的服务,然后响应,先注册中心发送心跳已经在注册中心的那部分完成了。

首先定义了一个常量 MagicNumber,表示这是一个 geerpc 的请求。同时定义了一个 Option 结构体,包含了一些选项参数,如编解码类型、超时时间等。另外还定义了一个默认值 DefaultOption,其中包含了使用 Gob 编解码的设置和默认的超时时间为 10 秒钟。

接下来是 Server 结构体,其中包含了一个 serviceMap 的同步映射,用来保存多个服务,每个服务包含多个方法。然后是 NewServer() 函数,返回新的服务器实例。

接着是 Register(rcvr interface{}) 方法,通过给定的接收器创建一个新的服务。具体地说,它会将 rcvr 转换成一个 service 结构体,并将其存储在 serviceMap 中。如果该服务已经存在,则返回错误。

const MagicNumber = 0x3bef5c

//
type Option struct {
	MagicNumber int        // MagicNumber marks this's a geerpc request
	CodecType   codec.Type // client may choose different Codec to encode body
	// 两个超时处理
	ConnectTimeout time.Duration // 0 means no limit
	HandleTimeout  time.Duration
}
//默认使用gob序列化
var DefaultOption = &Option{
	MagicNumber: MagicNumber,
	// CodecType:   codec.GobType,
	CodecType:   codec.JsonType,
	// 默认10秒
	ConnectTimeout: time.Second * 10,
}

// Server represents an RPC Server.
// service 保存好评多个 服务 也就是i多个结构体  每个服务里面有多个方法   考虑到并发安全所以用了这个map
type Server struct{
	serviceMap sync.Map
}

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

// Register publishes in the server the set of methods of the
func (server *Server) Register(rcvr interface{}) error {
	s := newService(rcvr)
    for key, _ := range s.method {
        fmt.Printf("%s \n", key)
		// registry.Heartbeat(registryAddr, "tcp@"+serverAddr.Addr().String()+ "/"+s.name+"/"+key, 15 * time.Second)
    }
	if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup {
		
		// 先注册中心 发送心跳注册注册
		return errors.New("rpc: service already defined: " + s.name)
		
	}
	return nil
}
// Register publishes in the server the set of methods of the
func (server *Server) NewRegister(registryAddr string,serverAddr net.Listener,rcvr interface{}) error {
	s := newService(rcvr)

    for key, _ := range s.method {
        fmt.Printf("%s \n", key)
		registry.Heartbeat(registryAddr, "tcp@"+serverAddr.Addr().String()+ "/"+s.name+"/"+key, 15 * time.Second)
    }
	if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup {
		
		// 先注册中心 发送心跳注册注册
		return errors.New("rpc: service already defined: " + s.name)
		
	}
	return nil
}



然后是 findService(serviceMethod string) 方法,用于根据给定的服务方法字符串查找对应的服务和方法。如果输入格式不正确或者找不到服务或方法,则返回相应的错误。否则返回服务和方法类型。

接下来是 ServeConn(conn io.ReadWriteCloser) 方法,用于在单个连接上运行服务器。首先解码传递的选项,然后检查魔数是否正确。通过指定的编解码类型创建一个 Codec 实例,并使用互斥锁和等待组管理请求的处理。然后循环读取请求,处理请求,直到客户端挂断连接。


// 传入 服务 方法  字符串 找到对应的服务和方法  并且返回
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
}



// ServeConn runs the server on a single connection.
// ServeConn blocks, serving the connection until the client hangs up.
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
	defer func() { _ = conn.Close() }()
	var opt Option
	if err := json.NewDecoder(conn).Decode(&opt); err != nil {
		log.Println("rpc server: options error: ", err)
		return
	}
	if opt.MagicNumber != MagicNumber {
		log.Printf("rpc server: invalid magic number %x", opt.MagicNumber)
		return
	}
	// 解析出 解码器函数
	f := codec.NewCodecFuncMap[opt.CodecType]
	if f == nil {
		log.Printf("rpc server: invalid codec type %s", opt.CodecType)
		return
	}
	sending := new(sync.Mutex) // make sure to send a complete response
	wg := new(sync.WaitGroup)  // wait until all request are handled

	cc:=f(conn);
	for {
		req, err := server.readRequest(cc)
		if err != nil {
			if req == nil {
				break // it's not possible to recover, so close the connection
			}
			req.h.Error = err.Error()
			server.sendResponse(cc, req.h, invalidRequest, sending)
			continue
		}
		wg.Add(1)
		go server.handleRequest(cc, req, sending, wg,opt.HandleTimeout)
	}
	wg.Wait()
	_ = cc.Close()
}



之后是 invalidRequest 结构体,用作错误响应时的占位符。同时还有一个 request 结构体,用于存储调用的所有信息。其中包含了请求头、参数、服务、方法等。

接下来是 readRequestHeader(cc codec.Codec) (*codec.Header, error) 方法,用于读取请求头。如果出现错误,则返回空指针和错误信息。否则返回请求头。

接着是 readRequest(cc codec.Codec) (*request, error) 方法,用于读取请求并将其解析。具体地说,它会先读取请求头,然后查找对应的服务和方法,并将参数读入相应的参数值中。最后返回请求实例。

然后是 sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) 方法,用于发送响应。它使用互斥锁确保发送完整的响应。如果发送错误,则记录日志。

接下来是 handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup, timeout time.Duration) 方法,用于处理请求。首先创建两个 channel,一个用于通知方法已经被调用,另一个用于通知响应已经发送。然后启动一个新的 goroutine,调用 svc.call() 方法,并根据情况发送相应的错误信息或响应。如果超时时间为 0,则等待方法完成;否则使用 select 语句进行超时处理。最后,减少等待组的计数器。

// invalidRequest is a placeholder for response argv when error occurs
var invalidRequest = struct{}{}

// request stores all information of a call
type request struct {
	h            *codec.Header // header of request
	argv, replyv reflect.Value // argv and replyv of request
	mtype        *methodType
	svc          *service
}

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
}

//读取请求  找到对应的服务 和方法 存放到req.svc, req.mtype,  然后 把参数读出来 存放到argvi中去
func (server *Server) readRequest(cc codec.Codec) (*request, error) {
	h, err := server.readRequestHeader(cc)
	if err != nil {
		return nil, err
	}
	req := &request{h: h}
	// TODO: now we don't know the type of request argv
	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()
	}
	if err = cc.ReadBody(argvi); err != nil {
		log.Println("rpc server: read body err:", err)
		return req, err
	}
	return req, 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)
	}
}
// 处理请求  这个req的服务 调用call 把这个req的 方法类型 输入参数 都传进去 返回reply
// 超时处理 通过channel进行 如果  而且分为send和call
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
	}
}
// Accept accepts connections on the listener and serves requests
// for each incoming connection.
func (server *Server) Accept(lis net.Listener) {
	for {
		conn, err := lis.Accept()
		if err != nil {
			log.Println("rpc server: accept error:", err)
			return
		}
		// 得到 网络连接
		go server.ServeConn(conn)
	}
}

最后是 Accept(lis net.Listener) 方法,用于接受连接并运行服务器。它会循环接受连接,并在新的 goroutine 中为每个连接运行 ServeConn(conn io.ReadWriteCloser) 方法。

七、更完善的注册中心

总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

你可能感兴趣的:(分布式系统架构,项目总结,golang,rpc,分布式,负载均衡)