Dawn's Blogs

分享技术 记录成长

0%

GO语言杂谈 (2) RPC

RPC

简介

RPC(Remote Procedure Call,远程过程调用)允许一台计算机通过网络从远程计算机的程序中请求服务。RPC协议构建于TCP或UDP,或者是HTTP上。允许开发者直接调用另一台服务器上的程序,而开发者无需另外的为这个调用过程编写网络通信相关代码。

RPC允许跨机器跨语言调用计算机程序方法。

工作原理

运行时,一次客户机对服务器的 RPC 调用,其内部操作大致有如下:

  1. 调用客户端句柄:执行传送参数
  2. 调用本地系统内核发送网络消息
  3. 消息传送到远程主机
  4. 服务器句柄得到消息并取得参数
  5. 执行远程过程
  6. 执行的过程将结果返回服务器句柄
  7. 服务器句柄返回结果,调用远程系统内核
  8. 消息传回本地主机
  9. 客户句柄由内核接收消息
  10. 客户接收句柄返回的数据

img

RPC vs RESTful

RPC 可以通过 TCP、UDP 或者 HTTP 等进行传输消息,称为 RPC over TCP、RPC over HTTP。

首先比较 RPC over HTTP 和 RESTful:

  • RPC 的客户端和服务器是紧密耦合的,,客户端需要知道调用的过程的名字,过程的参数以及它们的类型、顺序等。一旦服务器更改了过程的实现, 客户端的实现很容易出问题。RESTful 的客户端和服务器是松散的关系,客户端并不关心服务器端的具体实现,比较灵活。
  • 它们操作的对象不一样。RPC 操作的是方法对象,而 RESTful 操作的是资源
  • RESTful 执行的是对资源的增删改查(CRUD),如果实现一个其他具体操作 RPC 的实现更加具有意义。

接着比较 RPC over TCP 和 RESTful:

  • RPC over TCP 减少了 HTTP 协议带来的额外开销(如HTTP 请求头)。
  • RPC over TCP 可以通过长连接减少连接的建立所产生的花费,当然 RESTful 也可以通过 keep-alive 实现长连接,但是它最大的一个问题是它的 request-response 模型是阻塞的(HTTP 1.0 和 HTTP 1.1 是阻塞的,HTTP 2.0 不是),发送一个请求得到响应后才能发送第二个,而 RPC 没有这个限制。

Go RPC

GO语言官方提供了net/rpc包实现RPC,使用encoding/gob进行编解码,支持TCP或者HTTP数据传输方式,由于其他语言不支持gob编解码方式,所以使用net/rpc实现的RPC方法没办法实现跨语言调用

net/rpc 介绍

net/rpc包允许PRC客户端程序通过网络或者其他IO连接,调用一个远程对象的公开方法。在PRC服务端,可将一个对象注册为可访问的服务,之后该对象的公开方法就能够以远程的方式提供访问。一个RPC服务端可以注册多个不通类型的对象,但不允许注册同一类型的多个对象。

只有满足如下标准的方法才能用于远程访问,其余方法会被忽略:

  • 方法是导出的(首字母大写)
  • 方法有两个参数,第一个参数是接收的参数,第二个参数是返回给客户端的参数
  • 方法的第二个参数是指针
  • 方法只有一个error接口类型的返回值

事实上,方法必须看起来像这样:

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

方法的第一个参数代表调用者提供的参数第二个参数代表返回给调用者的参数。方法的返回值,如果非nil,将被作为字符串回传,在客户端看来就和errors.New创建的一样。如果返回了错误,回复的参数将不会被发送给客户端。

net/rpc/jsonrpc 介绍

官方还提供了net/rpc/jsonrpc实现RPC方法,JSON RPC采用JSON进行数据编解码,因而支持跨语言调用。但目前的jsonrpc库是基于TCP协议实现的,暂时不支持使用http进行数据传输

案例

net/rpc

下面利用HTTP作为RPC载体,通过net/http监听客户端的连接请求。

  • server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

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

// 算数运算结构体
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 {
// 除数为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服务
rpc.HandleHTTP() // 采用http协议作为rpc载体

lis, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
log.Fatalln("Listen error:", err)
}

fmt.Println("start connection...")

http.Serve(lis, nil)
}
  • client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

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

// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}

type ArithResponse struct {
Pro int // 乘积
Quo int // 商
Rem int // 余数
}

func main() {
conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8888")
if err != nil {
log.Fatalln("Dial error:", err)
}

req := ArithRequest{10, 3}
var res ArithResponse

err = conn.Call("Arith.Multiply", req, &res) // 进行乘法运算
if err != nil {
log.Fatalln("Multiply 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("Divide error:", err)
}
fmt.Printf("%d / %d = %d ... %d\n", req.A, req.B, res.Quo, res.Rem)

}

net/rpc/jsonrpc

下面演示net/rpc/jsonrpc的使用方法,此方法只支持TCP作为载体,但是可以跨语言调用

  • server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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 {
// 除数为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:6666")
if err != nil {
log.Fatalln("Listen error:", err)
}

fmt.Println("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)
}
}
  • client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

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

// 算数运算结构体
type Arith struct {
}

// 算数运算请求结构体
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:6666")
if err != nil {
log.Fatalln("Dial error:", err)
}

req := ArithRequest{10, 3}
var res ArithResponse

err = conn.Call("Arith.Multiply", req, &res) // 进行乘法运算
if err != nil {
log.Fatalln("Multiply 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("Divide error:", err)
}
fmt.Printf("%d / %d = %d ... %d\n", req.A, req.B, res.Quo, res.Rem)

}