首页 > Go语言 > Go语言网络编程 阅读:3,030

Go语言RPC协议:远程过程调用

RPC 协议构建于 TCP、UDP 或者是 HTTP 之上,允许开发人员直接调用另一台计算机上的程序,而开发人员无需额外地为这个调用过程编写网络通信相关代码,使得开发网络分布式类型的应用程序更加容易。

Go语言的标准库提供了 RPC 框架和不同的 RPC 实现。

什么是 RPC

远程过程调用(Remote Procedure Call,简称 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而开发人员无需额外地为这个交互作用编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用。

通俗的来讲就是,RPC 允许跨机器、跨语言调用计算机程序。例如我们用Go语言写了一个获取用户信息的方法 getUserInfo,并把Go语言程序部署在阿里云服务器上面,另外我们还有一个部署在腾讯云上面的 php 项目,需要调用Go语言的 getUserInfo 方法获取用户信息,php 跨机器调用 Go 方法的过程就是 RPC 调用。

RPC 的工作流程如下图所示:

远程过程调用
图:远程过程调用流程图

流程说明如下:
  • (1) 调用客户端句柄,执行传送参数;
  • (2) 调用本地系统内核发送网络消息;
  • (3) 消息传送到远程主机;
  • (4) 服务器句柄得到消息并取得参数;
  • (5) 执行远程过程;
  • (6) 执行的过程将结果返回服务器句柄;
  • (7) 服务器句柄返回结果,调用远程系统内核;
  • (8) 消息传回本地主机;
  • (9) 客户句柄由内核接收消息;
  • (10) 客户接收句柄返回的数据。

Go语言中如何实现 RPC 的

在Go语言中实现 RPC 非常简单,有封装好的官方包和一些第三方包提供支持。Go语言中 RPC 可以利用 tcp 或 http 来传递数据,可以对要传递的数据使用多种类型的编解码方式。

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

此外,Go语言还提供了 net/rpc/jsonrpc 包实现 RPC 方法,JSON RPC 采用 JSON 进行数据编解码,因而支持跨语言调用。但目前的 jsonrpc 包是基于 tcp 协议实现的,暂时不支持使用 http 进行数据传输。

除了Go语言官方提供的 rpc 包,还有许多第三方包为在Go语言中实现 RPC 提供支持,大部分第三方 rpc 包的实现都是使用 protobuf 进行数据编解码,根据 protobuf 声明文件自动生成 rpc 方法定义与服务注册代码,所以在Go语言中可以很方便的进行 rpc 服务调用。

net/rpc 包

rpc 包提供了通过网络或其他 I/O 连接对一个对象的导出方法的访问。服务端注册一个对象,使它作为一个服务被暴露,服务的名字是该对象的类型名。注册之后,对象的导出方法就可以被远程访问。服务端可以注册多个不同类型的对象(服务),但注册具有相同类型的多个对象是错误的。

只有满足如下标准的方法才能用于远程访问,其余方法会被忽略:
  • 方法是可导出的;
  • 方法有两个参数,都是导出类型或内建类型;
  • 方法的第二个参数是指针类型;
  • 方法只有一个 error 接口类型的返回值。

下面的示例演示了Go语言 net/rpc 包实现 RPC 方法,使用 http 作为 RPC 的载体,通过 net/http 包监听客户端连接请求。

服务端代码如下:
package main

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

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

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

// 算数运算响应结构体
type ArithResponse struct {
    Pro int // 乘积
    Quo int // 商
    Rem int // 余数
}

// 乘法运算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
    res.Pro = req.A * req.B
    return nil
}

// 除法运算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
    if req.B == 0 {
        return errors.New("除以零")
    }
    res.Quo = req.A / req.B
    res.Rem = 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:8080")
    if err != nil {
        log.Fatalln("致命错误: ", err)
    }

    fmt.Fprintf(os.Stdout, "%s", "开始连接")

    http.Serve(lis, nil)
}
服务端程序运行之后将会监听本地的 8080 端口,下面我们再来看一下客户端程序,用于连接服务端并实现 RPC 方法调用,完整代码如下:
package main

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

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

