net/rpc源码分析

1. 前言

首先介绍一下rpc的概念,首先它是一种框架,而不是一种协议, 它的初衷是让用户可以向调本地方法一样调用远端的服务,在通信过程中,客户端和服务端需要约定一种通信机制,确保发送者发送的数据能被接收者能正确的解析,这就是协议 ,

  • 如grpc框架采用的是HTTP2协议,而HTTP2是采用protocolbuf来进行编解码。

  • dubbo rpc框架采用dubbo通信协议,编解码也是采用dubbo特有的编解码方式

  • sofaRpc框架采用的是bolt私有协议,由蚂蚁金服开发

golang官方的net/rpc库使用encoding/gob进行编解码,支持tcp或http数据传输方式,由于其他语言不支持gob编解码方式,所以使用net/rpc库实现的RPC方法没办法进行跨语言调用。

net/rpc同时支持tcp或http数据传输方式。

本文将首先展示使用net/rpc包进行rpc调用的主要方法,随后对于net/rpc包的结构和流程进行具体分析。

2. 服务端

点击查看代码
package main

import (
    "fmt"
    "log"
    "net"
    "net/http"
    "net/rpc"
    "os"
)

// 算数运算结构体
type Arith struct {
}

// 算数运算请求结构体
type ArithRequest struct {
    A int
    B int
}

// 算数运算响应结构体
type ArithResponse struct {
    Result int
}


// 加法运算方法
func (this *Arith) Add(req ArithRequest, res *ArithResponse) error {
    res.Result = req.A + req.B
    return nil
}

func main() {
    rpc.Register(new(Arith)) // 注册rpc服务
    rpc.HandleHTTP()         // 采用http协议作为rpc载体

    lis, err := net.Listen("tcp", "127.0.0.1:8006")
    if err != nil {
        log.Fatalln("fatal error: ", err)
    }

    fmt.Fprintf(os.Stdout, "%s", "start connection")
    http.Serve(lis, nil)
}

HandleHTTP()方法内容如下:

// rpc/server.go
var DefaultServer = NewServer()

func NewServer() *Server {
	return &Server{}
}

type Server struct {
	serviceMap sync.Map   // map[string]*service
	reqLock    sync.Mutex // protects freeReq
	freeReq    *Request
	respLock   sync.Mutex // protects freeResp
	freeResp   *Response
}


func HandleHTTP() {
   DefaultServer.HandleHTTP(DefaultRPCPath, DefaultDebugPath)
}


const (
	// Defaults used by HandleHTTP
	DefaultRPCPath   = "/_goRPC_"
	DefaultDebugPath = "/debug/rpc"
)


func (server *Server) HandleHTTP(rpcPath, debugPath string) {
	http.Handle(rpcPath, server)    // 
	http.Handle(debugPath, debugHTTP{server})
}

可以看到在HandleHTTP函数中完成了http协议的处理方法的注册,而http请求处理函数的注册方式有两种:

http.Handle("/myrpc",http.HandlerFunc(customFunc))  // 方法1
http.HandleFunc("/myrpc",customFunc)			   // 方法2

其中,方法2接收一个类型为func(w http.ResponseWriter,r *http.Request)类型的处理函数,而方法一需要接收一个实现下述接口的实例:

type Handler interface {
   ServeHTTP(ResponseWriter, *Request)
}

那就看看rpc server是如何实现这个ServeHTTP方法的。

// rpc/server.go

func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if req.Method != "CONNECT" {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusMethodNotAllowed)
		io.WriteString(w, "405 must CONNECT\n")
		return
	}
	conn, _, err := w.(http.Hijacker).Hijack()     //利用Hijack()函数将HTTP的TCP连接取出
	if err != nil {
		log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error())
		return
	}
    // 打印响应头 CONNECT /_goRPC_ HTTP/1.0
	io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n") 
	server.ServeConn(conn)
}

继续看server.ServeConn是怎么处理的:

其实走到这里已经跟HTTP没有什么关系了,处理流程都是拿tcp的conn来解析数据并处理,然后返回给客户端

func (server *Server) ServeConn(conn io.ReadWriteCloser) {
	buf := bufio.NewWriter(conn)
	srv := &gobServerCodec{
		rwc:    conn,
		dec:    gob.NewDecoder(conn),  //gob的解码器
		enc:    gob.NewEncoder(buf),   //gob的编码器
		encBuf: buf,
	}
	server.ServeCodec(srv)
}

