Go语言高并发与微服务实战专题精讲——远程过程调用 RPC——服务端注册源代码实现原理分析

远程过程调用 RPC——服务端注册源代码实现原理分析

rpc server demo代码参考我前一篇博文:https://www.cnblogs.com/zuoyang/p/18146870

RPC Server端的RPC代码架构主要由两大部分构成:

      1. 第一部分是服务方法的注册过程。在这个过程中, 我们首先通过调用rpc.Register接口将服务对象(如示例中的StringService)的方法进行注册。rpc.Register函数内部会利用反射机制, 自动提取出服务对象中满足RPC接口要求的方法 (即那些接收两个参数且返回error类型结果的方法), 并将这些方法的信息存储在一个内部的方法映射表中。在我们的Server端代码中,rpc.Register(service)这行代码就完成了StringService中的ConcatDiff方法的注册, 使得这些方法可以被远程调用。
      2. 第二部分是处理来自网络的RPC调用。Server端会监听指定的端口(如示例中的:1234),等待客户端的连接。一旦有客户端连接成功,Server就会读取传入的数据包, 解码RPC请求, 并根据请求中指定的方法名从方法映射表中查找到对应的方法。然后, Server会通过反射调用该方法, 将结果编码后发送回客户端。在我们的Server端代码中, 这部分逻辑是通过l.Accept()接受连接, 并使用rpc.ServeConn(conn)来处理每一个RPC请求实现的。

关于第一部分的处理步骤, 可以概括为以下几个环节:

      1. 首先, 通过rpc.Register(service)注册StringService中的RPC方法, 如Concat
      2. 接着, rpc包利用Go语言的反射功能, 自动获取已注册方法的详细信息, 如方法名、参数类型和返回值类型等。
      3. 最后, 这些信息被保存在RPC服务器的内部数据结构中, 以便在处理客户端的RPC调用时能够快速定位和执行相应的方法。

一、注册服务

Register 和 RegisterName 方法是进行 RPC 服务注册的入口方法, 其参数 interface{} 类型的 rcvr 就是要注册的 RPC 服务类型。这两个方法都直接调用了 DefaultServer 的相应方法,

C:\Program Files\Go\src\net\rpc\server.go,源代码如下所示:

上述代码中的 DefaultServer 是 rpc 库自带的默认网络 Server 的实例, 它的定义如下所示:

  来, 我们详细看看,在Go的RPC(远程过程调用)框架中, 服务的注册是一个关键步骤, 它使得客户端能够通过网络调用服务端的方法。在提供的代码片段中, RegisterRegisterName是两个用于注册RPC服务的方法, 它们都是对DefaultServer中相应方法的封装。

  首先, 我们来看一下DefaultServer的定义:

// DefaultServer is the default instance of *Server.  
var DefaultServer = NewServer()

  DefaultServer是一个全局变量, 它是Server类型的一个实例。Server类型代表了一个RPC服务器, 其中包含了服务映射(serviceMap)、请求锁(reqLock)和响应锁(respLock)等字段,用于处理并发的RPC请求和响应。通过调用NewServer()函数, 我们创建了一个新的Server实例, 并将其赋值给DefaultServer

  接下来, 我们分析RegisterRegisterName函数:

1、Register 函数:

func Register(rcvr any) error {  
    return DefaultServer.Register(rcvr)  
}


  Register函数接收一个interface{}类型的参数rcvr, 这个参数代表要注册的RPC服务类型。函数内部直接调用了DefaultServerRegister方法, 将服务注册到默认的RPC服务器上。如果注册成功, 则返回nil, 如果注册失败, 则返回一个错误。

  需要注意的是, Register函数使用接收者的具体类型作为服务的名称。这意味着, 如果你注册了同一个类型的多个接收者, 后面注册的接收者会覆盖先前注册的服务。

2、RegisterName 函数:

func RegisterName(name string, rcvr any) error {  
    return DefaultServer.RegisterName(name, rcvr)  
}


  与Register函数类似, RegisterName函数也接收一个RPC服务类型作为参数, 但是它还额外接收一个字符串参数name, 用于指定服务的名称。这样, 你可以为同一类型的多个接收者指定不同的服务名称, 从而避免服务被覆盖的问题。

  在RegisterName函数内部, 同样调用了DefaultServerRegisterName方法来完成服务的注册。

3、Register 和 RegisterName 函数的关键区别: 

registerregisterName函数在Go语言的RPC(远程过程调用)框架中用于注册服务, 以便远程客户端可以调用这些服务。除了函数名不同之外, 它们之间还有以下关键区别:

1. 服务命名方式:

register函数使用接收者的具体类型名作为RPC服务的名称。这意味着, 如果你有两个相同类型的服务实例并尝试使用register进行注册, 后注册的服务会覆盖先前注册的服务, 因为它们共享相同的类型名。
registerName函数允许开发者显式地指定一个字符串作为服务的名称, 而不是使用接收者的类型名。这个特性提供了更大的灵活性, 允许为同一类型的多个服务实例注册不同的名称, 从而避免了命名冲突和服务覆盖的问题。

