Golang 17_Golang标准库RPC

参考博文:

一、Golang RPC 简介

1.1 RPC 简介

RPC 是 远程过程调⽤(Remote Procedure Call)的简称,是分布式系统中不同节点间流⾏的通信⽅式,它可以使一台主机上的进程调用另一台主机的进程,用于访问其它若干个主机提供服务,也就是我们常说的 C/S架构的服务,Server 与 Client 之间通过 RPC方式进行通信。

Golang 提供的 net/rpc库 使用 encoding/gob 进行编解码,支持 TCP 或 HTTP 数据传输方式,由于其它语言不支持 gob编解码方式,因此使用 net/rpc库 实现的 RPC方法是没有办法进行跨语言调用。

但是 Golang 还提供了 net/rpc/jsonrpc库 实现 rpc方法,jsonrpc 采用的是 json格式 进行数据编码,因此支持跨语言调用,目前jsonrpc 是基于tcp协议实现的,暂不支持http传输方式。

  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
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// server/main.go
package main

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

type HelloService struct {}

// Hello的逻辑就是将对方发送的消息前面添加一个Hello然后返还给对方
// 由于是一个rpc服务,因此参数上面还是有约束:
// 第一个参数是请求
// 第二个参数是响应
// 可以类比 Http handler
func (p *HelloService) Hello(request string, reply *string) error {
    *reply = "Hello: " + request
    return nil
}

func (p *HelloService) Echo(request string, reply *string) error {
    *reply = "Hello: " + request
    return nil
}

// 不可跨语言,默认gob序列化
func tcpServer(){
    //1. 实例化一个server
    listener, err := net.Listen("tcp", ":8002")
    if err != nil {
        log.Fatal("ListenTCP error:", err)
    }

    //2. 注册处理逻辑 handler
    // 把 HelloService 对象注册成一个 rpc 的 receiver
    // 其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,
    // 所有注册的方法会放在"HelloService"服务空间之下
    _ = rpc.RegisterName("HelloService", &HelloService{})

    //3. 启动服务
    // 通过 rpc.ServeConn函数在该TCP链接上为对⽅提供RPC服务。
    // 每Accept一个请求,就创建一个 goroutie 进行处理
    for {
        // 从监听里获取一个连接
        conn, err := listener.Accept()  // Accept 调用阻塞,当一个新的连接进来的时候调用返回
        if err != nil {
            log.Fatal("Accept error:", err)
        }

        // 将获取的连接交给RPC
        // 前面都是tcp的知识,到这个RPC就接管了
        // 因此,可以认为 rpc 帮我们封装了消息到函数调用的这个逻辑,
        // 提升了工作效率,逻辑比较简洁
        go rpc.ServeConn(conn)
    }
}

