第二十七章:Go语言与RPC

captain89 · · 952 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

1. RPC 概述

RPC 是Remote Procedure Call Protocol 的简写,其中文意思是远程过程调用协议 ,就是通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.RPC将本地调用变为远程服务器上调用,这为系统处理能力和吞吐量带来了更大的提升,在OSI网络通信模型中RPC跨越了传输层和应用层.

  • 我们通俗的理解就是像调用本地函数一样区调用远程的函数,实现函数调用模式的网络化.那么这个远程到底是多远,既可以是物理上的远程也可以是逻辑上的远程.
  • 因为PRC的这种跨越了物理服务器的限制,在 RPC 中可选的网络传输方式有多种,可以选择 TCP 协议、UDP 协议、HTTP 协议
  • 在现在的分布式系统中不同的节点之间比较常见的通信方式也是RPC

既然有远程过程调用 那么就有本地过程调用,本地过程调用在不同的系统中叫法不

在Windows系统中称为 LPC

在Linux系统中称为 IPC 进程间通信

不论称呼如何其本质都是 本机上不同的进程之间通信协作的调用方式

2. RPC 组成

我们简单的看 RPC技术在构成上是由四部分组成的 客户端 ,客户端存根,服务端,服务端存根

  • 客户端(client) : 服务调用的发起方
  • 客户端存根(client Stub)
    • 运行在客户端机器上
    • 存储调用服务器地址
    • 将客户端请求的数据信息打包成数据包
    • 通过网发送给服务端存根程序
    • 接收服务端的发回的调用结果数据包,解析后给客户端
  • 服务端 : 服务提供者
  • 服务端存根(server Stub) :
    • 存在与服务端机器上
    • 接收客户端Stub程序发送来请求消息数据包
    • 调用服务端中的程序方法
    • 将结果打包成数据包发送给客户端Stub程序

3. RPC 调用流程

rpc.png
  1. 服务消费者(Client )通过本地调用的方式调用服务。
  2. 客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体。
  3. 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
  4. 服务端存根(Server Stub)收到消息后进行解码(反序列化操作),服务端存根(Server Stub)根据解码结果调用本地的服务进行相关处理
  5. 服务端(Server)本地服务业务处理。
  6. 处理结果返回给服务端存根(Server Stub)。
  7. 服务端存根(Server Stub)序列化结果,
  8. 服务端存根(Server Stub)将结果通过网络发送至消费方。
  9. 客户端存根(Client Stub)接收到消息,并进行解码(反序列化)。
  10. 服务消费方得到最终结果。

通过上面的操作简单分析之后,我们可以将PRC调用看出一系列操作的集合,但是RPC涉及的几个核心点我们可以看一下:

  • 动态代理技术 : 客户端存根(client Stub) 和 服务端存根(server Stub) 在具体实现中都是用动态代理技术自动生成的一段程序
  • 序列化反序列化 :为啥要进行序列化和反序列化操作呢?
    • RPC调用的过程我们可以看成是A机器上的程序调用B机器上的函数,那么这个过程中需要进行数据的传输,我们知道所有的数据都是以字节的形式进行传输的,但是在具体编程过程中我们基本使用的是数据对象,因此想在网络中进行数据对象和变量的传输,就需要将数据对象进行序列化和反序列化
    • 序列化 : 将数据对象转换成字节序列的过程,也就是编码的过程
    • 反序列化: 将字节序列恢复成数据对象的过程,也就是解码的过程

4. Go语言实现PRC

