0.1、索引

https://waterflow.link/articles/1665938704477

我们都知道当发起http请求的时候,服务端会返回一些http状态码,不管是成功还是失败。客户端可以根据服务端返回的状态码,判断服务器出现了哪些错误。

我们经常用到的比如下面这些:

  • 200:OK,请求成功
  • 204:NO CONTENT,此请求没有要发送的内容,但标头可能很有用。 用户代理可以用新的更新其缓存的资源头。
  • 400:Bad Request,由于被认为是客户端错误(例如,格式错误的请求语法、无效的请求消息帧或欺骗性请求路由),服务器无法或不会处理请求。
  • 404:Not Found,服务器找不到请求的资源。 在浏览器中,这意味着无法识别 URL。 在 API 中,这也可能意味着端点有效但资源本身不存在。

同样的,当我们调用 gRPC 调用时,客户端会收到带有成功状态的响应或带有相应错误状态的错误。 客户端应用程序需要以能够处理所有潜在错误和错误条件的方式编写。 服务器应用程序要求您处理错误并生成具有相应状态代码的适当错误。

发生错误时,gRPC 会返回其错误状态代码之一以及可选的错误消息,该消息提供错误条件的更多详细信息。 状态对象由一个整数代码和一个字符串消息组成,这些消息对于不同语言的所有 gRPC 实现都是通用的。

gRPC 使用一组定义明确的 gRPC 特定状态代码。 这包括如下状态代码:

  • OK:成功状态,不是错误。
  • CANCELLED:操作被取消,通常是由调用者取消的。
  • DEADLINE_EXCEEDED:截止日期在操作完成之前到期。
  • INVALID_ARGUMENT:客户端指定了无效参数。

详细的状态code、number和解释可以参考这里:https://github.com/grpc/grpc/blob/master/doc/statuscodes.md

1、grpc错误

之前的章节中我们写过关于简单搭建grpc的文章:https://waterflow.link/articles/1665674508275

我们在这个基础上稍微修改一下,看下下面的例子。

首先我们在服务端,修改下代码,在service的Hello方法中加个判断,如果客户端传过来的不是hello,我们我们将返回grpc的标准错误。像下面这样:

func (h HelloService) Hello(ctx context.Context, args *String) (*String, error) {
time.Sleep(time.Second)
// 返回参数不合法的错误
if args.GetValue() != "hello" {
return nil, status.Error(codes.InvalidArgument, "请求参数错误")
}
reply := &String{Value: "hello:" + args.GetValue()}
return reply, nil
}

我们客户端的代码像下面这样:

func unaryRpc(conn *grpc.ClientConn) {
client := helloservice.NewHelloServiceClient(conn)
ctx := context.Background()
md := metadata.Pairs("authorization", "mytoken")
ctx = metadata.NewOutgoingContext(ctx, md)
// 调用Hello方法,并传入字符串hello
reply, err := client.Hello(ctx, &helloservice.String{Value: "hello"})
if err != nil {
log.Fatal(err)
}
log.Println("unaryRpc recv: ", reply.Value)
}

我们开启下服务端,并运行客户端代码:

go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:05:18 unaryRpc recv: hello:hello

可以看到会输出正确的结果。现在我们修改下客户端代码:

func unaryRpc(conn *grpc.ClientConn) {
client := helloservice.NewHelloServiceClient(conn)
ctx := context.Background()
md := metadata.Pairs("authorization", "mytoken")
ctx = metadata.NewOutgoingContext(ctx, md)
// 调用Hello方法,并传入字符串f**k
reply, err := client.Hello(ctx, &helloservice.String{Value: "f**k"})
if err != nil {
log.Fatal(err)
}
log.Println("unaryRpc recv: ", reply.Value)
}

然后运行下客户端代码:

go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:14:13 rpc error: code = InvalidArgument desc = 请求参数错误
exit status 1

可以看到我们获取到了服务端返回的错误。

2、获取grpc错误类型

有时候客户端通过服务端返回的不同错误类型去做一些具体的处理,这个时候客户端可以这么写:

func unaryRpc(conn *grpc.ClientConn) {
client := helloservice.NewHelloServiceClient(conn)
ctx := context.Background()
md := metadata.Pairs("authorization", "mytoken")
ctx = metadata.NewOutgoingContext(ctx, md)
reply, err := client.Hello(ctx, &helloservice.String{Value: "f**k"})
if err != nil {
fromError, ok := status.FromError(err)
if !ok {
log.Fatal(err)
}
// 判断服务端返回的是否是指定code的错误
if fromError.Code() == codes.InvalidArgument {
log.Fatal("invalid arguments")
}
}
log.Println("unaryRpc recv: ", reply.Value)
}

