Go语言实现rpc和grpc

RPC介绍

RPC(Remote Procedure Call)是一种远程调用协议,能够使应用像调用本地方法一样调用远程的过程或服务。可以应用在分布式服务、分布式计算、远程调用等场景。

Go实现rpc-server

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

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

type World struct {

}

func (this *World) HelloWorld (name string,resp *string) error {
*resp = name + " 你好!"
return nil
}

func main() {
//注册rpc服务
err := rpc.RegisterName("hello",new(World))
if err != nil {
fmt.Println("注册rpc服务失败!,err",err)
return
}

//设置监听
listener,err := net.Listen("tcp","127.0.0.1:8800")
if err != nil {
fmt.Println("net.Listen err:",err)
return
}
defer listener.Close()
fmt.Println("开始监听....")
//建立链接
conn,err := listener.Accept()
if err != nil {
fmt.Println("Accept() err:",err)
return
}
defer conn.Close()
fmt.Println("链接成功...")
//绑定服务
rpc.ServeConn(conn)
}

首先我们要实定义类对象并绑定一个类方法。在server中的main函数中第一步需要实现的是注册RPC服务,绑定对象方法。RegisterName函数的第一个参数是服务名,是一个字符串类型。第二个参数对应rpc对象。方法必须是导出的,必须有两个参数,方法第二个参数必须是指针。方法只有一个error接口类型的返回值。第二步是设置监听,这里我们使用net.listen监听本地tcp下的8800端口。第三步是建立链接,使用Accept会返回一个用于通信的connect。最后绑定服务即可。这里注意对于listener和conn在使用完后一定要close。

Go实现rpc-client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"net/rpc"
)

func main01() {
//用rpc链接服务器
conn,err := rpc.Dial("tcp","127.0.0.1:8800")
if err != nil {
fmt.Println("Dial err:",err)
return
}
defer conn.Close()
//调用远程函数
var reply string
err = conn.Call("hello.HelloWorld","elssm",&reply)
if err != nil {
fmt.Println("Call err:",err)
return
}
fmt.Println(reply)
}

client端第一步先通过Dial链接服务器。链接成功之后通过Call调用远程函数,对于Call的第一个参数是服务名.方法名,第二个是传入参数,第三个是接收参数,因为在server端类方法的第二个参数是指针类型。所以这里Call的第三个参数要传地址用于接收函数返回值。

Json-rpc

使用nc -l 127.0.0.1 8800充当服务器发起通信产生乱码

1

这是因为rpc使用了go语言特有的数据序列化gob,其他编程语言不能解析。因此我们可以使用通用的序列化,反序列化例如json、protobuf来解决。

修改上面client端的链接服务器代码如下即可

1
conn,err := jsonrpc.Dial("tcp","127.0.0.1:8800")

2

RPC封装

服务端封装
1
2
3
4
5
6
7
8
9
10
//创建接口,在接口中定义方法原型
type MyInterface interface {
HelloWorld(string,*string) error

}

//封装注册服务方法
func RegisterService(i MyInterface) {
rpc.RegisterName("hello",i)
}
客户端封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义类
type Myclient struct {
c *rpc.Client
}

//初始化客户端
func InitClient(addr string) Myclient {
conn,_:= jsonrpc.Dial("tcp",addr)
return Myclient{c:conn}
}

//绑定类方法
func (this *Myclient) HelloWorld(a string,b *string) error {
return this.c.Call("hello.HelloWorld",a,b)
}

protobuf

protobuf是Google开发出来的一个语言无关、平台无关的数据序列化工具,在rpc或tcp通信等很多场景都可以使用。通俗来讲,如果客户端和服务端使用的是不同的语言,那么在服务端定义一个数据结构,通过protobuf转化为字节流,再传送到客户端解码,就可以得到对应的数据结构。如果实现rpc我们可以使用json进行序列化。如果使用protobuf做序列化就需要使用gRPC。

protobuf语法:https://www.bookstack.cn/read/topgoer/abb9896b6124ea54.md

安装protobuf

macos使用brew进行安装protobuf编译器

