Go语言之RPC入门

RPC是什么

RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,用于使一个计算机程序能够调用远程计算机上的服务或函数,就像调用本地函数一样。它允许不同的进程或计算机之间进行通信和交互,将远程计算机上的函数调用封装为本地函数调用。

通过使用 RPC,开发人员可以将分布式系统中的不同组件连接在一起,使它们能够透明地进行通信和交互。RPC 隐藏了网络通信的复杂性,使开发人员能够专注于业务逻辑的实现,而不必过多关注底层通信细节。

RPC 可以使用不同的传输协议和序列化机制来实现,例如基于 HTTP 的 RESTful API、基于 TCP 的 gRPC、基于消息队列的 AMQP 等。每种实现方式都有其特点和适用场景,开发人员可以根据具体需求选择合适的 RPC 实现。

RPC简单例子

Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。下面我们将尝试基于rpc实现一个类似的例子。

我们先创建一个服务端,创建一个UserService类型,和一个Say()方法:

golang 复制代码
type UserService struct {
}

func (s *UserService) Say(request string, reply *string) error {
	*reply = "hi," + request
	return nil
}

其中Say()方法必须满足Go语言的RPC规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。

然后就可以将HelloService类型的对象注册为一个RPC服务:

golang 复制代码
func main() {
	var err error
	err = rpc.RegisterName("UserService", new(UserService))
	if err != nil {
		panic(err)
	}

	listener, err := net.Listen("tcp", ":8011")
	if err != nil {
		panic(err)
	}

	conn, err := listener.Accept()
	if err != nil {
		panic(err)
	}

	rpc.ServeConn(conn)
}

其中rpc.RegisterName()函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在HelloService服务的空间之下。然后建立一个唯一的TCP链接,并且通过rpc.ServeConn()函数在该TCP链接上为对方提供RPC服务。

下面是Client端代码:

golang 复制代码
func main() {
	client, err := rpc.Dial("tcp", "localhost:8011")
	if err != nil {
		panic(err)
	}

	var reply string
	err = client.Call("UserService.Say", "hi", &reply)
	if err != nil {
		panic(err)
	}
	fmt.Println(reply)
}

更合理的RPC代码

在涉及RPC的应用中,作为开发人员一般至少有3种角色:首先是服务器端实现RPC方法的开发人员,其次是客户端调用RPC方法的人员,最后也是最重要的是制定服务器端和客户端RPC接口规范的设计人员。在前面的例子中,为了简化我们将以上几种角色的工作全部放到了一起,虽然看似实现简单,但是不利于后期的维护和工作的切割。

重构服务代码

go 复制代码
const UserServiceName = "path/user.UserService"

type UserServiceInterface interface {
	Say(request string, reply *string) error
}

func RegisterUserService(s UserServiceInterface) error {
	return rpc.RegisterName(UserServiceName, s)
}

我们将RPC服务的接口规范分为3部分:首先是服务的名字,然后是服务要实现的详细方法列表,最后是注册该类型服务的函数。为了避免名字冲突,我们在RPC服务的名字中增加了包路径前缀(这个是RPC服务抽象的包路径,并非完全等价于Go语言的包路径)。RegisterUserService注册服务时,编译器会要求传入的对象满足UserServiceInterface接口。

服务端代码:

golang 复制代码
type UserService struct {
}

func (s *UserService) Say(request string, reply *string) error {
	*reply = "hi," + request
	return nil
}

func main() {
	err := RegisterUserService(new(UserService))
	if err != nil {
		panic(err)
	}
	listener, err := net.Listen("tcp", ":8012")
	if err != nil {
		panic(err)
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal(err)
		}

		go rpc.ServeConn(conn)
	}

}

在新的RPC服务器端实现中,我们用RegisterUserService()函数来注册函数,这样不仅可以避免命名服务名称的工作,同时也保证了传入的服务对象满足RPC接口的定义。最后新的服务改为支持多个TCP链接,然后为每个TCP链接提供RPC服务。

重构客户端代码

golang 复制代码
type UserClient struct {
	*rpc.Client
}

var _ UserServiceInterface = (*UserClient)(nil)

func GetUserClient(network, address string) (*UserClient, error) {
	c, err := rpc.Dial(network, address)
	if err != nil {
		return nil, err
	}
	return &UserClient{Client: c}, nil
}
func (p *UserClient) Say(request string, reply *string) error {
	return p.Client.Call(UserServiceName+".Say", request, reply)
}

我们在接口规范中针对客户端新增加了UserClient类型,该类型也必须满足UserServiceInterface接口,这样客户端用户就可以直接通过接口对应的方法调用RPC函数。同时提供了一个GetUserClient函数,直接拨号UserService服务。