我们可以看下status.FromError的返回结果:

  • 如果 err 是由这个包产生的或者实现了方法 GRPCStatus() *Status,返回相应的状态。
  • 如果 err 为 nil,则返回带有代码的状态。OK 并且没有消息。
  • 否则,err 是与此包不兼容的错误。 在这个情况下,返回一个 Status 结构是 code.Unknown 和 err 的 Error() 消息,并且ok为false。

我们重新执行下客户端代码:

go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:26:11 invalid arguments
exit status 1

可以看到,当服务端返回的是codes.InvalidArgument错误时,我们重新定义了错误。

3、获取grpc错误更详细的信息

当我们服务端返回grpc错误时,我们想带上一些自定义的详细错误信息,这个时候就可以像下面这样写:

func (h HelloService) Hello(ctx context.Context, args *String) (*String, error) {
time.Sleep(time.Second)
if args.GetValue() != "hello" {
errorStatus := status.New(codes.InvalidArgument, "请求参数错误")
details, err := errorStatus.WithDetails(&errdetails.BadRequest_FieldViolation{
Field: "string.value",
Description: fmt.Sprintf("expect hello, get %s", args.GetValue()),
})
if err != nil {
return nil, errorStatus.Err()
}
return nil, details.Err()
}
reply := &String{Value: "hello:" + args.GetValue()}
return reply, nil
}

我们重点看下WithDetails方法:

  • 该方法传入一个proto.Message类型的数组,Message是一个protocol buffer的消息
  • 返回一个新Status,并将提供的详细信息消息附加到Status
  • 如果遇到任何错误,则返回 nil 和遇到的第一个错误

然后我们修改下客户端代码:

func unaryRpc(conn *grpc.ClientConn) {
client := helloservice.NewHelloServiceClient(conn)
ctx := context.Background()
md := metadata.Pairs("authorization", "mytoken")
ctx = metadata.NewOutgoingContext(ctx, md)
reply, err := client.Hello(ctx, &helloservice.String{Value: "f**k"})
if err != nil {
fromError, ok := status.FromError(err)
if !ok {
log.Fatal(err)
}
if fromError.Code() == codes.InvalidArgument {
// 获取错误的详细信息,因为详细信息返回的是数组,所以这里我们需要遍历
for _, detail := range fromError.Details() {
detail = detail.(*proto.Message)
log.Println(detail)
}
log.Fatal("invalid arguments")
}
}
log.Println("unaryRpc recv: ", reply.Value)
}

接着重启下服务端,运行下客户端代码:

go run helloclient/main.go
invoker request time duration: 1
2022/10/16 23:58:51 field:"string.value" description:"expect hello, get f**k"
2022/10/16 23:58:51 invalid arguments
exit status 1

可以看到详细信息打印出来了。

4、定义标准错误之外的错误

现实中我们可能会有这样的要求:

  • 当grpc服务端是自定义错误时,客户端返回自定义错误
  • 当grpc服务端返回的是标准错误时,客户端返回系统错误

我们可以创建一个自定义测错误类:

package xerr

import (
"fmt"
) /**
常用通用固定错误
*/
type CodeError struct {
errCode uint32
errMsg string
} //返回给前端的错误码
func (e *CodeError) GetErrCode() uint32 {
return e.errCode
} //返回给前端显示端错误信息
func (e *CodeError) GetErrMsg() string {
return e.errMsg
} func (e *CodeError) Error() string {
return fmt.Sprintf("ErrCode:%d,ErrMsg:%s", e.errCode, e.errMsg)
}

然后grpc服务端实现一个拦截器,目的是把自定义错误转换成grpc错误:

func LoggerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {

	resp, err = handler(ctx, req)
if err != nil {
causeErr := errors.Cause(err) // err类型
if e, ok := causeErr.(*xerr.CodeError); ok { //自定义错误类型 //转成grpc err
err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
} } return resp, err
}

然后客户端处理错误代码的部分修改如下:

//错误返回

		causeErr := errors.Cause(err)                // err类型
if e, ok := causeErr.(*xerr.CodeError); ok { //自定义错误类型
//自定义CodeError
errcode = e.GetErrCode()
errmsg = e.GetErrMsg()
} else {
errcode := uint32(500)
errmsg := "系统错误"
}

其中用到的errors.Cause的作用就是递归获取根错误。

这其实就是go-zero中实现自定义错误的方式,大家可以自己写下试试吧。

