3、RPC框架解析:gRPC实践

这里抛几个问题:

  • RPC服务目录结构怎么设计?
  • 代码仓库如何处理?
  • 协议仓库如何处理?
  • 对外如何提供接口服务?

3.1、gRPC

​ gRPC是一个现代的开源高性能RPC框架,可以在任何环境下运行。它可以有效地连接数据中心内和数据中心之间的服务,并支持可插拔的负载平衡、跟踪、运行状况检查和身份验证。它也适用于分布式计算、移动应用程序和浏览器连接到后端服务。

​ 并且gRPC的资料很多,实践的文章也很多,后面统一使用gRPC作为我们了解RPC框架的实践。

源码地址

3.2、stub生成

协议我们使用Protobuf,pb的demo.proto协议如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
syntax = "proto3";
package rpc_demo.helloworld;

option go_package = "github.com/ldaysjun/protobuf/helloworld";

// The greeter service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

生成命令

1
protoc --go_out=plugins=grpc:. demo.proto

生成工具protoc的安装可以google,很多资料。

3.3、工程目录

工程目录是一个团队或者工程最应该统一的,有效的提升代码的可读性。这里我们统一定义一个目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
├── cmd                    cmd目录仅存放main函数,不要放入业务逻辑
│   └── main.go
├── conf 配置文件
├── doc 文档
├── go.mod
├── go.sum
├── internal 存放仅供该服务使用的代码,如果需要外部使用,需要挪到pkg目录
│   ├── logic 服务主要逻辑
│   │ ├── imp.go
│   │   └── helloworld.go demo.proto的接口实现
│   ├── model 定义结构体
│   └── utils 工具方法
│   ├── itoa.go
│   └── itoa_test.go
├── makefile
├── pkg 需要被外部使用的代码
│   └── api 提供外部调用该服务的api
│   └── client.go
└── README.md 可以编译成可执行文件工具代码

​ 我们的cmd中存放main函数,internal是我们服务内部逻辑,也就是接口的具体实现,pkg中存放供外部调用的clien,为什么要有这个?使用go mod的开发模式,只需要把你client的仓库地址告诉对方,就可以将所有依赖的代码加载进来,很方便。

3.4、工程实现

根据上面的目录结构,我们来具体实现以下server

3.4.1、接口实现

imp.go文件中,定义了greeter结构体,以及NewGreeter()用来创建greeter结构。

1
2
3
4
5
6
7
8
9
10
11
package logic

import "github.com/ldaysjun/rpc_learn/protobuf/helloworld"

type greeter struct {
}

func NewGreeter() (helloworld.GreeterServer,error){
imp := &greeter{}
return imp,nil
}

helloworld.go实现了SayHello方法。

1
2
3
4
5
6
func (g *greeter) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
rsp := &helloworld.HelloReply{
Message: fmt.Sprintf("hello:%s", req.Name),
}
return rsp, nil
}

3.4.2、启动服务

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

import (
"github.com/ldaysjun/rpc_learn/protobuf/helloworld"
"github.com/ldaysjun/rpc_learn/rpc_demo/internal/logic"
"google.golang.org/grpc"
"log"
"net"
)

const port = ":50052"

func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
return
}
// 初始化server
s := grpc.NewServer()
// 获取接口实现
imp, err := logic.NewGreeter()
if err != nil {
panic(err)
}
// 注册接口
helloworld.RegisterGreeterServer(s, imp)
// 启动服务
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

3.4.3、client api

/pkg/api/client.go实现如下:外部可以直接调用NewGreeterClient生成对应的client

1
2
3
4
5
6
7
8
9
10
var address = "localhost:50052"

func NewGreeterClient() helloworld.GreeterClient {
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
c := helloworld.NewGreeterClient(conn)
return c
}

3.4.5、test

启动服务

1
go run main.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
package main

import (
"context"
"fmt"
"github.com/ldaysjun/rpc_learn/protobuf/helloworld"
"github.com/ldaysjun/rpc_learn/rpc_demo/pkg/api"
"log"
"time"
)

func main() {
// 调用之前实现的NewGreeterClient
client := api.NewGreeterClient()
req := &helloworld.HelloRequest{
Name: "ldaysjun",
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
rsp, err := client.SayHello(ctx, req)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}

fmt.Println("rsp.message = ",rsp.Message)
}

执行结果:

1
rsp.message = hello:ldaysjun

3.5、总结

​ 本篇主要是构建一个服务,约定一些规则,其中也涉及到gRPC的安装,protoc工具的生成,这些随便一搜有很多的资料可供参考。下一篇分析gRPC的详细调用过程。