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)
浙公網安備 33010602011771號