客户端代码:

golang 复制代码
func main() {
	client, err := GetUserClient("tcp", "localhost:8012")
	if err != nil {
		panic(err)
	}

	var replay string
	err = client.Say("hi", &replay)
	if err != nil {
		panic(err)
	}
	fmt.Println(replay)
}

直接用封装好的GetUserClient获取client,然后用client中的Say方法调用RPC。

RPC跨语言

标准库的RPC默认采用Go语言特有的Gob编码,因此从其他语言调用Go语言实现的RPC服务将比较困难。在互联网的微服务时代,每个RPC以及服务的使用者都可能采用不同的编程语言,因此跨语言是互联网时代RPC的一个首要条件。得益于RPC的框架设计,Go语言的RPC其实也是很容易实现跨语言支持的。

Go语言的RPC框架有两个比较有特色的设计:一个是RPC数据打包时可以通过插件实现自定义的编码和解码;另一个是RPC建立在抽象的io.ReadWriteCloser接口之上,我们可以将RPC架设在不同的通信协议之上。这里我们将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的RPC。

基于json的RPC服务:

golang 复制代码
func main() {
    rpc.RegisterName("UserService", new(UserService))
    listener, err := net.Listen("tcp", ":8013")
    if err != nil {
        log.Fatal("ListenTCP error:", err)
    }
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal("Accept error:", err)
        }
        go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}

代码中最大的变化是用rpc.ServeCodec()函数替代了rpc.ServeConn()函数,传入的参数是针对服务器端的JSON编解码器。

客户端:

golang 复制代码
func main() {
    conn, err := net.Dial("tcp", "localhost:8013")
    if err != nil {
        log.Fatal("net.Dial:", err)
    }
    client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
    var reply string
    err = client.Call("UserService.Say", "hi", &reply)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(reply)
}

在确保客户端可以正常调用RPC服务的方法之后,我们用一个普通的TCP服务代替Go语言版本的RPC服务,这样可以查看客户端调用时发送的数据格式。例如,通过命令nc -l 8013在同样的端口启动一个TCP服务。然后再次执行一次RPC调用将会发现nc输出了以下的信息:

json 复制代码
{"method":"UserService.Say","params":["hi"],"id":0}

这是一个JSON编码的数据,其中method部分对应要调用的由RPC服务和方法组合成的名字,params部分的第一个元素为参数,id是由调用方维护的唯一的调用编号。

请求的JSON数据对象在内部对应两个结构体:客户端是clientRequest,服务器端是serverRequest。clientRequest和serverRequest结构体的内容基本是一致的:

golang 复制代码
type clientRequest struct {
    Method string         `json:"method"`
    Params [1]interface{} `json:"params"`
    Id     uint64         `json:"id"`
}

type serverRequest struct {
    Method string           `json:"method"`
    Params *json.RawMessage `json:"params"`
    Id     *json.RawMessage `json:"id"`
}

在获取到RPC调用对应的JSON数据后,可以通过直接向架设了RPC服务的TCP服务器发送JSON数据模拟RPC方法调用:

json 复制代码
$ echo -e '{"method":"UserService.Say","params":["hi"],"id":1}' | nc localhost 8013

返回的结果也是JSON格式的数据:

json 复制代码
{"id":1,"result":"hi,hi","error":null}

其中id对应输入的id参数,result为返回的结果,error部分在出问题时表示错误信息。对顺序调用来说,id不是必需的。但是Go语言的RPC框架支持异步调用,当返回结果的顺序和调用的顺序不一致时,可以通过id来识别对应的调用。

返回的JSON数据也对应内部的两个结构体:客户端是clientResponse,服务器端是serverResponse。两个结构体的内容同样也是类似的:

golang 复制代码
type clientResponse struct {
    Id     uint64           `json:"id"`
    Result *json.RawMessage `json:"result"`
    Error  interface{}      `json:"error"`
}
type serverResponse struct {
    Id     *json.RawMessage `json:"id"`
    Result interface{}      `json:"result"`
    Error  interface{}      `json:"error"`
}

因此无论采用何种语言,只要遵循同样的JSON结构,以同样的流程就可以和Go语言编写的RPC服务进行通信。这样就实现了跨语言的RPC。

相关推荐
追逐时光者1 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~1 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581361 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳2 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾2 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
星就前端叭3 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
小林coding4 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
AI理性派思考者4 小时前
【保姆教程】手把手教你在Linux系统搭建早期alpha项目cysic的验证者&证明者
后端·github·gpu
从善若水4 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust