go官方RPC库
go官方rpc库:net/rpc
包rpc提供了通过网络访问一个对象的输出方法的能力。
服务器需要注册对象, 通过对象的类型名暴露这个服务。注册后这个对象的输出方法就可以远程调用,这个库封装了底层传输的细节,包括序列化(默认GOB序列化器)。
服务器可以注册多个不同类型的对象,但是注册相同类型的多个对象的时候会出错。
同时,如果对象的方法要能远程访问,它们必须满足一定的条件,否则这个对象的方法会被忽略。
这些条件是:
- 方法的类型是可输出的
- 方法本身也是可输出的
- 方法必须有两个参数,必须是输出类型或者是内建类型
- 方法的第二个参数必须是指针类型
- 方法返回类型为error
所以一个输出方法的格式如下:
func (t *T) MethodName(argType T1, replyType *T2) error
这里的T、T1、T2能够被encoding/gob序列化,即使使用其它的序列化框架,将来的这个需求可能会被弱化,
这个方法的第一个参数代表调用者(client)提供的参数,第二个参数代表要返回给调用者的计算结果。
如果返回error,则reply参数不会返回给调用者。
服务器通过调用ServeConn在一个连接上处理请求,更典型地, 它可以创建一个network listener然后accept请求。
对于HTTP listener来说,可以调用 HandleHTTP 和 http.Serve。细节会在下面介绍。
客户端可以调用Dial和DialHTTP建立连接。 客户端有两个方法调用服务: Call 和 Go,可以同步地或者异步地调用服务。
当然,调用的时候,需要把服务名、方法名和参数传递给服务器。异步方法调用Go 通过 Done channel通知调用结果返回。
除非显示的设置codec,否则这个库默认使用包encoding/gob作为序列化框架。
简单例子
首选介绍一个简单的例子, 这个例子摘自官方标准库,是一个非常简单的服务。
这个例子中提供了对两个数相乘和相除的两个方法。
第一步你需要定义传入参数和返回参数的数据结构:
package server
type Args struct {
	A, B int
}
type Quotient struct {
	Quo, Rem int
}
第二步定义一个服务对象,这个服务对象可以很简单, 比如类型是int或者是interface{},重要的是它输出的方法。
这里我们定义一个算术类型Arith,其实它是一个int类型,但是这个int的值我们在后面方法的实现中也没用到,所以它基本上就起一个辅助的作用。
type Arith int
第三步实现这个类型的两个方法乘法和除法
func (a *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return nil
}
func (a *Arith) Divide(args *Args, quo *Quotient) error {
	if args.B == 0 {
		return errors.New("divide by zero")
	}
	quo.Quo = args.A / args.B
	quo.Rem = args.A % args.B
	return nil
}
目前为止,我们的准备工作已经完成,喝口茶继续下面的步骤。
第四步实现RPC服务器
func main() {
	// 实例化一个算数服务
	arith := new(Arith)
	// 注册服务
	_ = rpc.Register(arith)
	rpc.HandleHTTP()
	l, e := net.Listen("tcp", ":8000")
	if e != nil {
		log.Fatalln("net.Listen Fail", e)
	}
	go func() {
		_ = http.Serve(l, nil)
	}()
	select {
	}
}
这里我们生成了一个Arith对象,并使用rpc.Register注册这个服务,然后通过HTTP暴露出来。
客户端可以看到服务Arith以及它的两个方法Arith.Multiply和Arith.Divide。
第五步创建一个客户端,建立客户端和服务器端的连接:
func main() {
	client, err := rpc.DialHTTP("tcp", ":8000")
	if err != nil {
		log.Fatalln("rpc.DialHTTP Fail:", err)
	}
}
然后客户端就可以进行远程调用了,比如同步的方式调用:
	args := &Args{A: 9, B: 2}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Println("client.Call 【Arith.Multiply fail】:", err)
	}
	fmt.Printf("Arith:%d*%d=%d", args.A, args.B, reply)
或者异步的方式:
	args := &Args{A: 9, B: 2}
	reply := new(Quotient)
	multiplyDone := client.Go("Arith.Divide", args, reply, nil)
	replyCall := <- multiplyDone.Done
	replay := replyCall.Reply.(*Quotient)
	fmt.Printf("Arith:%d/%d =%d,%d\n", args.A, args.B, replay.Quo, replay.Rem)
