GRPC框架
源代码:https://gitee.com/g_night/grpc-demo
安装
go get github.com/golang/protobuf/proto
go get google.golang.org/grpc
go get github.com/golang/protobuf/protoc-gen-go
go get google.golang.org/protobuf/reflect/protoreflect
go get google.golang.org/protobuf/runtime/protoimpl
- 需要
protoc-gen-go.exe
、protoc.exe
,在源代码的目录中有个protobuf工具
,可以将这些可执行文件放到GOPATH/bin下
入门案例
- 编写中间文件:
创建 hello.proto
:
syntax = "proto3";
package service;
// 用于指定golang package的名字,新版本要求必须包含 /
option go_package = "service/";
message HelloRequest {
int64 id = 1;
}
message HelloResponse {
string name = 1;
}
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse);
}
- 核心服务代码:
运行指令,生成hello.pb.go
protoc --go_out=plugins=grpc:../ hello.proto
# 规则是: --go_out=目录 proto文件
# 注意:这里需要添加 plugins=grpc: 这个用于指定使用的服务
创建文件hello_service.go
,编写核心服务:
package service
import (
"context"
"strconv"
)
// hello服务具体实现
type HelloService struct{}
// 方法名&参数 --> 就在 hello.pb.go 的 HelloServiceServer
func (server *HelloService) SayHello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) {
number := strconv.Itoa(int(req.Id))
resp := &HelloResponse{Name: "hello word no." + number}
return resp, nil
}
- 运行服务:
创建文件server.go
package main
import (
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"grpc-demo/service"
"net"
)
// 具体server代码
func main() {
// 创建新服务
server := grpc.NewServer()
// 注册服务
service.RegisterHelloServiceServer(server, &service.HelloService{})
// 设置端口监听服务
listen, err := net.Listen("tcp", "127.0.0.1:9091")
if err != nil {
grpclog.Fatalf("Failed to listen: %v", err)
}
server.Serve(listen)
}
- 测试文件调用服务:
创建文件client_test.go
:
package main
// 客户端示例
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"grpc-demo/service"
"testing"
)
func TestClient(t *testing.T) {
// 连接 127.0.0.1:9091,地址要一致
conn, err := grpc.Dial("127.0.0.1:9091", grpc.WithInsecure())
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 初始化客户端
c := service.NewHelloServiceClient(conn)
// 构造请求
req := &service.HelloRequest{Id: 1}
// 调用服务
resp, err := c.SayHello(context.Background(), req)
// 服务调用失败
if err != nil {
grpclog.Fatalln(err)
}
fmt.Println("resp.Name = ", resp.Name)
}
首先运行server.go
的main
方法,确保服务开启。然后运行client_test.go
,就能看到显示:
=== RUN TestClient
resp.Name = hello word no.1 # 返回预期结果
--- PASS: TestClient (0.02s)
目录结构(源代码中位于demo1-helloword
文件夹):
步骤总结:
-
编写中间文件:
xxx.proto
-
生成代码:
protoc --go_out=plugins=grpc:../ xxx.proto
-
编写服务核心代码,声明对象,实现方法接口(Name):
func (s *XXX) Name(ctx context.Context, req *XXXRequest) (*XXXResponse, error)
-
运行服务:
// 创建新服务 server := grpc.NewServer() // 注册服务 service.RegisterXXXServer(server, &service.XXXService{}) // 设置端口监听服务 listen, err := net.Listen("tcp", "127.0.0.1:9091") if err != nil { grpclog.Fatalf("Failed to listen: %v", err) } server.Serve(listen)
进阶案例
Token鉴权
我们在demo1-helloword
的基础上加入token
校验,项目文件在源代码的demo2-token
。
-
引入认证包:
go get google.golang.org/grpc/credentials
-
客户端加入
token
信息:编写
customCredential
自定义认证,实现PerRPCCredentials
接口的两个方法:-
GetRequestMetadata(ctx context.Context, uri …string) (map[string]string, error)
-
RequireTransportSecurity() bool
-
package main
// 客户端示例
import (
"context"
"demo2-token/service"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"testing"
)
// customCredential 自定义认证
type customCredential struct{}
// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"token": "real_token",
}, nil
}
// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {
return false
}
func TestClient(t *testing.T) {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
normalClient(opts)
}
// 常规请求操作
func normalClient(opts []grpc.DialOption) {
// 连接 127.0.0.1:9091,地址要一致
// 加入 token
conn, err := grpc.Dial("127.0.0.1:9091", opts...)
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 初始化客户端
c := service.NewHelloServiceClient(conn)
// 构造请求
req := &service.HelloRequest{Id: 1}
// 调用服务
resp, err := c.SayHello(context.Background(), req)
// 服务调用失败
if err != nil {
grpclog.Fatalln(err)
}
fmt.Println("resp.Name = ", resp.Name)
}
- 编写服务端,取出token进行校验:
package main
// 客户端示例
import (
"context"
"demo2-token/service"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"testing"
)
// customCredential 自定义认证
type customCredential struct{}
// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"token": "real_token",
}, nil
}
// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {
return false
}
func TestClient(t *testing.T) {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
normalClient(opts)
}
// 常规请求操作
func normalClient(opts []grpc.DialOption) {
// 连接 127.0.0.1:9091,地址要一致
// 加入token
conn, err := grpc.Dial("127.0.0.1:9091", opts...)
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 初始化客户端
c := service.NewHelloServiceClient(conn)
// 构造请求
req := &service.HelloRequest{Id: 1}
// 调用服务
resp, err := c.SayHello(context.Background(), req)
// 服务调用失败
if err != nil {
grpclog.Fatalln(err)
}
fmt.Println("resp.Name = ", resp.Name)
}
Protobuf语法总结
本文以 “proto3” 为依据!
-
文件以.proto做为文件后缀,除结构定义外的语句以分号结尾
-
结构定义可以包含:message、service、enum
-
末尾的数字表示在序列化数组里面的顺序。可以定义为任何数字(不能重复),不需要总是从1或者0开始
-
Service、Rpc方法名、Message命名采用驼峰命名方式(首字母大写),字段命名采用 小写字母 + 下划线 分隔方式
-
Enum类型命名采用 驼峰命名 ,而字段命名采用 大写字母 + 下划线 分隔方式
message HelloRequest { required string name = 1; } enum MyEnums { FIRST_VALUE = 1; }
-
Protobuf定义了一套基本数据类型,对应规则:
.proto | C++ | Java | Go |
---|---|---|---|
double | double | double | float64 |
float | float | float | float32 |
int32 | int32 | int | int32 |
int64 | int64 | long | int64 |
uint32 | uint32 | int | uint32 |
uint64 | uint64 | long | uint64 |
sint32 | int32 | int | int32 |
sint64 | int64 | long | int64 |
fixed32 | uint32 | int | uint32 |
fixed64 | uint64 | long | uint64 |
sfixed32 | int32 | int | int32 |
sfixed64 | int64 | long | int64 |
bool | bool | boolean | bool |
string | string | String | string |
bytes | string | ByteString | []byte |
Message
message
就相当于结构体,一般定义请求体、响应体。
字段修饰符(其实除非是repeated,否则不用写了):
- Required: 表示是一个必须字段。(proto3弃用,不要使用了!)
- Optional:表示是一个可选字段,可以有选择性的设置或者不设置该字段的值。(默认如此,不用声明)
- Repeated:表示该字段可以包含0~N个元素。可以类比成一个数组的值。
声明细节:
声明的结构体可以嵌套使用:
message SearchResponse {
Result result = 1;
}
message Result {
string url = 1;
string title = 2;
}
如果是内部声明的message类型,则只能内部直接使用
message SearchResponse {
message Result {
string url = 1;
string title = 2;
}
Result result = 1;
}
Enum
- 枚举类型中必须包含值为0的元素且为第一个元素,兼容 proto2 语法。
- 枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
message SearchRequest {
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
}
Corpus corpus = 1;
}
Service
定义RPC服务接口,protocol buffer编译器
将会根据所选择的不同语言生成服务接口代码
service HelloService {
rpc Name(XXXRequest) returns (XXXResponse);
}
Map
如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法:
map<key_type, value_type> map_field = N;
其中key_type
可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type
可以是任意类型。
例如,如果你希望创建一个project的映射,每个Projecct
使用一个string作为key,你可以像下面这样定义:
message SearchRequest {
map<string, string> mp = 3;
}
- 从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用。
- 序列化后的顺序和map迭代器的顺序是乱序的。
- Map的字段不能被repeated修饰。