// 可跨语言
func tcpJsonServer(){
    //1. 实例化一个server
    listener, err := net.Listen("tcp", ":8001")
    if err != nil {
        log.Fatal("ListenTCP error:", err)
    }

    //2. 注册处理逻辑 handler
    _ = rpc.RegisterName("HelloService", &HelloService{})
    //3. 启动服务
    for {
        
        // 从监听里获取一个连接
        conn, err := listener.Accept()  // Accept 调用阻塞,当一个新的连接进来的时候调用返回
        if err != nil {
            log.Fatal("Accept error:", err)
        }

        go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}

func httpServer(){
    _ = rpc.RegisterName("HelloService", &HelloService{})
    rpc.HandleHTTP()
    http.ListenAndServe(":8000", nil)
}
func main(){
    go tcpServer()
    go tcpJsonServer()
    httpServer()
}


// client/main.go
package main

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

func TcpClient(){
    //1. 建立连接
    client, err := rpc.Dial("tcp", "localhost:8002")
    if err != nil {
        panic("连接失败")
    }
    var reply string //string有默认值
    //调用连接。
    // 然后通过client.Call调⽤具体的RPC⽅法
    // 在调⽤client.Call时:
    //   第⼀个参数是⽤点号链接的RPC服务名字和⽅法名字,
    //   第⼆和第三个参数分别为定义RPC⽅法的两个参数:
    //      第二个参数:请求参数
    //      第三个参数:请求响应,必须是一个指针,由底层的rpc服务负责赋值
    err = client.Call("HelloService.Hello", "bobby...", &reply)
    if err != nil {
        fmt.Println(err)
        //panic("调用失败")
    }
    fmt.Println(reply)
    err = client.Call("HelloService.Echo", "echo...", &reply)
    if err != nil {
        fmt.Println(err)
        //panic("调用失败")
    }
    fmt.Println(reply)
}

func TcpJsonClient(){
    //1. 建立连接
    conn, err := net.Dial("tcp", "localhost:8001")
    if err != nil {
        panic("连接失败")
    }
    var reply string //string有默认值
    client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
    err = client.Call("HelloService.Hello", "bobby", &reply)
    if err != nil {
        panic("调用失败")
    }
    fmt.Println(reply)
    err = client.Call("HelloService.Echo", "echo...", &reply)
    if err != nil {
        fmt.Println(err)
        //panic("调用失败")
    }
    fmt.Println(reply)
}

func HttpClient(){
    //1. 建立连接
    client, err := rpc.DialHTTP("tcp", "localhost:8000")
    if err != nil {
        panic("连接失败")
    }
    var reply string //string有默认值
    //调用连接。
    err = client.Call("HelloService.Hello", "bobby", &reply)
    if err != nil {
        fmt.Println(err)
        //panic("调用失败")
    }
    fmt.Println(reply)
    err = client.Call("HelloService.Echo", "echo...", &reply)
    if err != nil {
        fmt.Println(err)
        //panic("调用失败")
    }
    fmt.Println(reply)
}
func main() {
    TcpClient()
    HttpClient()
    TcpJsonClient()
}

二、RPC Service

2.1 RPC Service 注册服务要求

服务端注册一个对象,使其作为一个服务被暴露,服务的名字是该对象的类型名,注册后对象导出的方法就可以被远程访问。

服务端可以注册多个不同类型的对象(服务),禁止注册具有相同类型的多个对象。

Golang 的 RPC函数 需要满足以下条件才能被远程调用,不然会被忽略:

1
func (t *T) MethodName(argType T1, replyType *T2) error
  • RPC方法名 必须首字母大写;
  • RPC方法 的第一参数是输入参数,第二个参数是返回给客户端的参数(必须是指针类型);
  • RPC方法 必须有一个返回值 error;

将方法放在服务端,注册服务器定义的方法:

  • 服务端需要提供一个套接字服务,监听客户端发送的请求(Listen()),解析获得客户端的请求参数;
  • 服务端会创建一个网络监听器然后调用Accept,服务端接收到连接时调用 rpc 的 ServeConn 方法处理请求请求。或者,对于 HTTP监听器,调用 HandleHTTP 和 http.ListenAndServe;
  • 获取请求参数执行服务器的调用函数,将返回结果交给客户端;

2.2 RPC Service 的 tcp 和 http 实现

示例:

 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
package main

import (
    "net"
    "net/rpc"
)

type HelloService struct{}
func (p *HelloService) Hello(request string, reply *string) error {
    *reply = "hello:" + request
    return nil
}

func main() {
    // tcp 实现
    listen, _ := net.Listen("tcp", ":8000")
    //2. 注册处理逻辑 handler
    _ = rpc.RegisterName("HelloService", &HelloService{})

    //3. 启动服务
    
    // 通过 rpc.ServeConn函数在该TCP链接上为对⽅提供RPC服务。
    // 每Accept一个请求,就创建一个 goroutie 进行处理
    for{
        // 从监听里获取一个连接
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal("Accept error:", err)
        }

        // 将获取的连接交给RPC
        // 前面都是tcp的知识,到这个RPC就接管了
        // 因此,可以认为 rpc 帮我们封装了消息到函数调用的这个逻辑,
        // 提升了工作效率,逻辑比较简洁
        go rpc.ServeConn(conn)
    }
}