Golang与RPC

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 调用流程

在这里插入图片描述

  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(string $method, array $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
在Go语言中,如果想要通过RabbitMQ实现远程过程调用(RPC),你可以使用AmqpClient库来连接RabbitMQ,并利用其发布订阅(publish-subscribe)模式作为基本的RPC架构。一个常见的做法是创建一个请求消息队列,服务提供者生产数据,服务消费者接收并处理这些请求。 当你需要从RabbitMQ获取RPC返回时,通常的做法是: 1. **发送请求**:服务消费者创建一个包含请求信息的消息,并将其发送到特定的请求队列(Request Queue)。这可以视为RPC的调用。 2. **设置回调**:当服务提供者收到请求时,它会处理这个请求并在完成操作后生成一个响应。为了返回结果给原发者,服务提供者通常会在处理完后向一个响应队列(Response Queue)发送一个应答消息。 3. **绑定和确认机制**:通过设置自动确认(autoack)或者手动确认机制,服务提供者可以保证消息已经成功消费并准备好了回应。 4. **消费响应**:服务消费者的逻辑需要监听响应队列,每当有新消息到达,就会解析消息内容,即RPC的结果。 5. **结果处理**:消费者接收到响应后,可以根据需要更新状态、存储数据或直接返回给调用者。 例如,使用`github.com/streadway/amqp`这样的库,你可以定义如下的伪代码片段: ```go // 客户端调用RPC conn, err := amqp.Dial("amqp://...") channel, err := conn.Channel() reqQueueName := "request_queue" resQueueName, _ := channel.QueueDeclare("", false, true, false, "", []string{reqQueueName}) msgBody := encodeRequestData() // 请求数据编码 _, err = channel.BasicPublish("", reqQueueName, "", msgBody) response, _, err := channel.Get(resQueueName) if err == nil { respData := decodeResponse(response.Body) // 解码响应数据 processResponse(respData) } // 服务端处理请求 channel, err = ... // 服务提供者连接RabbitMQ channel.QueueBind(resQueueName, reqQueueName, "") req, ok := <-channel.Consume(reqQueueName, "", true, false, false, false, nil) if ok { for req != nil && req.Acknowledged { // 等待应答并确认 data, err := processRequest(req.Body) if err == nil { respBody := encodeResponseData(data) // 应答数据编码 channel.Publish("", resQueueName, false, respBody) } req.Ack(false) // 手动确认 } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值