1
brew install protobuf
protobuf编写
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
//默认是proto2
syntax = "proto3";

//指定所在包名
package pb;

enum Week {
Monday = 0;
Turesday = 1;
}

//定义消息体
message Student {
int32 age = 1;
string name = 2;
People p = 3;
repeated int32 score = 4; //数组
//枚举
Week w = 5;
//联合体
oneof data {
string teacher = 6;
string class = 7;

}
}

//消息体可以嵌套
message People {
int32 weight = 1;
}
编译protobuf
1
protoc --go_out=./ *.proto

在当前目录下会生成myproto.pb.go文件

consul

Consul是一个服务网格解决方案,提供了一个功能齐全的控制平面,具有服务发现、配置和分段功能。 这些功能中的每一项都可以根据需要单独使用,也可以一起使用来构建一个完整的服务网格。 Consul需要一个数据平面,并支持代理和原生集成模型。

安装consul
1
brew install consul

macos在/etc目录下创建consul.d文件夹,进入consul.d文件夹下。

创建json文件

6

启动consul
1
consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=n1 -bind=127.0.0.1 -ui -rejoin -config-dir=/etc/consul.d/ -client 0.0.0.0

3

浏览器查看

访问127.0.0.1:8500查看节点信息

4

命令行查看
1
curl -s 127.0.0.1:8500/v1/catalog/service/bj38

5

健康检查
1
sudo /etc/consul.d/web.json

写入配置文件信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"service":{
"name":"bj38",
"tags":["itcast","itheima"],
"port":8800,
"check":{
"id":"api",
"name":"itcast check",
"http":"http://127.0.0.1:8800",
"interval":"5s",
"timeout":"1s"
}
}
}
启动consul
1
consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=n1 -bind=127.0.0.1 -ui -rejoin -config-dir=/etc/consul.d/ -client 0.0.0.0

7

gRPC介绍

官方中文文档:http://doc.oschina.net/grpc?t=58008

Go实现gRPC

首先创建一个简单的go版本的protobuf文件,该文件在项目的pb文件夹中,命名为person.proto,项目名为day02

1
2
3
4
5
6
7
8
9
10
11
12
syntax = "proto3";

//package pb;
option go_package = "/;pb";

message Person {
string name = 1;
int32 age=2;
}
service hello {
rpc sayHello(Person) returns (Person);
}

这里遇到了一个很坑的问题,就是要将上面的package pb;改为option go_package = "/;pb";,这个问题折磨了我有半个下午。大概报错如下图

8

一开始根本不知道问题出在哪。baidu/google了很久都没有找到解决办法。我想这可能跟protoc-gen-go的版本有关。来来回回安装protoc-gen-go,无果。最后终于在github上找到了解决办法。

9

生成.go文件
1
protoc --go_out=plugins=grpc:./ *.proto
gRPC-server
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
package main

import (
"context"
"fmt"
"google.golang.org/grpc"
"day02/pb"
"net"
)

//定义类
type Children struct {

}

//绑定类方法,实现接口
func (this *Children) SayHello(ctx context.Context,p *pb.Person) (*pb.Person,error) {
p.Name = "hello " + p.Name
return p,nil
}

func main() {
// 初始化grpc对象
grpcServer := grpc.NewServer()

//注册服务
pb.RegisterHelloServer(grpcServer,new(Children))

//设置监听
listener,err := net.Listen("tcp","127.0.0.1:8800")
if err != nil {
fmt.Println("Listen err:",err)
return
}
fmt.Println("开始监听....")
defer listener.Close()

//启动服务
grpcServer.Serve(listener)
}

10

gRPC-client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"context"
"fmt"
"google.golang.org/grpc"
"day02/pb"
)

func main() {
//链接服务
grocConn,_ := grpc.Dial("127.0.0.1:8800",grpc.WithInsecure())

//初始化grpc客户端
grpcClient := pb.NewHelloClient(grocConn)

var person pb.Person
person.Name = "elssm"
person.Age = 22
//调用远程函数
p,err := grpcClient.SayHello(context.TODO(),&person)
fmt.Println(p,err)
}

11