RPC 原理与 Go RPC

地址:https://www.liwenzhou.com/posts/Go/rpc/

 

什么是RPC

RPC (Remote Procedure Call),远程过程调用。它允许像调用本地服务一样调用远程服务。

RPC 是一种服务器-客户端(Client/Server),经典实现是一个通过发送请求---接受回应进行信息交互的系统。

 

本地调用

复制代码
import "fmt"

func add(x, y int) int {
    return x + y
}

func main()  {
    // 调用本地函数
    a := 10
    b := 20
    ret := add(a ,b)
    fmt.Println(ret)
}
复制代码

将上述程序编译成二进制文件后运行,会输出结果是30。

在运行程序调用 add 函数执行流程,可以理解为以下四个步骤。

  1、将变量 a 和变量 b 的值分别压入堆栈上

  2、执行 add 方法,从堆栈中获取 a 和 b 的值,并将他们分别分配给 x 和  y

  3、计算 x + y 的值并将其保存到堆栈中

  4、退出 add 函数并将 x + y  的值赋给 ret

 

 

rpc 调用

本地过程调用发生在同一个进程中----定义 add 函数的代码和调用 add 函数的代码共享同一个内存空间,所以调用能正常执行。

                      

 

但是我们无法直接在另一个程序---app2 上调用 add 函数,因为它们是两个程序---内存空间是相互隔离的。(app1 和 app2 可能部署在同一台服务器上,也可能部署在不同的服务器上)。

 

RPC 就是为了解决类似远程,跨内存空间的函数/方法调用的。要实现 RPC 就需要解决以下三个问题。

1、如何确定要执行的函数?在本地调用中,函数主题通过函数指针来确定,然后调用 add 函数,编译器通过函数指针来确定函数在内存中的位置。但是在 RPC 中,调用不能通过函数指针来完成。因此,调用者和被调用者都需要维护一个 (function <-> ID )映射表,以确保调用正确的函数。

 

2、如果表达参数:参数或者返回值需要在传输期间序列化并转化为字节流。

 

3、如何进行网络传输?函数的调用方和被调用方通常是通过网络连接的,也就是说,function ID 和序列化字节流需要通过

网络传输,因此,只要能够完成传输,调用方和被调用方就不受某个网络协议的限制。例如,一些 RPC 框架使用 TCP 协议,一些使用 HTTP 。

 

HTTP 调用 RESTful API

首先,我们编写一个基于 HTTP 的 server 服务,它将接受其他程序发来的 HTTP 请求,执行特定的程序并将结果返回。

复制代码
type addParam struct {
    X int `json:"x"`
    Y int `json:"y"`
}

type addResult struct {
    Code int `json:"code"`
    Data int `json:"data"`
}

func add(x, y int) int {
    return x + y
}

func addHandler(w http.ResponseWriter, r *http.Request) {
    // 解析参数
    b, _ := ioutil.ReadAll(r.Body)
    var param addParam
    json.Unmarshal(b, &param)
    // 业务逻辑
    ret := add(param.X, param.Y)
    // 返回响应
    respBytes, _ := json.Marshal(addResult{Code: 0, Data: ret})
    w.Write(respBytes)
}

func main() {
    http.HandleFunc("/add", addHandler)
    log.Fatal(http.ListenAndServe(":9090", nil))
}
复制代码

 

然后我们写一个客户端发送上述 HTTP 服务,传递 x 和 y 两个整数,等待返回的结果。

复制代码
// client/main.go

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
)

type addParam struct {
    X int `json:"x"`
    Y int `json:"y"`
}

type addResult struct {
    Code int `json:"code"`
    Data int `json:"data"`
}

func main() {
    // 通过HTTP请求调用其他服务器上的add服务
    url := "http://127.0.0.1:9090/add"
    param := addParam{
        X: 10,
        Y: 20,
    }
    paramBytes, _ := json.Marshal(param)
    resp, _ := http.Post(url, "application/json", bytes.NewReader(paramBytes))
    defer resp.Body.Close()

    respBytes, _ := ioutil.ReadAll(resp.Body)
    var respData addResult
    json.Unmarshal(respBytes, &respData)
    fmt.Println(respData.Data) // 30
}
复制代码

这种模式是我们目前比较常见的跨服务或跨语言之间基于HTTP API的服务调用模式。 既然使用API调用也能实现类似远程调用的目的,为什么还要用RPC呢?

 

使用 RPC 的目的是让我们调用远程方法像调用本地方法一样无差别。并且 RESTful 是基于 HTTP 协议,传输数据采用 JSON  等文本协议。相较于 RPC 直接使用 TCP 协议,传输数据采用二进制协议。RPC  通常相比 RESTful API 性能更好。

 

HTTP API多用于前后端之间的数据传输,而目前微服务架构下各个微服务之间多采用RPC调用。

 

net/rpc

基础 RPC 示例

Go 语言的rpc 包通过网络或其他 i/o 连接导出的对象方法的访问,服务器注册一个对象,并把它作为服务对外可见(服务名称就是类型名称)。注册后,对象导出方法将支持远程访问。服务器可以注册不同类型的多个对象(服务),但是不支持注册同一个类型的多个对象。

在下面的代码中定义一个 ServiceA 类型,并为其定义一个可以导出的 Add 方法。

 

服务端代码 【在腾讯云上】

复制代码
type Args struct {
    X, Y int
}

// 自定义一个结构体类型
type ServiceA struct {
}

// Add 为 ServerA 类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {
    *reply = args.X + args.Y
    return nil
}

func (s *ServiceA) Sub(args *Args, reply *int) error {
    *reply = args.X - args.Y
    return nil
}

func main() {
    service := new(ServiceA)
    rpc.Register(service) // 注册 RPC 服务
    rpc.HandleHTTP()      // 基于 HTTP 协议
    l, e := net.Listen("tcp", ":9091")
    if e != nil {
        log.Fatalln("listen error:", e)
    }
    http.Serve(l, nil)
}
复制代码

 

 

客户端代码 【在本地】

复制代码
func main() {
    // 建立 HTTP 连接
    client, err := rpc.DialHTTP("tcp", "101.42.97.221:9091")
    if err != nil {
        log.Fatalln("dialing", err)
    }

    fmt.Printf("aa")
    // 同步调用
    args := &Args{10, 20}
    var reply int
    err = client.Call("ServiceA.Add", args, &reply)
    if err != nil {
        log.Fatal("ServiceA.Add error:", err)
    }
    fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)

    err = client.Call("ServiceA.Sub", args, &reply)
    if err != nil {
        log.Fatal("ServiceA.Add error:", err)
    }
    fmt.Printf("ServiceA.Add: %d-%d=%d\n", args.X, args.Y, reply)

  
    // 异步调用
	var reply2 int
	divCall := client.Go("ServiceA.Add", args, &reply2, nil)
	replyCall := <-divCall.Done // 接收调用结果
	fmt.Println(replyCall.Error)
	fmt.Println(reply2)
}
复制代码

 

posted @   dogRuning  阅读(51)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示