Golang 中提供的标准包中实现了对PRC 的支持

  • Golang中提供的PRC标准包,只能支持使用Golang语言开发的RPC服务,也就是使用使用Golang 开发的PRC 服务端,只能使用Golang开发的PRC客户端程序调用 ,为啥为这样? 因为golang的自带的RPC标准包采用的是 gob编码

    • gob 是Golang包自带的一个数据结构序列化的编码/解码工具。编码使用Encoder,解码使用Decoder。一种典型的应用场景就是RPC(remote procedure calls)。
  • Golang 实现的PRC 可以支持三种方式请求 HTPP , TCPJSONPRC

  • Golang PRC 的函数必须是特定的格式写法才能被远程方法,不然就访问不到了,golang RPC 对外暴露服务的标准如下

    func (t *T) MethodName(argType T1, replyType *T2) error
    

    简单说明如下:

    1. 方法的类型是能导出的
    2. 方法是能导出的
    3. 方法的只有两个参数,这两个参数必须是能导出的或者是内建类型
      1. 参数 T1表示调用方提供的参数
      2. 参数T2 表示要放回调用方的结果
      3. 参数T1和T2 必须能被golang 的encoding/gob 包 编码和解码
    4. 方法的第二个参数必须是指针类型的
    5. 方法的返回值必须是 error类型的

4.1 HTTP PRC

我们看看golang 中的RPC的第一种实现方式方式通过HTTP传输

rpc 服务端代码

rpc_server1.go

package main

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

type Arguments struct {
    A int
    B int
}
type DemoRpc struct{}

func (d *DemoRpc) Add(req Arguments, resp *int) error {
    *resp = req.A + req.B
    return nil
}
func (d *DemoRpc) Minus(req Arguments, resp *int) error {
    *resp = req.A - req.B
    return nil
}

func main() {
    // 注册rpc服务
    rpc.Register(new(DemoRpc))
    // 将用于RPC消息的HTTP处理程序注册到DefaultServer
    rpc.HandleHTTP()
    // 监听8080端口
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err.Error())
    }
}

rpc 客户端代码

rpc_client1.go

package main

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

type Arguments struct {
    A int
    B int
}

func main() {
    //DialHTTP连接到指定网络地址的HTTP RPC服务器
    //返回一个rpc客户端
    client, err := rpc.DialHTTP("tcp", ":8080")
    if err != nil {
        log.Fatal(err.Error())
    }
    arg := Arguments{99, 1}
    var resp int
    //调用指定的函数并等待其完成
    err = client.Call("DemoRpc.Add", arg, &resp)
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Printf("rpc DemoRpc Add %v\n", resp)
    err = client.Call("DemoRpc.Minus", arg, &resp)
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Printf("rpc DemoRpc Minus %v\n", resp)
    //模拟一个错误的rpc调用
    err = client.Call("DemoRpc.Nothing", arg, &resp)
    if err != nil {
        log.Fatal(" call err:", err.Error())
    }
    fmt.Printf("rpc DemoRpc Nothing %v\n", resp)

}

运行这两个代码文件,结果如下

rpc DemoRpc Add 100
rpc DemoRpc Minus 98
2019/12/14 13:49:25  call err:rpc: can't find method DemoRpc.Nothing

4.2 TCP RPC

rpc 服务端代码

rpc_server2.go

package main

import (
    "github.com/pkg/errors"
    "log"
    "net"
    "net/rpc"
)

type Demo struct{}
type Params struct {
    X int
    Y int
}

// 暴露对外的服务
func (d *Demo) Add(p Params, result *int) error {
    *result = p.X + p.Y
    return nil
}
func (d *Demo) Minus(p Params, result *int) error {
    *result = p.X - p.Y
    return nil
}
func (d *Demo) Div(p Params, result *int) error {
    if p.Y == 0 {
        return errors.New("dividend is zero")
    }
    *result = p.X / p.Y
    return nil
}
func main() {
    //注册一个自定义名称的rpc服务
    //和rpc.Register作用是一样
    rpc.RegisterName("DemoRpc", new(Demo))
    // 开启一个tcp服务,监听8081端口
    listen, err := net.Listen("tcp", ":8081")
    if err != nil {
        log.Fatal(err.Error())
    }
    for {
        // 等待连接
        conn, err := listen.Accept()
        if err != nil {
            log.Fatal(err.Error())
        }
        go rpc.ServeConn(conn)
    }

}

rpc 客户端代码

rpc_client2.go

package main

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

type Params struct {
    X int
    Y int
}

