rpc是一种通信协议,也就是计算机之间的沟通方式,类比http,mysql等,计算机之间通信是网络连接,tcp/ip协议里一个网络连接包含五元组信息-协议-端口-ip,rpc通过在一个网络连接上设置stub,监控这个连接的rpc请求,一般情况rpc的server端绑定的stub的载体有:1.http协议上,监听所有的http连接(指定server端ip,协议,端口),所有通过http协议过来的连接,如果有rpc请求,都会被rpc的server端捕捉(当前前提是这个请求已经注册在了rpc-seveer中);2.tcp连接
rpc关注2方面内容:1.传输方式-服务端的载体-传递数据的协议-因为rpc是会话层的沟通方式,所以要考虑在传输的下层的哪一层绑定:是绑定了一个协议-基于http协议,还是绑定在了一个tcp连接上,2.服务端和客户端共同使用的-数据传输的格式:json/xml/protobuf 只要统一了数据传输格式,以及载体,就可以用客户端调用服务端的rpc服务,客户端不限语言。
JSON和protobuf是支持多语言的-即这些协议能够翻译成其他语言如java;golang官方的net/rpc
库使用encoding/gob
进行编解码,支持http和tcp协议的传输方式;
远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。 如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用。维基百科:远程过程调用
用通俗易懂的语言描述就是:RPC允许跨机器、跨语言调用计算机程序方法。打个比方,我用go语言写了个获取用户信息的方法getUserInfo,并把go程序部署在阿里云服务器上面,现在我有一个部署在腾讯云上面的php项目,需要调用golang的getUserInfo方法获取用户信息,php跨机器调用go方法的过程就是RPC调用。
在golang中实现RPC非常简单,有封装好的官方库和一些第三方库提供支持。Go RPC可以利用tcp或http来传递数据,可以对要传递的数据使用多种类型的编解码方式。golang官方的net/rpc
库使用encoding/gob
进行编解码,支持tcp
或http
数据传输方式,由于其他语言不支持gob
编解码方式,所以使用net/rpc
库实现的RPC方法没办法进行跨语言调用。
golang官方还提供了net/rpc/jsonrpc
库实现RPC方法,JSON RPC采用JSON进行数据编解码,因而支持跨语言调用。但目前的jsonrpc库是基于tcp协议实现的,暂时不支持使用http进行数据传输。
除了golang官方提供的rpc库,还有许多第三方库为在golang中实现RPC提供支持,大部分第三方rpc库的实现都是使用protobuf
进行数据编解码,根据protobuf
声明文件自动生成rpc方法定义与服务注册代码,在golang中可以很方便的进行rpc服务调用。
rpc | rpc 包提供了一个方法来通过网络或者其他的I/O连接进入对象的外部方法. | |
jsonrpc | jsonrpc 包使用了rpc的包实现了一个JSON-RPC的客户端解码器和服务端的解码器. |
下面的例子演示一下如何使用golang官方的net/rpc
库实现RPC方法,使用http
作为RPC的载体,通过net/http
包监听客户端连接请求。准确地说,我认为是使用golang封装的http协议进行通信的连接,都被注册到了rpc的server端的stub上。
//服务端代码
package main
import (
"errors"
"fmt"
"net/http"
"net/rpc"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
//test http rpc
func main() {
arith := new(Arith)
rpc.Register(arith)// 注册rpc服务 server-called
rpc.HandleHTTP()// 采用http协议作为rpc载体 server-stub
go func() {
err := http.ListenAndServe(":1234", nil)
if err != nil {
fmt.Println(err.Error())
}
}()
go func() {
err1 := http.ListenAndServe(":12341", nil)
if err1 != nil {
fmt.Println(err1.Error())
}
}()
select{}
}
==============================客户端代码===================
package main
import (
"fmt"
"log"
"net/rpc"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
serverAddress := "127.0.0.1"
client, err := rpc.DialHTTP("tcp", serverAddress+":1234") //!!!!!!!! client-stub
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)//!!!!!!!!client-call
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
test(serverAddress)
}
func test(serverAddress string){
fmt.Println("===question2--from-port-12341")
client, err := rpc.DialHTTP("tcp", serverAddress+":12341")
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
上面的例子我们演示了使用net/rpc
实现RPC的过程,但是没办法在其他语言中调用上面例子实现的RPC方法。所以接下来的例子我们演示一下使用net/rpc/jsonrpc
库实现RPC方法,此方式实现的RPC方法支持跨语言调用。
$GOPATH/src/test/rpc/jsonrpc_server.go
package main
import (
"errors"
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"os"
)
// 算数运算结构体
type Arith struct {
}
// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}
// 算数运算响应结构体
type ArithResponse struct {
Pro int // 乘积
Quo int // 商
Rem int // 余数
}
// 乘法运算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
res.Pro = req.A * req.B
return nil
}
// 除法运算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
if req.B == 0 {
return errors.New("divide by zero")
}
res.Quo = req.A / req.B
res.Rem = req.A % req.B
return nil
}
func main() {
rpc.Register(new(Arith)) // 注册rpc服务
lis, err := net.Listen("tcp", "127.0.0.1:8096")
if err != nil {
log.Fatalln("fatal error: ", err)
}
fmt.Fprintf(os.Stdout, "%s", "start connection")
for {
conn, err := lis.Accept() // 接收客户端连接请求
if err != nil {
continue
}
go func(conn net.Conn) { // 并发处理客户端请求
fmt.Fprintf(os.Stdout, "%s", "new client in coming\n")
jsonrpc.ServeConn(conn)
}(conn)
}
}
上述服务端程序启动后,将会监听本地的8096端口,并处理客户端的tcp连接请求。我们可以用golang实现一个客户端程序连接上述服务端并进行RPC调用。
$GOPATH/src/test/rpc/jsonrpc_client.go
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
)
// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}
// 算数运算响应结构体
type ArithResponse struct {
Pro int // 乘积
Quo int // 商
Rem int // 余数
}
func main() {
conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8096")
if err != nil {
log.Fatalln("dailing error: ", err)
}
req := ArithRequest{9, 2}
var res ArithResponse
err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
if err != nil {
log.Fatalln("arith error: ", err)
}
fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)
err = conn.Call("Arith.Divide", req, &res)
if err != nil {
log.Fatalln("arith error: ", err)
}
fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
}
为了实现跨语言调用,在golang中实现RPC方法的时候我们应该选择一种跨语言的数据编解码方式,比如JSON,上述的jsonrpc
可以满足此要求,但是也存在一些缺点,比如不支持http传输,数据编解码性能不高等。于是呢,一些第三方rpc库都选择采用protobuf
进行数据编解码,并提供一些服务注册代码自动生成功能。下面的例子我们使用protobuf
来定义RPC方法及其请求响应参数,并使用第三方的protorpc
库来生成RPC服务注册代码。
首先,需要安装protobuf
及protoc
可执行命令,可以参考此篇文章:protobuf快速上手指南
然后,我们编写一个proto文件,定义要实现的RPC方法及其相关参数。
$GOPATH/src/test/rpc/pb/arith.proto
syntax = "proto3";
package pb;
// 算术运算请求结构
message ArithRequest {
int32 a = 1;
int32 b = 2;
}
// 算术运算响应结构
message ArithResponse {
int32 pro = 1; // 乘积
int32 quo = 2; // 商
int32 rem = 3; // 余数
}
// rpc方法
service ArithService {
rpc multiply (ArithRequest) returns (ArithResponse); // 乘法运算方法
rpc divide (ArithRequest) returns (ArithResponse); // 除法运算方法
}
接下来我们需要根据上述定义的arith.proto
文件生成RPC服务代码。
要先安装protorpc
库:go get github.com/chai2010/protorpc
然后使用protoc
工具生成代码:protoc --go_out=plugin=protorpc=. arith.proto
执行protoc
命令后,在与arith.proto
文件同级的目录下生成了一个arith.pb.go
文件,里面包含了RPC方法定义和服务注册的代码。
基于生成的arith.pb.go
代码我们来实现一个rpc服务端
$GOPATH/src/test/rpc/protorpc_server.go
package main
import (
"errors"
"test/rpc/pb"
)
// 算术运算结构体
type Arith struct {
}
// 乘法运算方法
func (this *Arith) Multiply(req *pb.ArithRequest, res *pb.ArithResponse) error {
res.Pro = req.GetA() * req.GetB()
return nil
}
// 除法运算方法
func (this *Arith) Divide(req *pb.ArithRequest, res *pb.ArithResponse) error {
if req.GetB() == 0 {
return errors.New("divide by zero")
}
res.Quo = req.GetA() / req.GetB()
res.Rem = req.GetA() % req.GetB()
return nil
}
func main() {
pb.ListenAndServeArithService("tcp", "127.0.0.1:8097", new(Arith))
}
运行上述程序,将会监听本地的8097端口并接收客户端的tcp连接。
基于ariti.pb.go
再来实现一个客户端程序。
$GOPATH/src/test/protorpc_client.go
package main
import (
"fmt"
"log"
"test/rpc/pb"
)
func main() {
conn, err := pb.DialArithService("tcp", "127.0.0.1:8097")
if err != nil {
log.Fatalln("dailing error: ", err)
}
defer conn.Close()
req := &pb.ArithRequest{9, 2}
res, err := conn.Multiply(req)
if err != nil {
log.Fatalln("arith error: ", err)
}
fmt.Printf("%d * %d = %d\n", req.GetA(), req.GetB(), res.GetPro())
res, err = conn.Divide(req)
if err != nil {
log.Fatalln("arith error ", err)
}
fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
}
上面的三个例子,我们分别使用net/rpc
、net/rpc/jsonrpc
、protorpc
实现了golang中的RPC服务端,并给出了对应的golang客户端RPC调用示例,因为JSON和protobuf是支持多语言的,所以使用jsonrpc
和protorpc
实现的RPC方法我们是可以在其他语言中进行调用的。下面给出一个php客户端程序,通过socket连接调用jsonrpc实现的服务端RPC方法。
$PHPROOT/jsonrpc.php
conn = fsockopen($host, $port, $errno, $errstr, 3);
if (!$this->conn) {
return false;
}
}
public function Call($method, $params) {
if (!$this->conn) {
return false;
}
$err = fwrite($this->conn, json_encode(array(
'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;
}
return json_decode($line,true);
}
}
$client = new JsonRPC("127.0.0.1", 8096);
$args = array('A'=>9, 'B'=>2);
$r = $client->Call("Arith.Multiply", $args);
printf("%d * %d = %d\n", $args['A'], $args['B'], $r['result']['Pro']);
$r = $client->Call("Arith.Divide", array('A'=>9, 'B'=>2));
printf("%d / %d, Quo is %d, Rem is %d\n", $args['A'], $args['B'], $r['result']['Quo'], $r['result']['Rem']);
除了上面提到的三种在golang实现RPC的方式外,还有一些其他的rpc库提供了类似的功能,比较出名的有google开源的grpc,但是grpc的初次安装比较麻烦,这里就不做进一步介绍了,有兴趣的可以自己了解。