grpc響應(yīng)碼設(shè)計(jì)-轉(zhuǎn)載

原文地址:https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server/

1. 服務(wù)端響應(yīng)的現(xiàn)狀

做后端服務(wù)的開發(fā)人員對(duì)錯(cuò)誤處理總是很敏感的,因此在做服務(wù)的響應(yīng)(response/reply)設(shè)計(jì)時(shí)總是會(huì)很慎重。

如果后端服務(wù)選擇的是HTTP API(rest api),比如json over http,API響應(yīng)(Response)中大多會(huì)包含如下信息:

{
    "code": 0,
    "msg": "ok",
    "payload" : {
        ... ...
    }
}

在這個(gè)http api的響應(yīng)設(shè)計(jì)中,前兩個(gè)狀態(tài)標(biāo)識(shí)這個(gè)請(qǐng)求的響應(yīng)狀態(tài)。這個(gè)狀態(tài)由一個(gè)狀態(tài)代碼(code)與狀態(tài)信息(msg)組成。狀態(tài)信息是對(duì)狀態(tài)代碼所對(duì)應(yīng)錯(cuò)誤原因的詳細(xì)詮釋。只有當(dāng)狀態(tài)為正常時(shí)(code = 0),后面的payload才具有意義。payload顯然是在響應(yīng)中意圖傳給客戶端的業(yè)務(wù)信息。

這樣的服務(wù)響應(yīng)設(shè)計(jì)是目前比較常用且成熟的方案,理解起來也十分容易。

好,現(xiàn)在我們看看另外一大類服務(wù):采用RPC方式提供的服務(wù)。我們還是以使用最為廣泛的gRPC為例。在gRPC中,一個(gè)service的定義如下(我們借用一下grpc-go提供的helloworld示例):

// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto
package helloworld;

// The greeting 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;
}

grpc對(duì)于每個(gè)rpc方法(比如SayHello)都有約束,只能有一個(gè)輸入?yún)?shù)和一個(gè)返回值。這個(gè).proto定義通過protoc生成的go代碼變成了這樣:

// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld_grpc.pb.go
type GreeterServer interface {
    // Sends a greeting
    SayHello(context.Context, *HelloRequest) (*HelloReply, error)
    ... ...
}

我們看到對(duì)于SayHello RPC方法,protoc生成的go代碼中,SayHello方法的返回值列表中多了一個(gè)Gopher們熟悉的error返回值。對(duì)于已經(jīng)習(xí)慣了HTTP API那套響應(yīng)設(shè)計(jì)的gopher來說,現(xiàn)在問題來了! http api響應(yīng)中表示響應(yīng)狀態(tài)的code與msg究竟是定義在HelloReply這個(gè)業(yè)務(wù)響應(yīng)數(shù)據(jù)中,還是通過error來返回的呢?這個(gè)grpc官方文檔似乎也沒有明確說明(如果各位看官找到位置,可以告訴我哦)。

2. gRPC服務(wù)端響應(yīng)設(shè)計(jì)思路

我們先不急著下結(jié)論!我們繼續(xù)借用helloworld這個(gè)示例程序來測試一下當(dāng)error返回值不為nil時(shí)客戶端的反應(yīng)!先改一下greeter_server的代碼:

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.GetName())
    return &pb.HelloReply{Message: "Hello " + in.GetName()}, errors.New("test grpc error")
}

在上面代碼中,我們故意構(gòu)造一個(gè)錯(cuò)誤并返回給調(diào)用該方法的客戶端。我們來運(yùn)行一下這個(gè)服務(wù)并啟動(dòng)greeter_client來訪問該服務(wù),在客戶端側(cè),我們得到的結(jié)果如下:

2021/09/20 17:04:35 could not greet: rpc error: code = Unknown desc = test grpc error

