下面以一个调用CreatePayment创建支付的情景为例,描述一整个微服务在项目中的工作过程
一次微服务的调用的完整链路如下:
1 | HTTP请求 |
代码生成
一般项目中会有两套接口:
| 定义文件 | 作用 | 通信方向 |
|---|---|---|
| .api | 定义对前端暴露的HTTP接口 | 前端→App网关 |
| .proto | 定义内部gRPC服务 | App网关→paymentRPC |
如CreatePayment的HTTP接口定义:
1 | @handler CreatePayment |
描述:POST /api/mall/orders/:id/pay
protobuf定义:
1 | service PaymentService { |
描述:/payment.PaymentService/CreatePayment
把这两套接口真正连接起来的是网关logic中的代码:
l.svcCtx.PaymentClient.CreatePayment(...)
API生成HTTP网关代码
HTTP接口定义在.api中,执行代码后,goctl会根据文件生成routes.go,handler和types.go
路由结构类似:
1 | { |
再加上统一前缀:rest.WithPrefix("/api/mall")
得到最终接口:POST /api/mall/orders/:id/pay
proto生成RPC代码
RPC定义放在.proto文件中,goctl会生成四类重要的rpc代码:
payment.pb.go:protobuf消息结构体payment_grpc.pb.go:gRPC客户端、服务端接口、方法注册信息payment_service.go:go-zero提供给其他服务使用的客户端包装payment_service_server.go:go-zero服务端适配层
服务建立连接
payment-rpc启动注册服务
payment-rpc入口在services下对应的main.go,负责创建gRPC
Server:
1 | s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { |
pb.RegisterPaymentServiceServer是protobuf生成的代码,用于告诉gRPC
Server,收到对应的gRPC接口时(/payment.PaymentService/CreatePayment),调用对应的PaymentServiceServer.CreatePayment方法
APP网关创建gRPC客户端
APP网关配置中包含PaymentRpc zrpc.RpcClientConf,用于根据配置文件创建客户端,在service_context.go中创建客户端:
1
2
3paymentClient := paymentservice.NewPaymentService(
zrpc.MustNewClient(c.PaymentRpc, tokenPropagator),
)
有两层逻辑:
1 | rpcClient := zrpc.MustNewClient(c.PaymentRpc, tokenPropagator) |
负责根据配置或etcd找到paymentrpc,并连接gRPC,同时处理超时、状态和服务发现,以及执行拦截器
1 | paymentClient := paymentservice.NewPaymentService(rpcClient) |
将底层连接包装成强类型接口
1 | type PaymentService interface { |
最后把客户端保存到依赖容器
1 | type ServiceContext struct { |
这样所有网关logic都可以通过l.svcCtx.PaymentClient调用支付服务,不用每次重新连接
HTTP请求进入网关
假设用户请求:
1 | POST /api/mall/orders/order-001/pay |
路由找到handler并解析
routes.go会将该URL映射到对应handler,位于cmd层的handler中
handler负责解析HTTP参数,通过httpx.Parse()将路径中的参数解析为types文件中之前定义好的请求体(通过api文件定义的)
1 | types.MallCreatePaymentReq{ |
handler调用网关logic
handler通过
1 | l := mall.NewCreatePaymentLogic(ctx, svcCtx) |
进入网关执行CreatePayment逻辑的函数func (l *CreatePaymentLogic) CreatePayment(req *types.MallCreatePaymentReq,) (*types.MallCreatePaymentResp, error)进而执行
- 读取当前登录用户;
- 调用mall-rpc查询订单;
- 校验订单归属;
- 校验订单金额;
- 校验订单状态。
等操作
HTTP转换为Protobuf类型
网关完成校验后调用:
1 | rpcResp, err := l.svcCtx.PaymentClient.CreatePayment( |
这里由HTTP请求类型,通过网关读取订单补充数据转为RPC请求类型
因为请求中只携带了order_id,而发给rpc的数据又添加了userid和amount,前者来自登录状态,后者来自mall-rpc,而不是从前端获取数据
PaymentClient内部
PaymentClient实现于payment_service.go
l.svcCtx.PaymentClient.CreatePayment进入了该Client的实现:
1 | func (m *defaultPaymentService) CreatePayment( |
这里又包装了一层gozero的paymentService客户端,用于统一调用方式、对接zrpc连接、提供更方便的接口和集成服务发现、超时、拦截器等功能,执行RPC请求的是pb.NewPaymentServiceClient(m.cli.Conn())
发送网络请求
protobuf生成的_grpc.pb.go中的CreatePayment方法用于发送网络请求:
1 | func (c *paymentServiceClient) CreatePayment( |
1 | c.cc.Invoke( |
用于拼成gRPC方法,接下来gRPC会根据protobuf定义序列化req,通过gRPC连接发送到payment-rpc,等待服务端响应后将响应反序列化为resp
我们的请求对象如下:
1 | &paymentpb.CreatePaymentReq{ |
protobuf在网络传输时主要依赖字段编号,而不是字段名称,因此字段编号一旦发布就不能随意更改含义
拦截器传递身份
创建客户端时调用了
1 | tokenPropagator := zrpc.WithUnaryClientInterceptor( |
会从当前HTTP请求的context中读取JWT等身份信息,写入gRPC的metadata,链路如下:
1 | HTTP Authorization Header |
因此调用RPC时需要传递当前请求的context:PaymentClient.CreatePayment(l.ctx, req)
RPC服务找到对应方法
服务器收到/payment.PaymentService/CreatePayment后,通过protobuf生成代码中注册的服务描述:
1 | var PaymentService_ServiceDesc = grpc.ServiceDesc{ |
找到对应handler:_PaymentService_CreatePayment_Handler,负责将protobuf中的二进制数据反序列化为*pb.CreatePaymentReq并调用srv.(PaymentServiceServer).CreatePayment(ctx, in)
注意:正式进入业务方法之前,请求会通过AddUnaryInterceptors执行各种中间件和拦截器,负责业务前的日志记录、链路追踪、jwt验证、身份信息注入metadata等,其中支付宝异步被排除认证,因为没有jwt
goctl生成server适配层
gRPC最终调用PaymentServiceServer.CreatePayment(ctx, in),由payment_service_server.go:
1 | func (s *PaymentServiceServer) CreatePayment( |
它本身不写具体业务,主要负责通过gRPC请求构造和调用logic,使得业务进入l.CreatePayment,进而执行具体逻辑
响应回到前端
payment-rpc返回resp后,响应沿着反方向返回:
1 | payment-rpc logic |
网关再把protobuf响应转换为HTTP响应
1 | return &types.MallCreatePaymentResp{ |
最后执行
1 | "out_trade_no": "abc123", |
完整的数据类型转换:
1 | HTTP JSON |