Protobuf实操
原创大约 7 分钟
metadata传值
当需要传递某些与业务无关的数据,例如Token
、表单验证
等内容的时候,直接将它们放到message
中不太合适,因为这样做不仅不安全,还会过多侵入业务。
这个时候可以用到metadata
,它相当于HTTP中的Header
所起的作用。
有如下hello.proto
文件。
syntax = "proto3";
option go_package=".;hello";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string reply = 1;
}
执行下面的命令生成.go
源代码文件。
> cd ~/workspace-go/go-project/grpc_metadata/proto
> protoc --go_out=. --go-grpc_out=. hello.proto
服务端server.go
文件源码。
package main
import (
"context"
"fmt"
"go-project/grpc_metadata/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"net"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, in *hello.HelloRequest) (*hello.HelloReply, error) {
// 服务端接收metadata
if md, ok := metadata.FromIncomingContext(ctx); ok {
for key, value := range md {
fmt.Println(key, value)
}
}
return &hello.HelloReply{
Reply: "hello " + in.Name,
}, nil
}
func main() {
// 1. 实例化服务
g := grpc.NewServer()
// 2. 注册服务
hello.RegisterGreeterServer(g, &Server{})
// 3. 启动服务
var listener, _ = net.Listen("tcp", ":1234")
_ = g.Serve(listener)
}
客户端client.go
文件源码。
package main
import (
"context"
"fmt"
"go-project/grpc_metadata/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func main() {
// 建立连接
conn, _ := grpc.Dial("localhost:1234", grpc.WithInsecure())
defer conn.Close()
client := hello.NewGreeterClient(conn)
// 利用metadata传值
// 第一种生成metadata的方式
// md := metadata.Pairs("username", "lixingyun", "password", "123456")
// 第二种生成metadata的方式
md := metadata.New(map[string]string{
"username": "lixingyun",
"password": "123456",
})
// 新建一个带有metadata的context
ctx := metadata.NewOutgoingContext(context.Background(), md)
r, _ := client.SayHello(ctx, &hello.HelloRequest{
Name: "lixingyun",
})
fmt.Println(r.Reply)
}
拦截器
如同Java中的AOP机制那样,Go也有自己的Interceptor
拦截器。
有如下hello.proto
文件。
syntax = "proto3";
option go_package=".;hello";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string reply = 1;
}
执行下面的命令生成.go
源代码文件。
> cd ~/workspace-go/go-project/grpc_intercept/proto
> protoc --go_out=. --go-grpc_out=. hello.proto
服务端server.go
文件源码。
package main
import (
"context"
"fmt"
"go-project/grpc_intercept/proto"
"google.golang.org/grpc"
"net"
"time"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, in *hello.HelloRequest) (*hello.HelloReply, error) {
time.Sleep(5 * time.Second)
return &hello.HelloReply{
Reply: "hello " + in.Name,
}, nil
}
func main() {
// 因为 NewServer(opt ...ServerOption) 中可以传递若干服务选项,grpc.UnaryInterceptor() 就是其中可传递的选项之一
// grpc.UnaryInterceptor()的完整形式 -> func UnaryInterceptor(i UnaryServerInterceptor) ServerOption
// UnaryServerInterceptor 是一个闭包 -> type UnaryServerInterceptor func(ctx context.Context, req any, info *UnaryServerInfo, handler UnaryHandler) (resp any, err error)
// 因此只需要实现 func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error) 就能实现自定义拦截器
// 自定义服务端拦截器
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
fmt.Println("拦截到新的请求")
result, err := handler(ctx, req)
fmt.Println("请求已执行")
fmt.Println(result)
return result, err
}
result := grpc.UnaryInterceptor(interceptor)
// 1. 实例化服务
g := grpc.NewServer(result)
// 2. 注册服务
hello.RegisterGreeterServer(g, &Server{})
// 3. 启动服务
var listener, _ = net.Listen("tcp", ":1234")
_ = g.Serve(listener)
}
客户端client.go
文件源码。
package main
import (
"context"
"fmt"
"go-project/grpc_intercept/proto"
"google.golang.org/grpc"
)
func main() {
// grpc.UnaryClientInterceptor() -> func(ctx context.Context, method string, req, reply any, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
// 自定义客户端拦截器
interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
fmt.Println("调用前执行")
if err := invoker(ctx, method, req, reply, cc, opts...); err != nil {
fmt.Println("出错时执行")
return err
}
fmt.Println("调用后执行,耗时:", time.Since(start))
return nil
}
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
// 建立连接
// conn, _ := grpc.Dial("localhost:1234", grpc.WithInsecure())
conn, _ := grpc.Dial("localhost:1234", opts...)
defer conn.Close()
client := hello.NewGreeterClient(conn)
res, _ := client.SayHello(context.Background(), &hello.HelloRequest{
Name: "lixingyun",
},
retry.WithMax(3),
//retry.WithPerRetryTimeout(5),
retry.WithCodes(500, 400),
)
fmt.Println(res.Reply)
}
auth验证
将Metadata
和Interceptor
拦截器结合起来,就可以实现对所有的请求进行拦截验证,这样既不侵入现有业务代码,也能保证请求的安全性。
有如下hello.proto
文件。
syntax = "proto3";
option go_package=".;hello";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string reply = 1;
}
执行下面的命令生成.go
源代码文件。
> cd ~/workspace-go/go-project/grpc_auth/proto
> protoc --go_out=. --go-grpc_out=. hello.proto
服务端server.go
文件源码。
package main
import (
"context"
"fmt"
"go-project/grpc_auth/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"net"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, in *hello.HelloRequest) (*hello.HelloReply, error) {
return &hello.HelloReply{
Reply: "hello " + in.Name,
}, nil
}
func main() {
// grpc.UnaryInterceptor() -> UnaryServerInterceptor -> func(ctx context.Context, req any, info *UnaryServerInfo, handler UnaryHandler) (resp any, err error)
// 自定义服务端拦截器
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
fmt.Println("拦截到新的请求")
// 使用metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return resp, status.Error(codes.Unauthenticated, "无认证信息")
}
var username, password string
if result, ok := md["username"]; ok {
username = result[0]
}
if result, ok := md["password"]; ok {
password = result[0]
}
if username != "lixingyun" {
return resp, status.Error(codes.Unauthenticated, "用户不存在")
}
if password != "123456" {
return resp, status.Error(codes.Unauthenticated, "密码错误")
}
result, err := handler(ctx, req)
fmt.Println("请求已执行")
fmt.Println(result)
return result, err
}
result := grpc.UnaryInterceptor(interceptor)
// 1. 实例化服务
g := grpc.NewServer(result)
// 2. 注册服务
hello.RegisterGreeterServer(g, &Server{})
// 3. 启动服务
var listener, _ = net.Listen("tcp", ":1234")
_ = g.Serve(listener)
}
客户端client.go
文件源码。
package main
import (
"context"
"fmt"
"go-project/grpc_auth/proto"
"google.golang.org/grpc"
)
type CustomerCredentials struct{}
// 实现认证接口
func (c *CustomerCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"username": "lixingyun",
"password": "111",
}, nil
}
func (c *CustomerCredentials) RequireTransportSecurity() bool {
return false
}
func main() {
// grpc.UnaryClientInterceptor() -> func(ctx context.Context, method string, req, reply any, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
// 方式一:自定义客户端拦截器
//interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// start := time.Now()
// fmt.Println("调用前执行")
//
// // 通过metadata传值
// md := metadata.New(map[string]string{
// "username": "lixingyun",
// "password": "123456",
// })
// ctx = metadata.NewOutgoingContext(context.Background(), md)
//
// if err := invoker(ctx, method, req, reply, cc, opts...); err != nil {
// fmt.Println("出错时执行")
// return err
// }
// fmt.Println("调用后执行,耗时:", time.Since(start))
// return nil
//}
// 客户端可选项切片
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
// opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
// 方式二:在go语言中针对这种拦截认证,有一个专门解决方案
// grpc.WithPerRPCCredentials() -> PerRPCCredentials -> GetRequestMetadata() + RequireTransportSecurity()
opts = append(opts, grpc.WithPerRPCCredentials(&CustomerCredentials{}))
// 建立连接
conn, err := grpc.Dial("localhost:1234", opts...)
if err != nil {
panic(err)
}
defer conn.Close()
client := hello.NewGreeterClient(conn)
r, err := client.SayHello(context.Background(), &hello.HelloRequest{
Name: "lixingyun",
})
if err != nil {
panic(err)
}
fmt.Println(r.Reply)
}
表单验证
安装protoc-gen-validate插件。
按照官方给出的提示,执行下面的命令。
# 从github下载:
> cd /home/work
> git clone https://github.com/bufbuild/protoc-gen-validate.git
> cd github.com/envoyproxy/protoc-gen-validate\@v1.0.4
# 或者用go get命令:
> go get -d github.com/envoyproxy/protoc-gen-validate
> cd ~./go/pkg/mod/github.com/envoyproxy/protoc-gen-validate\@v1.0.4
# 然后编译构建:
> sudo make build
它会自动在$GOPATH
环境变量所在目录的/bin
目录中拷贝构建好的protoc-gen-validate
文件。
有如下hello.proto
文件。
syntax = "proto3";
option go_package=".;hello";
import "validate.proto";
service Greeter {
rpc SayHello (Person) returns (Person) {}
}
message Person {
uint64 id = 1 [(validate.rules).uint64.gt = 999];
string email = 2 [(validate.rules).string.email = true];
string name = 3 [(validate.rules).string = {
pattern: "^[A-Za-z]+( [A-Za-z]+)*$",
max_bytes: 256,
}];
Location home = 4 [(validate.rules).message.required = true];
message Location {
double lat = 1 [(validate.rules).double = {gte: -90, lte: 90}];
double lng = 2 [(validate.rules).double = {gte: -180, lte: 180}];
}
}
执行下面的命令生成.go
源代码文件。
> cd ~/workspace-go/go-project/grpc_validate/proto
> protoc --go_out=. --go-grpc_out=. --validate_out="lang=go:". hello.proto
服务端server.go
文件源码。
package main
import (
"context"
"go-project/grpc_validate/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net"
)
type Server struct{}
type Validator interface {
Validate() error
}
func (s *Server) SayHello(ctx context.Context, in *hello.Person) (*hello.Person, error) {
return &hello.Person{}, nil
}
func main() {
//// 验证属性
//person := &hello.Person{
// Id: 1001,
// Email: "123456@qq.com",
// Name: "lixingyun",
// Home: &hello.Person_Location{
// Lat: 50.0,
// Lng: 50.0,
// },
//}
//err := person.Validate()
//if err != nil {
// panic(err)
//}
var interceptor grpc.UnaryServerInterceptor
interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
// 继续处理请求
// 传入Validator接口,让验证满足所有的请求
if r, ok := req.(Validator); ok {
if err := r.Validate(); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
}
return handler(ctx, req)
}
var opts []grpc.ServerOption
opts = append(opts, grpc.UnaryInterceptor(interceptor))
// 1. 实例化一个server
g := grpc.NewServer(opts...)
// 2. 注册处理逻辑
hello.RegisterGreeterServer(g, &Server{})
// 3. 启动server
var listener, _ = net.Listen("tcp", ":1234")
_ = g.Serve(listener)
}
客户端client.go
文件源码。
package main
import (
"context"
"fmt"
"go-project/grpc_validate/proto"
"google.golang.org/grpc"
)
type CustomerCredentials struct{}
func (c *CustomerCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"username": "lixingyun",
"password": "111",
}, nil
}
func (c *CustomerCredentials) RequireTransportSecurity() bool {
return false
}
func main() {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
// 建立连接
conn, err := grpc.Dial("localhost:1234", opts...)
if err != nil {
panic(err)
}
defer conn.Close()
client := hello.NewGreeterClient(conn)
r, err := client.SayHello(context.Background(), &hello.Person{
Id: 90,
Email: "123456@qq.com",
Name: "lixingyun",
Home: &hello.Person_Location{
Lat: 50,
Lng: 50,
},
})
if err != nil {
panic(err)
}
fmt.Println(r)
}
错误码与超时机制
grpc错误码
有如下hello.proto
文件。
syntax = "proto3";
option go_package=".;hello";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
int32 age = 2;
}
message HelloReply {
string reply = 1;
}
执行下面的命令生成源码。
> cd ~/workspace-go/go-project/grpc_error/proto
> protoc --go_out=. --go-grpc_out=. hello.proto
服务端server.go
文件源码。
package main
import (
"context"
"go-project/grpc_error/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net"
"time"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, in *hello.HelloRequest) (*hello.HelloReply, error) {
// 休眠5秒,模拟超时
time.Sleep(5 * time.Second)
return nil, status.Error(codes.DeadlineExceeded, "执行超时")
}
func main() {
// 1. 实例化一个server
g := grpc.NewServer()
// 2. 注册处理逻辑
hello.RegisterGreeterServer(g, &Server{})
// 3. 启动server
var listener, _ = net.Listen("tcp", ":1234")
_ = g.Serve(listener)
}
客户端client.go
文件源码。
package main
import (
"context"
"fmt"
"go-project/grpc_error/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
"time"
)
func main() {
// 建立连接
conn, _ := grpc.Dial("localhost:1234", grpc.WithInsecure())
defer conn.Close()
client := hello.NewGreeterClient(conn)
// 超时3秒则报错
ctx, _ := context.WithTimeout(context.Background(), 3 * time.Second)
_, err := client.SayHello(ctx, &hello.HelloRequest{
Name: "lixingyun",
Age: 18,
})
if err != nil {
code, ok := status.FromError(err)
if !ok {
panic(err)
}
fmt.Println("code.Message() : ", code.Message())
fmt.Println("code.Code() : ", code.Code())
fmt.Println("code.Proto() : ", code.Proto())
fmt.Println("code.Details() : ", code.Details())
fmt.Println("code.String() : ", code.String())
fmt.Println("code.Err() : ", code.Err())
}
}
感谢支持
更多内容,请移步《超级个体》。