func (server *Server) ServeCodec(codec ServerCodec) {
	sending := new(sync.Mutex)
	wg := new(sync.WaitGroup)
	for {
        //解码,得到请求的service、方法、参数等信息
		service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec)
		if err != nil {
			if debugLog && err != io.EOF {
				log.Println("rpc:", err)
			}
			if !keepReading {
				break
			}
			// send a response if we actually managed to read a header.
			if req != nil {
				server.sendResponse(sending, req, invalidRequest, codec, err.Error())
				server.freeRequest(req)
			}
			continue
		}
		wg.Add(1)
        //反射执行service里对应的方法,得到结果后再编码数据并返回
		go service.call(server, sending, wg, mtype, req, argv, replyv, codec)
	}
	// We've seen that there are no more requests.
	// Wait for responses to be sent before closing codec.
	wg.Wait()
	codec.Close()
}

3. 服务注册

服务注册本来是需要一个独立的注册中心的,类似zk,但在net/rpc包中并不存在这样一个角色,目前的做法是在服务端注册一个服务列表功能,并在客户端包中做调用该功能的相关封装。因此客户端和服务端都要维护请求和响应相关的结构体信息。说白了就是客户端发送请求是需要按照一定的格式发给服务端,服务端会拿自己的格式来处理这些请求,处理完后也要按一定的格式返回给客户端,客户端会按期望的格式获取结果。其原理有点像GRPC的protocolbuf。