// 算数运算响应结构体
type ArithResponse struct {
    Pro int // 乘积
    Quo int // 商
    Rem int // 余数
}

func main() {
    conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8080")
    if err != nil {
        log.Fatalln("连接错误: ", err)
    }

    req := ArithRequest{11, 2}
    var res ArithResponse

    err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
    if err != nil {
        log.Fatalln("算术误差: ", err)
    }
    fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)

    err = conn.Call("Arith.Divide", req, &res)
    if err != nil {
        log.Fatalln("算术误差: ", err)
    }
    fmt.Printf("%d / %d, 商是 %d, 余数是 %d\n", req.A, req.B, res.Quo, res.Rem)
}
运行结果如下:

11 * 2 = 22
11 / 2, 商是 5, 余数是 1

net/rpc/jsonrpc 库

上面的例子演示了使用 net/rpc 包实现 RPC 的过程,但是没办法在其他语言中调用上面例子所实现的 RPC 方法。

Go语言提供了 net/rpc/jsonrpc 包,用于提供基于 json 编码的 RPC 支持。在不指定编码协议时,默认采用 Go 特有的 gob 编码协议。但是其他语言一般不支持 Go 的 gob 协议,所以如果需要跨语言的 RPC 调用,需要采用通用的编码协议。

服务端部分的代码如下所示:
package main

import (
    "errors"
    "fmt"
    "log"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
    "os"
)

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

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

// 算数运算响应结构体
type ArithResponse struct {
    Pro int // 乘积
    Quo int // 商
    Rem int // 余数
}

// 乘法运算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
    res.Pro = req.A * req.B
    return nil
}

// 除法运算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
    if req.B == 0 {
        return errors.New("除以零")
    }
    res.Quo = req.A / req.B
    res.Rem = req.A % req.B
    return nil
}

func main() {
    rpc.Register(new(Arith)) // 注册rpc服务

    lis, err := net.Listen("tcp", "127.0.0.1:8080")
    if err != nil {
        log.Fatalln("致命错误: ", err)
    }

    fmt.Fprintf(os.Stdout, "%s", "开始连接")

    for {
        conn, err := lis.Accept() // 接收客户端连接请求
        if err != nil {
            continue
        }

        go func(conn net.Conn) { // 并发处理客户端请求
            fmt.Fprintf(os.Stdout, "%s", "新连接接入\n")
            jsonrpc.ServeConn(conn)
        }(conn)
    }
}
上述服务端程序启动后,将会监听本地的 8080 端口,并处理客户端的 tcp 连接请求。下面我们再来实现一个客户端程序来连接上述服务端并进行 RPC 调用,完整代码如下:
package main

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

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

// 算数运算响应结构体
type ArithResponse struct {
    Pro int // 乘积
    Quo int // 商
    Rem int // 余数
}

func main() {
    conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8080")
    if err != nil {
        log.Fatalln("连接错误: ", err)
    }

    req := ArithRequest{11, 3}
    var res ArithResponse

    err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
    if err != nil {
        log.Fatalln("算术误差: ", err)
    }
    fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)

    err = conn.Call("Arith.Divide", req, &res)
    if err != nil {
        log.Fatalln("算术误差: ", err)
    }
    fmt.Printf("%d / %d, 商是 %d, 余数是 %d\n", req.A, req.B, res.Quo, res.Rem)
}
运行结果如下:

11 * 3 = 33
11 / 3, 商是 3, 余数是 2

爱面试的程序媛,一个分享面试经验的公众号。跟着站长一起学习,每天都有进步。

通俗易懂,深入浅出,定时分享程序员面试的那点事。

面试如何造火箭?工作如何拧螺丝?都在这个公号哦。

扫描二维码关注公众号,免费领取价值 1000 元的求职面试资料(限时免费)!

当你决定关注「爱面试的程序媛」,你已然超越了90%的程序员!

爱面试的程序媛二维码
微信扫描二维码关注