本博客由 [Pipe](https://github.com/b3log/pipe) 强力驱动

Golang RPC 实践

摘要: 总体上来说,HTTP 每次请求比较浪费资源的。虽然 HTTP 也是走在 TCP 上面的,但是 HTTP 请求自己添加了很多自己的信息,因此会消耗带宽资源。所以一些公司就是用 RPC 作为内部应用的通信协议。

如果你对 Go 感兴趣, 可以关注我的公众号: GoGuider

RPC

RPC(Remote Procedure Call,远程过程调用)是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络细节的应用程序通信协议。RPC 协议构建于 TCP 或 UDP,或者是 HTTP 上。

在 Go 中,标准库提供的 net/rpc 包实现了 RPC 协议需要的相关细节,开发者可以很方便的使用该包编写 RPC 的服务端和客户端程序。

从上图看, RPC 本身就是一个 client-server 模型。

下面列举一个实例代码, 来了解 RPC 调用过程

server.go

package main

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

type Args struct {
	A, B int
}

type Math int

//计算乘积
func (t *Math) Multiply(args *Args, reply *int) error {
	time.Sleep(time.Second * 3) //睡1秒,同步调用会等待,异步会先往下执行
	*reply = args.A * args.B
	fmt.Println("Multiply")
	return nil
}

//计算和
func (t *Math) Sum(args *Args, reply *int) error {
	time.Sleep(time.Second * 3)
	*reply = args.A + args.B
	fmt.Println("Sum")
	return nil
}

func main() {
	//创建对象
	math := new(Math)
	//rpc服务注册了一个Math对象 公开方法供客户端调用
	rpc.Register(math)
	//指定rpc的传输协议 这里采用http协议作为rpc调用的载体 也可以用rpc.ServeConn处理单个连接请求
	rpc.HandleHTTP()
	l, e := net.Listen("tcp", ":1234")
	if e != nil {
		log.Fatal("listen error", e)
	}
	go http.Serve(l, nil)
	os.Stdin.Read(make([]byte, 1))
}

client.go

package main

import (
	"fmt"
	"log"
	"net/rpc"
	"time"
)

type Args struct {
	A, B int
}

func main() {
	//调用rpc服务端提供的方法之前,先与rpc服务端建立连接
	client, err := rpc.DialHTTP("tcp", "127.0.0.1:1234")
	if err != nil {
		log.Fatal("dialHttp error", err)
		return
	}
	//同步调用服务端提供的方法

	args := &Args{7, 8}
	var reply int
	//可以查看源码 其实Call同步调用是用异步调用实现的。后续再详细学习
	err = client.Call("Math.Multiply", args, &reply) //这里会阻塞三秒

	if err != nil {
		log.Fatal("call Math.Multiply error", err)
	}
	fmt.Printf("Multiply:%d*%d=%d\n", args.A, args.B, reply)

	//异步调用

	var sum int

	divCall := client.Go("Math.Sum", args, &sum, nil)

	//使用select模型监听通道有数据时执行,否则执行后续程序
	for {
		select {
		case <-divCall.Done:
			fmt.Printf("%d+%d是%d, 退出执行!", args.A, args.B, sum)
			return
		default:
			fmt.Println("继续等待....")
			time.Sleep(time.Second * 1)
		}
	}
}

运行命令

go run server.go

go run client.go

运行结果

Multiply:7*8=56
继续等待....
继续等待....
继续等待....
7+8=15,出执行

调用过程解析

server 端

  • rpc 服务注册了一个 Math 对象 公开方法供客户端调用
  • 采用 http 协议作为 rpc 调用的载体, 处理请求

client 端

  • 调用 rpc 服务端提供的方法之前,先与 rpc 服务端建立连接
  • 使用 Call 方法调用远程方法

延伸

其实细心的朋友会注意到 client.go 里面有 client.Call 和 client.Go 调用;

查看源码可以看到 client.Call 底层就是调用的 client.Go

// 部分源码:

/ Go invokes the function asynchronously. It returns the Call structure representing
// the invocation. The done channel will signal when the call is complete by returning
// the same Call object. If done is nil, Go will allocate a new channel.
// If non-nil, done must be buffered or Go will deliberately crash.
func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
	call := new(Call)
	call.ServiceMethod = serviceMethod
	call.Args = args
	call.Reply = reply
	if done == nil {
		done = make(chan *Call, 10) // buffered.
	} else {
		// If caller passes done != nil, it must arrange that
		// done has enough buffer for the number of simultaneous
		// RPCs that will be using that channel. If the channel
		// is totally unbuffered, it's best not to run at all.
		if cap(done) == 0 {
			log.Panic("rpc: done channel is unbuffered")
		}
	}
	call.Done = done
	client.send(call)
	return call
}

// Call invokes the named function, waits for it to complete, and returns its error status.
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {
	call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
	return call.Error
}

参考文章

如果你对 Go 也感兴趣, 可以关注我的公众号

留下你的脚步