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 调用流程
- 服务消费者(Client )通过本地调用的方式调用服务。
- 客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体。
- 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
- 服务端存根(Server Stub)收到消息后进行解码(反序列化操作),服务端存根(Server Stub)根据解码结果调用本地的服务进行相关处理
- 服务端(Server)本地服务业务处理。
- 处理结果返回给服务端存根(Server Stub)。
- 服务端存根(Server Stub)序列化结果,
- 服务端存根(Server Stub)将结果通过网络发送至消费方。
- 客户端存根(Client Stub)接收到消息,并进行解码(反序列化)。
- 服务消费方得到最终结果。
通过上面的操作简单分析之后,我们可以将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
,TCP
和JSONPRC
Golang PRC 的函数必须是特定的格式写法才能被远程方法,不然就访问不到了,golang RPC 对外暴露服务的标准如下
func (t *T) MethodName(argType T1, replyType *T2) error
简单说明如下:
- 方法的类型是能导出的
- 方法是能导出的
- 方法的只有两个参数,这两个参数必须是能导出的或者是内建类型
- 参数 T1表示调用方提供的参数
- 参数T2 表示要放回调用方的结果
- 参数T1和T2 必须能被golang 的
encoding/gob
包 编码和解码- 方法的第二个参数必须是指针类型的
- 方法的返回值必须是
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 seejsonrpc 其实也是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
有疑问加站长微信联系(非本文作者)