服务器代码分析
首先, net/rpc定义了一个缺省的Server,所以Server的很多方法你可以直接调用,这对于一个简单的Server的实现更方便,但是你如果需要配置不同的Server,
比如不同的监听地址或端口,就需要自己生成Server:
var DefaultServer = NewServer()
Server有多种Socket监听的方式:
    func (server *Server) Accept(lis net.Listener)
    func (server *Server) HandleHTTP(rpcPath, debugPath string)
    func (server *Server) ServeCodec(codec ServerCodec)
    func (server *Server) ServeConn(conn io.ReadWriteCloser)
    func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request)
    func (server *Server) ServeRequest(codec ServerCodec) error
其中, ServeHTTP 实现了处理 http请求的业务逻辑, 它首先处理http的 CONNECT请求, 接收后就Hijacker这个连接conn, 然后调用ServeConn在这个连接上 处理这个客户端的请求。
它其实是实现了 http.Handler接口,我们一般不直接调用这个方法。
`Server.HandleHTTP`设置rpc的上下文路径,`rpc.HandleHTTP`使用默认的上下文路径`DefaultRPCPath`、 DefaultDebugPath。
这样,当你启动一个http server的时候 `http.ListenAndServe`,上面设置的上下文将用作RPC传输,这个上下文的请求会教给ServeHTTP来处理。
以上是RPC over http的实现,可以看出net/rpc只是利用http CONNECT建立链接,这和普通的 RESTful api还是不一样的。
Accept用来处理一个监听器,一直在监听客户端链接,一旦监听器收到了一个连接,则还是交给ServeConn交给另外一个goroutine去处理:
func (server *Server) Accept(lis net.Listener) {
	for {
		conn, err := lis.Accept()
		if err != nil {
			log.Print("rpc.Serve: accept:", err.Error())
			return
		}
		go server.ServeConn(conn)
	}
}
可以看出很重的一个方法是ServeConn:
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
	buf := bufio.NewWriter(conn)
	srv := &gobServerCodec{
		rwc:    conn,
		dec:    gob.NewDecoder(conn),
		enc:    gob.NewEncoder(buf),
		encBuf: buf,
	}
	server.ServeCodec(srv)
}
链接其实是交给一个ServeCodec去处理,这里默认使用gobServerCodec去处理,这是一个默认的编解码器,你也可以使用其它的编解码器,我们下面在介绍,这里我们看看ServeCodec是怎么实现的:
// ServeCodec is like ServeConn but uses the specified codec to
// decode requests and encode responses.
func (server *Server) ServeCodec(codec ServerCodec) {
	sending := new(sync.Mutex)
	wg := new(sync.WaitGroup)
	for {
		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)
		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()
}
它其实一直从连接中读取请求,然后调用go service.call,在另外的goroutine中处理服务调用。
我们从中可以学到
- 对象重用。 Request和Response都是可重用的,通过Lock处理竞争。这在大并发的情况下很有效。 有兴趣的读者可以参考fasthttp的实现。
- 使用了大量的goroutine。 和Java中的线程不同,你可以创建非常多的goroutine, 并发处理非常好。 如果使用一定数量的goutine作为worker池去处理这个case,可能还会有些性能的提升,但是更复杂了。使用goroutine已经获得了非常好的性能。
- 业务处理是异步的, 服务的执行不会阻塞其它消息的读取。
注意一个codec实例必然和一个connnection相关,因为它需要从connection中读取request和发送response。
go的rpc官方库的消息(request和response)的定义很简单, 就是消息头(header)+内容体(body)。
请求的消息头的定义如下,包括服务的名称和序列号:
type Request struct {
	ServiceMethod string   // format: "Service.Method"
	Seq           uint64   // sequence number chosen by client
	next          *Request // for free list in Server
}
消息体就是传入的参数。
返回的消息头的定义如下:
type Response struct {
	ServiceMethod string    // echoes that of the Request
	Seq           uint64    // echoes that of the request
	Error         string    // error, if any.
	next          *Response // for free list in Server
}
消息体是reply类型的序列化后的值。
Server还提供了两个注册服务的方法:
// Register publishes the receiver's methods in the DefaultServer.
func Register(rcvr any) error { return DefaultServer.Register(rcvr) }
// RegisterName is like Register but uses the provided name for the type
// instead of the receiver's concrete type.
func RegisterName(name string, rcvr any) error {
	return DefaultServer.RegisterName(name, rcvr)
}
第二个方法为服务起一个别名,否则服务名已它的类型命名,它们俩底层调用register进行服务的注册。
func (server *Server) register(rcvr any, name string, useName bool) error
受限于Go语言的特点, 我们不可能在接到客户端的请求的时候,根据反射动态的创建一个对象,就是Java那样,
因此在Go语言中,我们需要预先创建一个服务map这是在编译的时候完成的:
// serviceMap sync.Map   // map[string]*service
server.serviceMap = make(map[string]*service)
同时每个服务还有一个方法map: map[string]*methodType,通过suitableMethods建立:
func suitableMethods(typ reflect.Type, logErr bool) map[string]*methodType
这样rpc在读取请求header,通过查找这两个map,就可以得到要调用的服务及它对应的方法了,方法的调用:
func (s *service) call(server *Server, sending *sync.Mutex, wg *sync.WaitGroup, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) {
	if wg != nil {
		defer wg.Done()
	}
	mtype.Lock()
	mtype.numCalls++
	mtype.Unlock()
	function := mtype.method.Func
	// Invoke the method, providing a new value for the reply.
	returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})
	// The return value for the method is an error.
	errInter := returnValues[0].Interface()
	errmsg := ""
	if errInter != nil {
		errmsg = errInter.(error).Error()
	}
	server.sendResponse(sending, req, replyv.Interface(), codec, errmsg)
	server.freeRequest(req)
}
客户端源码分析
客户端要建立和服务器的连接,可以有以下几种方式:
    func Dial(network, address string) (*Client, error)
    func DialHTTP(network, address string) (*Client, error)
    func DialHTTPPath(network, address, path string) (*Client, error)
    func NewClient(conn io.ReadWriteCloser) *Client
    func NewClientWithCodec(codec ClientCodec) *Client