2. 使用场景:

当你希望自动化管理服务名称, 并且确信同一类型只会有一个服务实例被注册时, 可以使用register函数。
当你需要为同一类型的多个服务实例提供不同的访问点时, 或者想要更精细地控制服务名称时, 应该使用registerName函数。

3. 参数差异:

register函数只需要一个参数, 即要注册的服务实例(rcvr), 它是一个接口类型的变量, 表示任何符合RPC规范的结构体或对象。
registerName函数需要两个参数:一个自定义的服务名称(name)和要注册的服务实例(rcvr)。

registerregisterName函数的主要区别在于服务命名方式、使用场景以及函数参数的不同。这些差异使得开发者能够根据具体需求灵活选择适合的注册方式。 

二、反射处理

我们再来具体看一下RPC ServerRegister方法的实现。

通过反射获取接口类型和值,并通过suitableMethods函数判断注册的RPC 是否符合规范,最后调用serviceMapLoadOrStore(sname, s)方法将对应RPC 存根存放于map中,供以后查找。

C:\Program Files\Go\src\net\rpc\server.go,具体代码如下所示:


// 这段Golang代码定义了一个Server结构体,用于实现RPC(远程过程调用)服务。它包含了注册、检查类型和方法是否符合RPC要求的相关功能。以下是详细解释:

// Server结构体
type Server struct {
	serviceMap sync.Map   // map[string]*service 使用sync.Map存储已注册的服务(键为字符串,值为*service)。每个服务包含一个接收者对象及其可调用的方法。
	reqLock    sync.Mutex // protects freeReq 用于同步的锁对象,用于确保在处理请求时不会发生并发冲突。
	freeReq    *Request  // 空闲请求对象队列,用于存储不再使用的请求对象。
	respLock   sync.Mutex // protects freeResp 用于保护和管理响应对象的互斥锁对象队列。
	freeResp   *Response  // 用于保护和管理响应对象的空闲响应对象队列。
}

// 返回一个新创建的Server实例。
func NewServer() *Server {
	return &Server{}
}

// 默认的Server实例, 通过NewServer()初始化。
var DefaultServer = NewServer()

// isExportedOrBuiltinType函数, 用于检查给定的类型是否为导出的类型或内置类型。
func isExportedOrBuiltinType(t reflect.Type) bool {
  //对于指针类型,递归获取其元素类型。
	for t.Kind() == reflect.Pointer {
		t = t.Elem() // 获取指针指向的元素类型
	}
	// PkgPath will be non-empty even for an exported type,
	// so we need to check the type name as well.
  // 检查类型名是否以大写字母开头(表示导出),或其包路径是否为空(表示内置类型)。
	return token.IsExported(t.Name()) || t.PkgPath() == ""
}

// Register publishes in the server the set of methods of the
// receiver value that satisfy the following conditions:
//   - exported method of exported type
//   - two arguments, both of exported type
//   - the second argument is a pointer
//   - one return value, of type error
//
// It returns an error if the receiver is not an exported type or has
// no suitable methods. It also logs the error using package log.
// The client accesses each method using a string of the form "Type.Method",
// where Type is the receiver's concrete type.
//
// Server的Register方法, 用于将接收者的方法注册到服务中。
// 注册一个接收者对象到服务器,使其提供的方法可供远程调用。
// 接收两个参数:rcvr(任何类型的接收者对象)和name(服务名称,若留空则使用接收者的类型名)。
// 调用register方法进行实际注册,传入useName=false表示使用接收者的类型名作为服务名。
func (server *Server) Register(rcvr any) error {
	return server.register(rcvr, "", false)
}

// RegisterName is like Register but uses the provided name for the type
// instead of the receiver's concrete type.
//
// RegisterName方法与Register类似,但使用提供的名称作为服务名称。
// 类似Register,但允许用户指定服务名称。
// 调用register方法进行实际注册,传入useName=true表示使用指定的name作为服务名。
func (server *Server) RegisterName(name string, rcvr any) error {
	return server.register(rcvr, name, true)
}

// logRegisterError specifies whether to log problems during method registration.
// To debug registration, recompile the package with this set to true.
//
// logRegisterError控制是否在方法注册期间记录问题。
// 要调试注册,请将此设置为true重新编译该包。
const logRegisterError = false