func main() {
    // 连接到指定的rpc服务器
    client, err := rpc.Dial("tcp", ":8081")
    if err != nil {
        log.Fatal(err.Error())
    }
    var result int
    p := Params{99, 1}
    err = client.Call("DemoRpc.Add", p, &result)
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Printf("%d + %d = %d\n", p.X, p.Y, result)
    err = client.Call("DemoRpc.Minus", p, &result)
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Printf("%d - %d = %d\n", p.X, p.Y, result)
    p.Y = 0
    err = client.Call("DemoRpc.Div", p, &result)
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Printf("%d / %d = %d\n", p.X, p.Y, result)
}

运行两个代码文件,结果如下

99 + 1 = 100
99 - 1 = 98
2019/12/14 14:23:29 dividend is zero

我们看到了http PRC 和tcp RPC 的客户端处理特别相似,区别就在连接到服务端的方法一个是DialHTTP 另一个是 Dial

4.3 RPC 异步调用

这里的异步调用主要是指的 RPC 的客户端异步调用

我们对上面的代码稍做修改即可

rpc 服务端代码

rpc_server3.go

package main

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

type ArgsDemo struct {
    A int
    B int
}
type DemoRpc3 struct{}

func (d *DemoRpc3) Add(req ArgsDemo, resp *int) error {
    for i := 0; i < 5; i++ {
        fmt.Println("sleep...", i)
        time.Sleep(1 * time.Second)
    }
    *resp = req.A + req.B
    fmt.Println("Add Do")
    return nil
}
func (d *DemoRpc3) Minus(req ArgsDemo, resp *int) error {
    *resp = req.A - req.B
    return nil
}

func main() {
    // 注册rpc服务
    rpc.Register(new(DemoRpc3))
    // 将用于RPC消息的HTTP处理程序注册到DefaultServer
    rpc.HandleHTTP()
    // 监听8080端口
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err.Error())
    }
}

rpc 客户端代码

rpc_client3.go

package main

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

type ArgsDemo struct {
    A int
    B int
}

func main() {
    //DialHTTP连接到指定网络地址的HTTP RPC服务器
    //返回一个rpc客户端
    client, err := rpc.DialHTTP("tcp", ":8080")
    if err != nil {
        log.Fatal(err.Error())
    }
    arg := ArgsDemo{9999, 8888}
    var resp int
    //异步调用指定的函数并等待其完成
    call := client.Go("DemoRpc3.Add", arg, &resp, nil)
    // 正常的同步调用
    err = client.Call("DemoRpc3.Minus", arg, &resp)
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Printf("rpc DemoRpc Minus %v\n", resp)
    for {
        select {
        case <-call.Done:
            if call.Error != nil {
                log.Println(call.Error.Error())
                return
            }
            fmt.Printf("rpc DemoRpc Add %v\n", resp)
            return
        default:
            fmt.Println("wait...")
            time.Sleep(1 * time.Second)
        }
    }

}

运行两个代码文件,结果如下

rpc DemoRpc Minus 1111
wait...
wait...
wait...
wait...
wait...
rpc DemoRpc Add 18887

5. json rpc

首先我们要明白 JSON-RPC,是一个无状态且轻量级的远程过程调用(RPC)传送协议,其传递内容透过 JSON 为主 并非是Goalng独有的,其他的编程语言也能实现

我们前面都说了 golang 标准包中的RPC包采用的是gob的编码,这就导致其他计算机编程语言想调用Golang写的rpc 服务是行不通的,真是这样的话那也太尴尬了

但是不要慌,我们可以使用 jsonrpc 解决这个问题 Let me see see

jsonrpc 其实也是Golang中的RPC实现,但是它采用的是json 的编码格式,用到的是 net/rpc/jsonrpc 这个包

5.1 json rpc 服务端代码

使用Golang实现

jsonrpc_server.go

package main

import (
    "fmt"
    "github.com/pkg/errors"
    "log"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
)

type JsonDemo struct{}
type JsonParams struct {
    X int
    Y int
}

