Golang微服务(一)
一、protobuf常规使用及踩坑记录
1.类型映射关系及零值
关于protobuf的文档见:Protocol Buffers Documentation (protobuf.dev),文档中可见protobuf与go语言的类型映射关系,以及各种类型的零值。当message中的没有提供指定字段,接收端并不会抛出异常,而是会使用该字段的零值。见以下测试:
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1; // 此处定义请求字段name
}
message HelloResponse {
string greeting = 1;
}
// server端
package main
import (
"context"
"google.golang.org/grpc"
"learn/grpc_things/proto"
"net"
)
type Server struct {
proto.UnimplementedGreeterServer
}
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
return &proto.HelloResponse{
Greeting: "Hello, " + request.Name,
}, nil
}
func main() {
s := grpc.NewServer()
proto.RegisterGreeterServer(s, &Server{})
lis, err := net.Listen("tcp", ":6666")
if err != nil {
panic(err)
}
err = s.Serve(lis)
if err != nil {
panic(err)
}
}
// 客户端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"learn/grpc_things/proto"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:6666", grpc.WithInsecure())
defer conn.Close()
if err != nil {
panic(err)
}
c := proto.NewGreeterClient(conn)
name := proto.HelloRequest{} // 不提供proto文件中定义的Name字段
resp, errResp := c.SayHello(context.Background(), &name)
if errResp != nil {
panic(errResp)
}
fmt.Println(resp.Greeting) // 打印服务端的响应:Hello,
}
2.go_package设置
option go_package设置,格式是"生成结果文件存放路径;go项目中的package"。
“生成结果文件存放路径”可以使用".","..","../"之类的标识,当指定路径不存在时则会创建。go_package或者proto中的package用于分别指定当前文件在其同语言中的(类似)作用域,目的应该是确保标识符的唯一性。
3.protobuf的字段编号
在开发中需要保持服务端和客户端proto文件的完全一致,否则可能出现不容易发现的BUG。例如在proto文件中“string name = 1;”中的1指此字段在某个结构中的编号,protobuf通过自动生成其他语言对应的代码完成了一种在具体结构中字段--编号的对应,这样编码时可以通过“编号+值长度+值”代替“键+值”,可以减少用于实际传输的数据量,在另一端解析时则可以通过编号和“字段--编号”的映射顺序取出值并赋给对应的字段。见以下示例说明:
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
string url = 2;
// 如果另一端的proto文件设置为
// string url = 1;
// string name = 2;
// 此时代码可能可以继续运行并干扰调试
}
message HelloResponse {
string greeting = 1;
}
4.proto文件的import
可以自定义proto文件以供引用或者引用公共proto(google/protobuf/something),在go中引用公共支持的proto结构时引用proto对应的公共包路径即可,例如(import "github.com/golang/protobuf/ptypes/empty")
// base.proto
// 注意,如果想要在go代码中引用base.proto中的结构,该proto文件也需要通过protoc生成go文件
syntax = "proto3";
option go_package = ".;proto";
message Empty {
}
message Pong {
string res = 1;
}
// hello.proto
syntax = "proto3";
import "base.proto"
// 公共支持:import "google/protobuf/empty.proto";
option go_package = ".;proto";
service Greeter {
rpc SayHello(HelloRequest) returns (HelloResponse);
rpc Ping(Empty) returns (Pong);
// 调用公共支持:rpc Ping(google.protobuf.Empty) returns (Pong);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string greeting = 1;
}
5.protobuf的message嵌套
当一个message复杂到需要由其他message构成,而不需要将所有message提取至公共作用域时,可以直接在内部定义。
message HelloResponse {
string greeting = 1;
message Detail {
string info = 1;
string id = 2;
}
repeated Detail data = 2;
}
6.protobuf的枚举、map、timestamp
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello(HelloRequest) returns (HelloResponse);
}
// 枚举
enum Gender {
FEMALE = 0;
MALE = 1;
}
message HelloRequest {
string name = 1;
Gender g = 2;
google.protobuf.Timestamp ts = 3;
// timestamp go代码中可以用timestamppb.New()生成时间戳数据
}
message HelloResponse {
string greeting = 1;
map<string, int32> rank = 2; // map
}
二、gRPC常规使用及踩坑记录
1.metadata
类似http中的header,为当次调用提供信息。
type MD map[string][]string
使用
// 实例化metadata
md := metadata.New(map[string]string{"key": "value", })
md1 := metadata.Pairs("key1", "value1", "key2", "value2", )
// 发送metadata
ctx := metadata.NewOutgoingContext(context.Background(), md)
response, err := client.RPCThings(ctx, requestThings)
// 接收metadata
md, ok := metadata.FromIncomingContext(ctx)
2.gRPC拦截器
一元拦截器
server端
myInterceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
fmt.Println("新请求")
res, err := handler(ctx, req)
fmt.Println("新响应")
return res, err
} // 实现拦截器逻辑函数
itc := grpc.UnaryInterceptor(myInterceptor) // 实例化拦截器
s := grpc.NewServer(itc) // 初始化server时传入拦截器
client端
ci := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc)
fmt.Println("处理用时:", time.Since(start))
return err
} // 实现拦截器逻辑函数
conn, err := grpc.Dial("127.0.0.1:6666", grpc.WithInsecure(), grpc.WithUnaryInterceptor(ci)) // 实例化拦截器并传入拨号函数
3.gRPC Auth认证
-
手动实现
// 利用metadata和拦截器实现认证
// client端,在拦截器中修改ctx,为每次请求增加验证信息
ci := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
md := metadata.New(map[string]string{"token": "right"})
ctx = metadata.NewOutgoingContext(context.Background(), md)
err := invoker(ctx, method, req, reply, cc)
fmt.Println("处理用时:", time.Since(start))
return err
}
conn, err := grpc.Dial("127.0.0.1:6666", grpc.WithInsecure(), grpc.WithUnaryInterceptor(ci))
// server端,在拦截器中获取metadata并校验
myInterceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
fmt.Println("新请求")
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return resp, status.Errorf(codes.Unauthenticated, "认证失败")
}
if authInfo, got := md["token"]; got && strings.Join(authInfo, "") == "right" {
res, err := handler(ctx, req)
return res, err
} else {
return resp, status.Errorf(codes.Unauthenticated, "认证失败")
}
}
itc := grpc.UnaryInterceptor(myInterceptor)
s := grpc.NewServer(itc)
-
使用PerRPCCredentials接口实现
// client端
// 实现PerRPCCredentials接口
type myCdt struct {
}
func (mc myCdt) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{"token": "right"}, nil
}
func (mc myCdt) RequireTransportSecurity() bool {
return false
}
// 拨号
conn, err := grpc.Dial("127.0.0.1:6666", grpc.WithInsecure(), grpc.WithPerRPCCredentials(myCdt{}))
4.gRPC err处理
状态码:grpc/statuscodes.md at master · grpc/grpc · GitHub
// server端返回status err
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.G)
return nil, status.Errorf(codes.InvalidArgument, "参数错误:%s", request.Name)
}
// status.Errorf同样基于status.New()方法,只不过在New()的结果上调用了.Err()方法
// client端解析status err
resp, errResp := c.SayHello(ctx, &name)
if errResp != nil {
statusErr, ok := status.FromError(errResp)
if !ok {
fmt.Println("解析status err失败")
} else {
fmt.Println(statusErr.Message(), statusErr.Code())
}
return
}
5.gRPC超时
重在设置超时的原因:防止服务链中的节点单次任务占用资源过多以及负面作用叠加
常规简单处理:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)