RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
RPC原理及调用步骤 了解完了RPC技术的组成结构我们来看一下具体是如何实现客户端到服务端的调用的。实际上,如果我们想要在网络中的任意两台计算机上实现远程调用过程,要 解决很多问题,比如: 两台物理机器在网络中要建立稳定可靠的通信连接。 两台服务器的通信协议的定义问题,即两台服务器上的程序如何识别对方的请求和返回结果。也就是说两台计算机必须都能够识别对方发来的信息,并且能够 识别出其中的请求含义和返回含义,然后才能进行处理。这其实就是通信协议所要完成的工作。
参考
Go语言基础可参看:《Go语言入门及技术指南》 https://blog.youkuaiyun.com/yan_dk/article/details/110557155
Php-Swoole可参看:https://blog.youkuaiyun.com/yan_dk/article/details/89445254
RPC 原理
客户端(Client):服务调用发起方,也称为服务消费者。
客户端存根(Client Stub):该程序运行在客户端所在的计算机机器上,主要用来存储要调用的服务器的地址,另外,该程序还负责将客户端请求远端服务器程 序的数据信息打包成数据包,通过网络发送给服务端Stub程序;其次,还要接收服务端Stub程序发送的调用结果数据包,并解析返回给客户端。
服务端(Server):远端的计算机机器上运行的程序,其中有客户端要调用的方法。
服务端存根(Server Stub):接收客户Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调 用的结果进行数据处理打包发送给客户端Stub程序。
- 客户端想要发起一个远程过程调用,首先通过调用本地客户端Stub程序的方式调用想要使用的功能方法名;
- 客户端Stub程序接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。
- 客户端Stub查找到远程服务器程序的IP地址,调用Socket通信协议,通过网络发送给服务端。
- 服务端Stub程序接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。
- 服务端Stub程序准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。
- 服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序。
- 服务端Stub程序将程序调用结果按照约定的协议进行序列化,并通过网络发送回客户端Stub程序。
- 客户端Stub程序接收到服务端Stub发送的返回数据,对数据进行反序列化操作,并将调用返回的数据传递给客户端请求发起者。
- 客户端请求发起者得到调用结果,整个RPC调用过程结束。
示例(说明GRPC通信原理)
需求场景说明:包括一个服务端、一个客户端,服务端用GO声明远程方法FindById,客户端发送请求调用FindById方法,服务端返回响应。
实现说明:客户端可以是多种客户端(如go,php)。
GO客户端调用GO服务端
说明:go使用net.rpc包
Go服务端server/main.go
服务端:server/main.go
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"time"
)
func main() {
//goods := new(Goods)
goods := &Goods{
Id: 1,
Name: "苹果",
}
rpc.Register(goods)
rpc.HandleHTTP()
// 开启rpc监听 -》端口和ip
listen, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("listen error:", err)
}
go http.Serve(listen, nil)
fmt.Println("服务器启动...")
time.Sleep(1000e9)
}
type Goods struct {
Id int
Name string
}
func (g *Goods) FindById(args *Goods, reply *Goods) error {
fmt.Println("接收到请求信息 :", args)
*reply = *g
return nil
}
启动服务端,监听1234端口
客户端:client/main.go
package main
import (
"fmt"
"net/rpc"
)
type Goods struct {
Id int
Name string
}
func main() {
client, _ := rpc.DialHTTP("tcp", "127.0.0.1:1234")
res_goods := Goods{}
req_goods := Goods{
Id: 1,
}
// 发起请求,调用服务端的方法
// err := client.Call("Goods.FindById", reqParam, &resgoods)
err := client.Call("Goods.FindById", req_goods, &res_goods)
if err != nil {
fmt.Println("err : ", err)
}
fmt.Println("resgoods : ", res_goods)
}
运行客户端
得到响应结果。
PHP客户端调用Go服务端
说明:我们使用php的swoole扩展,linux环境,需要开放端口如9500,还需要搭建go环境
我们先模拟swoole客户端调用服务端,看能否调用通过,server.php仅仅是测试swoole服务是否正常。实际使用的仍然是php客户端调用go服务端。
php-swoole服务端
<?php
$server = new Swoole\Server('127.0.0.1', 9500);
$server->on('connect', function ($server, $fd){
});
$server->on('receive', function ($server, $fd, $reactor_id, $data) {
var_dump('客户端请求数据:'.$data);
$result = [
"id" => 0,
"result" => "hello i'm swoole",
"error" => null
];
$server->send($fd, json_encode($result));
$server->close($fd);
});
$server->on('close', function ($server, $fd) {
});
var_dump("swoole 服务器启动...");
$server->start();
启动php-swoole服务器
php-swoole客户端
<?php
$client = new swoole_client(SWOOLE_SOCK_TCP);
//连接到服务器
if (!$client->connect('127.0.0.1', 9500))
{
die("connect failed.");
}
//向服务器发送数据
if (!$client->send('{"method":"goods.FindById","params":[{"Id":0,"Name":""}],"id":0}'))
{
die("send failed.");
}
//从服务器接收数据
$data = $client->recv();
if (!$data)
{
die("recv failed.");
}
echo $data;
//关闭连接
$client->close();
说明:这里php-swoole客户端调用是发送send了一个格式化的字符串,是按照go的RPC服务接口格式的字符串,’{“method”:“goods.FindById”,“params”:[{“Id”:0,“Name”:""}],“id”:0}’,go服务端会按照这个字符串中格式解析出相应的参数,执行相应调用。
php-swoole客户端运行测试
成功说明php-swoole客户端能正常调用
Go服务端
```go
package main
import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
type Goods struct {
Id int
Name string
}
type Params struct {
Id int
Name string
}
func (g *Goods) FindById(args *Params, reply *string) error {
fmt.Println("接收到请求信息 :", args)
*reply = "安卓777"
return nil
}
func main() {
// 开启rpc监听 -》端口和ip
listen, _ := net.Listen("tcp", "127.0.0.1:9500")
defer listen.Close()
// 建立连接
conn, _ := listen.Accept()
// 注册服务 =》》 hash表
// RegisterName (name,recv)
// (name 服务标识 recv 具体的服务
rpc.RegisterName("goods", new(Goods))
fmt.Println("服务器go启动...")
jsonrpc.ServeConn(conn)
}
运行go服务端
再运行php客户端
说明php客户端已成功调用go服务端。