// 暴露对外的服务
func (d *JsonDemo) Add(p JsonParams, result *int) error {
    *result = p.X + p.Y
    return nil
}
func (d *JsonDemo) Minus(p JsonParams, result *int) error {
    *result = p.X - p.Y
    return nil
}
func (d *JsonDemo) Div(p JsonParams, result *int) error {
    if p.Y == 0 {
        return errors.New("dividend is zero")
    }
    *result = p.X / p.Y
    return nil
}
func main() {
    //注册一个自定义名称的rpc服务
    rpc.RegisterName("JsonDemo", new(JsonDemo))
    // 开启一个tcp服务,监听8081端口
    listen, err := net.Listen("tcp", ":8081")
    if err != nil {
        log.Fatal(err.Error())
    }
    for {
        // 等待连接
        conn, err := listen.Accept()
        if err != nil {
            log.Fatal(err.Error())
        } else {
            fmt.Println(conn.RemoteAddr().String())
        }
        //在单个连接上运行JSON-RPC服务器
        go jsonrpc.ServeConn(conn)
    }
}

5.2 Golang json rpc 客户端

jsonrpc_client.go

package main

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

type JsonParams struct {
    X int
    Y int
}

func main() {
    // 连接到指定的json rpc服务器
    client, err := jsonrpc.Dial("tcp", ":8081")
    if err != nil {
        log.Fatal(err.Error())
    }
    var result int
    p := JsonParams{60, 40}
    err = client.Call("JsonDemo.Add", p, &result)
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Printf("%d + %d = %d\n", p.X, p.Y, result)
    err = client.Call("JsonDemo.Minus", p, &result)
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Printf("%d - %d = %d\n", p.X, p.Y, result)
    p.Y = 0
    err = client.Call("JsonDemo.Div", p, &result)
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Printf("%d / %d = %d\n", p.X, p.Y, result)
}

运行RPC调用客户端

go run jsonrpc_client.go

60 + 40 = 100
60 - 40 = 20
2019/12/14 15:58:44 dividend is zero

5.3 PHP json rpc客户端

jsonrpc_client.php

<?php
class JsonRpc
{
    // 定义一个私有变量
    private $conn;

    // 构造函数
    public function __construct(string $host, string $port)
    {
        // 建立一个socket连接
        $this->conn = fsockopen($host, $port);
        if (!$this->conn) {
            return false;
        }
    }

    // 定义公有方法
    public function CallRpc($method, $params)
    {
        if (!$this->conn) {
            return false;
        }
        // 发送json编码的数据对象
        $err = fwrite($this->conn, json_encode(
                array(
                    "jsonrpc" => "2.0",
                    "method" => $method,
                    "params" => array($params),
                    "id" => 0
                )) . "\n");
        if ($err === false) {
            return false;
        }
        // 设置流的超时时间
        stream_set_timeout($this->conn, 0, 3000);
        // 获取响应结果
        $line = fgets($this->conn);
        if ($line === false) {
            return NULL;
        }
        // json 解码
        return json_decode($line, true);
    }
}

$host = "127.0.0.1";
$port = "8081";
// 新建一个对象
$client = new JsonRpc($host, $port);
$params = array(
    "X" => 90,
    "Y" => 80
);
// 调用方法
$result = $client->CallRpc("JsonDemo.Add", $params);
if (empty($result["error"])) {
    printf("call JsonDemo.Add %d + %d = %s\n", $params["X"], $params["Y"], $result["result"]);
}
$result = $client->CallRpc("JsonDemo.Minus", $params);
if (empty($result["error"])) {
    printf("call JsonDemo.Minus.Minus %d - %d = %s\n", $params["X"], $params["Y"], $result["result"]);
}
$params["Y"] = 0;
$result = $client->CallRpc("JsonDemo.Div", $params);
if (empty($result["error"])) {
    printf("call JsonDemo.Div %d / %d = %s\n", $params["X"], $params["Y"], $result["result"]);
} else {
    printf("call JsonDemo.Div error %s\n", $result["error"]);
}

运行RPC调用客户端

php rpcjson_client.php

call JsonDemo.Add 90 + 80 = 170
call JsonDemo.Minus.Minus 90 - 80 = 10
call JsonDemo.Div error dividend is zero

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:captain89

查看原文:第二十七章:Go语言与RPC

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

952 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传