grpc错误处理的更多相关文章

  1. 微服务从代码到k8s部署应有尽有系列(十、错误处理)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  2. 一篇文章带你搞懂 etcd 3.5 的核心特性

    作者 唐聪,腾讯云资深工程师,极客时间专栏<etcd实战课>作者,etcd活跃贡献者,主要负责腾讯云大规模k8s/etcd平台.有状态服务容器化.在离线混部等产品研发设计工作. etcd ...

  3. 航空概论(历年资料,引之百度文库,PS:未调格式,有点乱)

    航空航天尔雅 选择题1. 已经实现了<天方夜谭>中的飞毯设想.—— A——美国2. 地球到月球大约—— C 38 万公里3. 建立了航空史上第一条定期空中路线—— B——德国4. 对于孔明 ...

  4. grpc编译错误解决

    berli@berli-VirtualBox:~/grpc$ make [MAKE]    Generating cache.mk [C]       Compiling src/core/lib/s ...

  5. 用Java开发gRPC服务的例子分析

    本文的代码例子来自:https://github.com/grpc/grpc-java  定义服务 这一步与其他语言完全一样,需要定义gRPC的服务.方法.request和response的类型. 完 ...

  6. 编写一个go gRPC的服务

    前置条件: 获取 gRPC-go 源码 $ go get google.golang.org/grpc 简单例子的源码位置: $ cd $GOPATH/src/google.golang.org/gr ...

  7. Google 高性能 RPC 框架 gRPC 1.0.0 发布(附精彩评论)

    gRPC是一个高性能.开源.通用的RPC框架,面向移动和HTTP/2设计,是由谷歌发布的首款基于Protocol Buffers的RPC框架. gRPC基于HTTP/2标准设计,带来诸如双向流.流控. ...

  8. 编译gRPC

    编译gRPC 目录 一.概述 二.编译gRPC 三.C#中使用gRPC 四.C++中使用gRPC 无论通过哪种语言调用gRPC,都必须要编译gRPC,因为生成proto访问类时,除了产生标准的数据定义 ...

  9. 初识google多语言通信框架gRPC系列(二)编译gRPC

    目录 一.概述 二.编译gRPC 三.C#中使用gRPC 四.C++中使用gRPC 无论通过哪种语言调用gRPC,都必须要编译gRPC,因为生成proto访问类时,除了产生标准的数据定义类之外,还需要 ...

随机推荐

  1. BMP位图之4位位图(二)

    起始结构 typedef struct tagBITMAPFILEHEADER { WORD bfType; //类型名,字符串"BM", DWORD bfSize; //文件大小 ...

  2. Dubbo源码(七) - 集群

    前言 本文基于Dubbo2.6.x版本,中文注释版源码已上传github:xiaoguyu/dubbo 集群(cluster)就是一组计算机,它们作为一个总体向用户提供一组网络资源.这些单个的计算机系 ...

  3. Apache DolphinScheduler 荣获国外知名媒体采访

    Apache DolphinScheduler 毕业的消息被北美科技媒体 TheNewStack 关注并邀请Apache DolphinScheduler PMC chair 代立冬 进行相关采访. ...

  4. 用VS Code搞Qt6:编译附加模块

    上一次水文中,老周所介绍的是编译 Qt 的基础模块-- qtbase.一次性编译所有代码可以一劳永逸,但体积相当大,编译时间较长,CPU负载大发热大,风扇转得猛,电费交得多.因此老周更喜欢分开来编译. ...

  5. Luogu1993 小K的农场 (差分约束)

    \(if \ a - b <= c, AddEdge(b, a, c)\) Be careful, MLE is not good. #include <cstdio> #inclu ...

  6. Redis 14 发布订阅

    参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 概述 Redi ...

  7. IDEA Git缓慢

    有的公司电脑会强制安装一些特定的杀毒软件或者监控软件. 在安装后,我们的 IDEA 可能会出现 Git 相关操作非常缓慢的情况. 虽然用 Git 命令操作不受影响,但终究没有可视化界面直观方便. 解决 ...

  8. docker compose搭建redis7.0.4高可用一主二从三哨兵集群并整合SpringBoot【图文完整版】

    一.前言 redis在我们企业级开发中是很常见的,但是单个redis不能保证我们的稳定使用,所以我们要建立一个集群. redis有两种高可用的方案: High availability with Re ...

  9. CAD二次开发---关于JoinEntity出现eNotApplicable的问题

    作者在使用JoinEntity时出现eNotApplicable的问题,查阅了Autodesk论坛的相关帖子,发现大多数人都有遇到这个问题,但没有找到合适的解决方法,可能原因是进行Join时两Curv ...

  10. 刷题记录:Codeforces Round #725 (Div. 3)

    Codeforces Round #725 (Div. 3) 20210704.网址:https://codeforces.com/contest/1538. 感觉这个比上一个要难. A 有一个n个数 ...