rpc.register函数传递一个带有相应方法的实例指针,根据源码注释的内容,方法必需遵循一系列格式要求:

  • the method's type is exported. 方法所属类型必须是导出类型(首字母大写)

  • the method is exported. 方法本身也必须是被导出的(首字母大写)

  • the method has two arguments, both exported (or builtin) types. 方法接收两个被导出类型或内置类型的参数

  • the method's second argument is a pointer. 方法的第二个参数必需是指针

  • the method has return type error. 方法的返回值必须是一个error类型的量

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

    func (server *Server) Register(rcvr interface{}) error { //接收一个空接口
    return server.register(rcvr, "", false)
    }

    func (server *Server) register(rcvr interface{}, name string, useName bool) error {
    s := new(service) //新建服务实例

      s.typ = reflect.TypeOf(rcvr)     //服务结构类型
      s.rcvr = reflect.ValueOf(rcvr)     //服务结构reflect.Value值
      sname := reflect.Indirect(s.rcvr).Type().Name()
      if useName {
      	sname = name
      }
      if sname == "" {
      	s := "rpc.Register: no service name for type " + s.typ.String()
      	log.Print(s)
      	return errors.New(s)
      }
      if !token.IsExported(sname) && !useName {
      	s := "rpc.Register: type " + sname + " is not exported"
      	log.Print(s)
      	return errors.New(s)
      }
      s.name = sname
    
      // Install the methods
      // 对传入量所包含的方法进行reflect处理,生成map[string]*methodType型的注册方法字典
      s.method = suitableMethods(s.typ, true)
      if len(s.method) == 0 {
      	str := ""
    
      	// To help the user, see if a pointer receiver would work.
      	method := suitableMethods(reflect.PtrTo(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 sync.Map)
      if _, dup := server.serviceMap.LoadOrStore(sname, s); dup {
      	return errors.New("rpc: service already defined: " + sname)
      }
      return nil
    

    }

其中关键的service和method结构体内容如下,分别代表了一个对外服务和服务中包含的一个方法。

type service struct {
	name   string                 // name of service服务名称
	rcvr   reflect.Value          // receiver of methods for the service服务方法的接收者
	typ    reflect.Type           // type of the receiver接收者类型
	method map[string]*methodType // registered methods服务中注册的方法
}


type methodType struct {  
	sync.Mutex // protects counter方法互斥锁
	method     reflect.Method//方法结构
	ArgType    reflect.Type//方法参数类型
	ReplyType  reflect.Type//方法返回值类型
	numCalls   uint   //被调用次数

}

在这一部分,其核心是使用reflect包的反射技术解析出注册服务传入的结构,包括服务信息和服务包含的方法集信息,关键代码如下:

func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType {
	methods := make(map[string]*methodType)
	for m := 0; m < typ.NumMethod(); m++ {
		method := typ.Method(m)
		mtype := method.Type
		mname := method.Name
 
		if method.PkgPath != "" //方法必需是被导出的
                ......
		if mtype.NumIn() != 3 //方法必需包含三个输入
                ......
		argType := mtype.In(1)  //第一个参数必须是被导出的或内置类型
		if !isExportedOrBuiltinType(argType) 
                ......
		replyType := mtype.In(2)
		if replyType.Kind() != reflect.Ptr  //第二个参数必需是一个指针型
                ......
		if !isExportedOrBuiltinType(replyType) //第二个参数必需是被导出的
                ......
		if mtype.NumOut() != 1 //必需有一个输出
                ......
		if returnType := mtype.Out(0); returnType != typeOfError //输出类型为error
                ......
                //将方法存储在方法集中
		methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType}
	}
	return methods

}

可以看到,在注册部分使用了大量的反射技术来获取服务和方法信息,最终也是以reflect包中定义的结构进行存储,因此需要掌握reflect包的使用方法才能理解这部分内容 。

4. 客户端

向服务端进行客户端连接后,调用call()函数,传递服务函数名(类型名.函数名)和调用函数参数,完成服务调用。

package main
import(
    "../rpc_objects"
    "fmt"
    "log"
    "net/rpc"
)
const serverAddress="localhost"
func main(){
	client,err:=rpc.DialHTTP("tcp",serverAddress+":1234")   //通过http协议连接服务器
	if err!=nil{
		log.Fatal(err)
	}
	args:=&rpc_objects.Args{13,4}
	var reply int
	err=client.Call("Args.Multiply",args,&reply)   //进行服务调用
	if err!=nil{
		log.Fatal("Args error",err)
	}
	fmt.Println(reply)
}

看看rpc.DialHTTP方法:

func DialHTTP(network, address string) (*Client, error) {
	return DialHTTPPath(network, address, DefaultRPCPath)
}

func DialHTTPPath(network, address, path string) (*Client, error) {
	var err error
	conn, err := net.Dial(network, address)   //向目标服务器建立TCP连接
	if err != nil {
		return nil, err
	}
	io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n")    //向目标服务器建立HTTP连接,请求路径为/_goRPC_,请求方法为CONNECT,CONNECT方法是需要使用tcp去直接连接的,所以不适合在网页开发中使用

    //在转向RPC高层处理前,以CONNECT方式尝试进行成功连接
	resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
	if err == nil && resp.Status == connected {
		return NewClient(conn), nil      //使用已建立的conn连接创建新Client实例
	}
	if err == nil {
		err = errors.New("unexpected HTTP response: " + resp.Status)
	}
	conn.Close()
	return nil, &net.OpError{
		Op:   "dial-http",
		Net:  network + " " + address,
		Addr: nil,
		Err:  err,
	}
}


func NewClient(conn io.ReadWriteCloser) *Client {
	encBuf := bufio.NewWriter(conn)
	client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}
	return NewClientWithCodec(client)
}


func NewClientWithCodec(codec ClientCodec) *Client {
	client := &Client{
		codec:   codec,    //gob编码解码器
		pending: make(map[uint64]*Call),   //rpc请求等待区
	}
	go client.input()    //启动input()协程处理接收内容
	return client

其中input方法用来接收服务端的响应数据,核心逻辑是解码并填充Reply等字段

5. 总结

  • net/rpc包在底层的HTTP连接阶段通过统一的HTTP访问路径和统一的method(CONNECT)完成了RPC握手。
    具体流程是客户端发送一个“CONNECT /goRPC HTTP/1.0”的HTTP报文,只是没有header和body部分,服务端会用标准的http服务来处理,只是处理时,只返回“ CONNECT /goRPC HTTP/1.0”的响应。

  • 在建立连接和完成握手之后,Client和Server端均通过操作TCP连接实例conn进行通信。

  • 客户端是采用gob进行编码的,通过捕获客户端conn.Read()信息,
    ServiceMethodt��
    Seq�� Arith.Add&��
    ArithRequest��AB��
    发现确实是用gob来编码的
    同理服务端也是
    ServiceMethodse��
    SeqError
    ArithResponse��Result�� Arith.Add&��

posted @ 2021-09-17 11:21  独揽风月  阅读(387)  评论(0编辑  收藏  举报