【Golang | RPC】Golang-RPC机制的理解

环境:
Golang:go1.18.2 linux/amd64

1. RPC简介

RPC(远程过程调用,Remote Procedure Call)一般采用C/S(客户端/服务端)架构,即请求方是客户端,服务提供方是服务端。相较于本地调用,RPC往往用于微服务实例间的通信,因此会多出网络开销。
大致的流程就是先建立socket连接(也可以是http,本文采用socket连接),rpc客户端将需要调用的方法以及方法所需的参数通过网络的形式(序列化)发送给服务端;服务端接收到request后,反序列化获取方法和参数,将服务端执行得到的结果再通过网络的形式(序列化)返回给客户端,客户端进行反序列化得到想要的response

为了有直观的认识,下面是通过json将request和response进行编解码后获取的信息

// request
{"method":"QueryService.GetAge","params":["bar"],"id":0}

// response 
{"id":0,"result":"The age of bar is 20","error":null}

2. 实践

我们通过Go语言自带的包/net/rpc来实现远程调用。现有下面一种场景:服务端保存着用户的年龄信息,客户端输入姓名,经RPC后获得对应的年龄

2.1 服务端

2.1.1 首先新建项目RPC,并创建Server目录,新建main.go

[root@tudou workspace]# mkdir -p RPC/Server && cd RPC/Server && touch main.go

2.1.2 新建一个字典,保存用户信息

// 用户信息
var userinfo = map[string]int{
	"foo": 18,
	"bar": 20,
}

2.1.3 新建一个结构体Query,该结构体实现一个GetAge的方法,该方法有两个参数,一个error接口类型的返回

// 实现查询服务,结构体Query实现了GetAge方法
type Query struct {
}

func (q *Query) GetAge(req string, res *string) error {
	*res = fmt.Sprintf("The age of %s is %d", req, userinfo[req])
	return nil
}

注:

  • 方法名GetAge必须可导出,即首字母要大写
  • req 的类型必须可导出,即首字母大写或内置类型
  • res的类型也必须可导出,同时为指针类型(这点很好理解,res是指针类型就对应着指针传递,GetAge函数内修改了res的值,函数外也可以获得修改后的值;而req可以值传递也可以指针传递)

2.1.4 注册服务的方法,并开启监听,等待rpc客户端的请求

func main() {
	// 注册服务方法
	if err := rpc.RegisterName("QueryService", new(Query)); err != nil {
		log.Println(err)
	}
	// 开启监听,接受来自rpc客户端的请求
	listener, _ := net.Listen("tcp", ":1234")
	rpc.Accept(listener)
}

注:这里使用RegisterName可以指定服务的名字,也可以使用Register不指定服务名,转而直接使用结构体名,参考如下源码

func (server *Server) register(rcvr any, name string, useName bool) error {
	s := new(service)
	s.typ = reflect.TypeOf(rcvr)
	s.rcvr = reflect.ValueOf(rcvr)
	sname := reflect.Indirect(s.rcvr).Type().Name()
	if useName {
		sname = name
	}

2.1.5 运行服务

[root@tudou Server]# go build main.go && ./main

2.2 客户端

2.2.1在RPC/Server同级目录下创建Client目录,新建main.go

[root@tudou workspace]# mkdir -p RPC/Client && cd RPC/Client && touch main.go

2.2.2 建立与RPC服务端的连接
这里rpc.Dial方法其实分两步,第一通过net.Dial建立socket连接,第二将Go语言独有的gob(golang binary)作为编解码建立client

client, _ := rpc.Dial("tcp", ":1234")

可以查看如下源码

// Dial connects to an RPC server at the specified network address.
func Dial(network, address string) (*Client, error) {
	conn, err := net.Dial(network, address)
	if err != nil {
		return nil, err
	}
	return NewClient(conn), nil
}

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

2.2.3 通过Call方法进行调用
第一个参数是服务名.方法名,第二个参数是GetAge的入参,第三个参数用来保存RPC返回结果(必须是指针类型)

	var res string
	err := client.Call("QueryService.GetAge", "bar", &res)
	if err != nil {
		log.Println(err)
	}
	fmt.Println(res)

2.2.4 运行客户端,得到如下结果

[root@tudou Client]# go run main.go
The age of bar is 20

3. json序列化

本来打算用gob作编解码器显示下序列化后得到的数据,奈何笔者技术不到家,得到的结果不够清晰明了,所以下面就用json作编解码器作个演示,本质是一样的。(可以参考另一篇文章【Golang | RPC】利用json编解码器实现RPC
首先利用nc工具监听1234端口,作为服务端,获取传入的数据

[root@tudou ~]# nc -l 1234

然后运行客户端,可以发现nc工具捕获到如下数据,这就是客户端利用json编码后发送给服务端的数据

{"method":"QueryService.GetAge","params":["bar"],"id":0}

同样,nc工具可以作为客户端,向RPC服务端发送数据,得到如下返回结果

[root@tudou ~]# echo -e '{"method":"QueryService.GetAge","params":["bar"],"id":0}' | nc localhost 1234
{"id":0,"result":"The age of bar is 20","error":null}

4. 总结

  • RPC是一个C/S架构,通过指定的编解码器发送request,获取response
  • 服务的方法名必须可导出,即首字母要大写;方法的第一个参数类型必须可导出,即首字母大写或内置类型, 方法的第二个参数类型也必须可导出,同时为指针类型

完整代码:
https://github.com/WanshanTian/GolangLearning
参考 GolangLearning/RPC/gobRPC目录

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

田土豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值