// Server的register方法:
// 注册一个接收者对象到服务器,使其提供的方法可供远程调用。
func (server *Server) register(rcvr any, name string, useName bool) error {
  // 创建一个新的服务存根对象s。
	s := new(service)
  // 获取接收者的类型。
	s.typ = reflect.TypeOf(rcvr)
  // 获取接收者的值。
	s.rcvr = reflect.ValueOf(rcvr)
  // 获取接收者的类型名。
	sname := name
  // 如果服务名未提供(!useName),则从接收者类型中提取
	if !useName {
		sname = reflect.Indirect(s.rcvr).Type().Name()
	}
  // 如果服务名不为空且不是导出的类型,则打印错误日志并返回错误。
	if sname == "" {
		s := "rpc.Register: no service name for type " + s.typ.String()
		log.Print(s)
		return errors.New(s)
	}
  // 如果服务名已存在,则打印错误日志并返回错误。
	if !useName && !token.IsExported(sname) {
		s := "rpc.Register: type " + sname + " is not exported"
		log.Print(s)
		return errors.New(s)
	}
  // 设置服务名称。
	s.name = sname

	// Install the methods
  // 获取接收者的方法。
	s.method = suitableMethods(s.typ, logRegisterError)

  // 如果接收者的方法为空,则打印错误日志并返回错误。
	if len(s.method) == 0 {
		str := ""

		// To help the user, see if a pointer receiver would work.
    // 如果接收者的方法为空,则尝试使用指针接收者。
		method := suitableMethods(reflect.PointerTo(s.typ), false)
    // 如果指针接收者的方法不为空,则打印警告日志。
		if len(method) != 0 {
			str = "rpc.Register: type " + sname + " has no exported methods of suitable type (hint: pass a pointer to value of that type)"
		} else {
			str = "rpc.Register: type " + sname + " has no exported methods of suitable type"
		}
		log.Print(str)
		return errors.New(str)
	}

  // 调用server.serviceMap.LoadOrStore方法将服务存根对象s存入服务映射map中。
	if _, dup := server.serviceMap.LoadOrStore(sname, s); dup {
    // 如果服务名已存在,则打印错误日志并返回错误。
		return errors.New("rpc: service already defined: " + sname)
	}
	return nil
}

三、存根保存

suitableMethods函数判断注册的服务类型是否符合规范,它会生成map[string]*methodType。它会遍历类型中所有的方法, 依次判断方法能否被输出,取出其参数类型和返回类型,最后统一存储在返回值的map中,实现代码如下所示:

    // suitableMethods returns suitable Rpc methods of typ. It will log
    // errors if logErr is true.
    //
    // suitableMethods函数返回typ类型的符合RPC规范的方法。
    func suitableMethods(typ reflect.Type, logErr bool) map[string]*methodType {
      // 创建一个map,用于存储符合RPC规范的方法。
      methods := make(map[string]*methodType)
      // 遍历typ的所有方法。
      for m := 0; m < typ.NumMethod(); m++ {
        // 获取typ的方法。
        method := typ.Method(m)
        // 获取方法的类型。
        mtype := method.Type
        // 获取方法的名称。
        mname := method.Name
    
        // Method must be exported.
        // 方法必须是导出的。
        if !method.IsExported() {
          continue
        }
    
        // Method needs three ins: receiver, *args, *reply.
        // 方法应具有三个输入参数(接收者、*args、*reply)。
        if mtype.NumIn() != 3 {
          if logErr {
            log.Printf("rpc.Register: method %q has %d input parameters; needs exactly three\n", mname, mtype.NumIn())
          }
          continue
        }
    
        // First arg need not be a pointer.
        // 第一个参数(接收者)无特定要求。
        argType := mtype.In(1)
        if !isExportedOrBuiltinType(argType) {
          if logErr {
            log.Printf("rpc.Register: argument type of method %q is not exported: %q\n", mname, argType)
          }
          continue
        }
    
        // Second arg must be a pointer.
        // 第二个参数(args)必须为指针类型。
        replyType := mtype.In(2)
        if replyType.Kind() != reflect.Pointer {
          if logErr {
            log.Printf("rpc.Register: reply type of method %q is not a pointer: %q\n", mname, replyType)
          }
          continue
        }
    
        // Reply type 必须为导出的。
        if !isExportedOrBuiltinType(replyType) {
          if logErr {
            log.Printf("rpc.Register: reply type of method %q is not exported: %q\n", mname, replyType)
          }
          continue
        }
    
        // 方法应有一个输出参数。
        if mtype.NumOut() != 1 {
          //  如果方法没有输出参数,则打印错误日志并继续。
          if logErr { 
            log.Printf("rpc.Register: method %q has %d output parameters; needs exactly one\n", mname, mtype.NumOut())
          }
          continue
        }
        // 输出参数必须为error类型。
        if returnType := mtype.Out(0); returnType != typeOfError {
          if logErr {
            log.Printf("rpc.Register: return type of method %q is %q, must be error\n", mname, returnType)
          }
          continue
        }
        methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType}
      }
      return methods
    }
posted @ 2024-04-20 15:18  左扬  阅读(13)  评论(0编辑  收藏  举报
levels of contents