從客戶端的輸出結(jié)果中,我們看到了我們自定義的錯(cuò)誤的內(nèi)容(test grpc error)。但我們還發(fā)現(xiàn)錯(cuò)誤輸出的內(nèi)容中還有一個(gè)”code = Unknown”的輸出,這個(gè)code是從何而來呢?似乎grpc期待的error形式是包含code與desc的形式。

這時(shí)候就不得不查看一下gprc-go(v1.40.0)的參考文檔了!在grpc-go的文檔中我們發(fā)現(xiàn)幾個(gè)被DEPRECATED的與Error有關(guān)的函數(shù):

image.png

在這幾個(gè)作廢的函數(shù)的文檔中都提到了用status包的同名函數(shù)替代。那么這個(gè)status包又是何方神圣?我們翻看grpc-go的源碼,終于找到了status包,在包說明的第一句中我們就找到了答案:

Package status implements errors returned by gRPC.

原來status包實(shí)現(xiàn)了上面grpc客戶端所期望的error類型。那么這個(gè)類型是什么樣的呢?我們逐步跟蹤代碼:

在grpc-go/status包中我們看到如下代碼:

type Status = status.Status

// New returns a Status representing c and msg.
func New(c codes.Code, msg string) *Status {
    return status.New(c, msg)
}

status包使用了internal/status包中的Status,我們?cè)賮砜磇nternal/status包中Status結(jié)構(gòu)的定義:

// internal/status
type Status struct {
    s *spb.Status
}

// New returns a Status representing c and msg.
func New(c codes.Code, msg string) *Status {
    return &Status{s: &spb.Status{Code: int32(c), Message: msg}}
}

internal/status包的Status結(jié)構(gòu)體組合了一個(gè)*spb.Status類型(google.golang.org/genproto/googleapis/rpc/status包中的類型)的字段,繼續(xù)追蹤spb.Status:

// https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status
type Status struct {
    // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
    Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
    // A developer-facing error message, which should be in English. Any
    // user-facing error message should be localized and sent in the
    // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
    Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
    // A list of messages that carry the error details.  There is a common set of
    // message types for APIs to use.
    Details []*anypb.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"`
    // contains filtered or unexported fields
}

我們看到最后的這個(gè)Status結(jié)構(gòu)包含了Code與Message。這樣一來,grpc的設(shè)計(jì)意圖就很明顯了,它期望開發(fā)者在error這個(gè)返回值中包含rpc方法的響應(yīng)狀態(tài),而自定義的響應(yīng)結(jié)構(gòu)體只需包含業(yè)務(wù)所需要的數(shù)據(jù)即可。我們用一幅示意圖來橫向建立一下http api與rpc響應(yīng)的映射關(guān)系:


image.png

有了這幅圖,再面對(duì)如何設(shè)計(jì)grpc方法響應(yīng)這個(gè)問題時(shí),我們就胸有成竹了!

grpc-go在codes包中定義了grpc規(guī)范要求的10余種錯(cuò)誤碼:

const (
    // OK is returned on success.
    OK Code = 0

    // Canceled indicates the operation was canceled (typically by the caller).
    //
    // The gRPC framework will generate this error code when cancellation
    // is requested.
    Canceled Code = 1

    // Unknown error. An example of where this error may be returned is
    // if a Status value received from another address space belongs to
    // an error-space that is not known in this address space. Also
    // errors raised by APIs that do not return enough error information
    // may be converted to this error.
    //
    // The gRPC framework will generate this error code in the above two
    // mentioned cases.
    Unknown Code = 2

    // InvalidArgument indicates client specified an invalid argument.
    // Note that this differs from FailedPrecondition. It indicates arguments
    // that are problematic regardless of the state of the system
    // (e.g., a malformed file name).
    //
    // This error code will not be generated by the gRPC framework.
    InvalidArgument Code = 3

    ... ...

    // Unauthenticated indicates the request does not have valid
    // authentication credentials for the operation.
    //
    // The gRPC framework will generate this error code when the
    // authentication metadata is invalid or a Credentials callback fails,
    // but also expect authentication middleware to generate it.
    Unauthenticated Code = 16

在這些標(biāo)準(zhǔn)錯(cuò)誤碼之外,我們還可以擴(kuò)展定義自己的錯(cuò)誤碼與錯(cuò)誤描述。

3. 服務(wù)端如何構(gòu)造error與客戶端如何解析error

前面提到,gRPC服務(wù)端采用rpc方法的最后一個(gè)返回值error來承載應(yīng)答狀態(tài)。google.golang.org/grpc/status包為構(gòu)建客戶端可解析的error提供了一些方便的函數(shù),我們看下面示例(基于上面helloworld的greeter_server改造):

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.GetName())
    return nil, status.Errorf(codes.InvalidArgument, "you have a wrong name: %s", in.GetName())
}