DialHTTP 和 DialHTTPPath是通过HTTP的方式和服务器建立连接,他俩的区别之在于是否设置上下文路径:
// DialHTTPPath connects to an HTTP RPC server
// at the specified network address and path.
func DialHTTPPath(network, address, path string) (*Client, error) {
	conn, err := net.Dial(network, address)
	if err != nil {
		return nil, err
	}
	io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n")
	// Require successful HTTP response
	// before switching to RPC protocol.
	resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
	if err == nil && resp.Status == connected {
		return NewClient(conn), nil
	}
	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,
	}
}
首先发送 CONNECT 请求,如果连接成功则通过NewClient(conn)创建client。
而Dial则通过TCP直接连接服务器:
func Dial(network, address string) (*Client, error) {
	conn, err := net.Dial(network, address)
	if err != nil {
		return nil, err
	}
	return NewClient(conn), nil
}
根据服务是over HTTP还是 over TCP选择合适的连接方式。
NewClient则创建一个缺省codec为gob序列化库的客户端:
func NewClient(conn io.ReadWriteCloser) *Client {
	encBuf := bufio.NewWriter(conn)
	client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}
	return NewClientWithCodec(client)
}
如果你想用其它的序列化库,你可以调用NewClientWithCodec方法<:></:>
func NewClientWithCodec(codec ClientCodec) *Client {
	client := &Client{
		codec:   codec,
		pending: make(map[uint64]*Call),
	}
	go client.input()
	return client
}
重要的是input方法,它以一个死循环的方式不断地从连接中读取response,然后调用map中读取等待的Call.Done channel通知完成。
消息的结构和服务器一致,都是Header+Body的方式。
客户端的调用有两个方法: Go 和 Call。 Go方法是异步的,它返回一个 Call指针对象, 它的Done是一个channel,如果服务返回,
Done就可以得到返回的对象(实际是Call对象,包含Reply和error信息)。 Call是同步的方式调用,它实际是调用Go实现的,
我们可以看看它是怎么实现的,可以了解一下异步变同步的方式:
// Call invokes the named function, waits for it to complete, and returns its error status.
func (client *Client) Call(serviceMethod string, args any, reply any) error {
	call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
	return call.Error
}
从一个Channel中读取对象会被阻塞住,直到有对象可以读取,这种实现很简单,也很方便。
其实从服务器端的代码和客户端的代码实现我们还可以学到锁Lock的一种实用方式,也就是尽快的释放锁,而不是defer mu.Unlock直到函数执行到最后才释放,那样锁占用的时间太长了。
codec/序列化框架
前面我们介绍了rpc框架默认使用gob序列化库,很多情况下我们追求更好的效率的情况下,或者追求更通用的序列化格式,我们可能采用其它的序列化方式, 比如protobuf, json, xml等。
gob序列化库有个要求,就是对于接口类型的值,你需要注册具体的实现类型:
func Register(value interface{})
func RegisterName(name string, value interface{})
初次使用rpc的人容易犯这个错误,导致序列化不成功。
Go官方库实现了JSON-RPC 1.0。JSON-RPC是一个通过JSON格式进行消息传输的RPC规范,因此可以进行跨语言的调用。
Go的net/rpc/jsonrpc库可以将JSON-RPC的请求转换成自己内部的格式,比如request header的处理:
 
                    
                
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号