status包提供了一個(gè)類似于fmt.Errorf的函數(shù),我們可以很方便的構(gòu)造一個(gè)帶有code與msg的error實(shí)例并返回給客戶端。

而客戶端同樣可以通過status包提供的函數(shù)將error中攜帶的信息解析出來,我們看下面代碼:

ctx, _ := context.WithTimeout(context.Background(), time.Second)
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "tony")})
if err != nil {
    errStatus := status.Convert(err)
    log.Printf("SayHello return error: code: %d, msg: %s\n", errStatus.Code(), errStatus.Message())
}
log.Printf("Greeting: %s", r.GetMessage())

我們看到:通過status.Convert函數(shù)可以很簡答地將rpc方法返回的不為nil的error中攜帶的信息提取出來。

4. 空應(yīng)答

gRPC的proto文件規(guī)范要求每個(gè)rpc方法的定義中都必須包含一個(gè)返回值,返回值不能為空,比如上面helloworld項(xiàng)目的.proto文件中的SayHello方法:

rpc SayHello (HelloRequest) returns (HelloReply) {}

如果去掉HelloReply這個(gè)返回值,那么protoc在生成代碼時(shí)會(huì)報(bào)錯(cuò)!

但是有些方法本身不需要返回業(yè)務(wù)數(shù)據(jù),那么我們就需要為其定義一個(gè)空應(yīng)答消息,比如:

message Empty {

}

考慮到每個(gè)項(xiàng)目在遇到空應(yīng)答時(shí)都要重復(fù)造上面Empty message定義的輪子,grpc官方提供了一個(gè)可被復(fù)用的空message:

// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/empty.proto

// A generic empty message that you can re-use to avoid defining duplicated
// empty messages in your APIs. A typical example is to use it as the request
// or the response type of an API method. For instance:
//
//     service Foo {
//       rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
//     }
//
// The JSON representation for `Empty` is empty JSON object `{}`.
message Empty {}

我們只需在.proto文件中導(dǎo)入該empty.proto并使用Empty即可,比如下面代碼:

// xxx.proto

syntax = "proto3";

import "google/protobuf/empty.proto";

service MyService {
    rpc MyRPCMethod(...) returns (google.protobuf.Empty);
}

當(dāng)然google.protobuf.Empty不僅僅適用于空響應(yīng),也適合空請(qǐng)求,這個(gè)就留給大家可自行完成吧。

5. 小結(jié)

本文我們講述了gRPC服務(wù)端響應(yīng)設(shè)計(jì)的相關(guān)內(nèi)容,最主要想說的是直接使用gRPC生成的rpc方面的error返回值來表示rpc調(diào)用的響應(yīng)狀態(tài),不要再在自定義的Message結(jié)構(gòu)中重復(fù)放入code與msg字段來表示響應(yīng)狀態(tài)了。

btw,做API的錯(cuò)誤設(shè)計(jì),google的這份API設(shè)計(jì)方面的參考資料是十分好的。有時(shí)間一定要好好讀讀哦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容