下面以一个调用CreatePayment创建支付的情景为例,描述一整个微服务在项目中的工作过程
一次微服务的调用的完整链路如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
HTTP请求
↓
routes.go
↓
CreatePaymentHandler
↓
CreatePaymentLogic
↓
svcCtx.PaymentClient
↓
goctl生成的paymentservice客户端包装
↓
protobuf生成的gRPC Client
↓
gRPC序列化 + HTTP/2网络传输
↓
payment-rpc的gRPC Server
↓
protobuf生成的Server Handler
↓
goctl生成的PaymentServiceServer
↓
CreatePaymentLogic
↓
数据库、支付宝
↓
响应沿原路返回代码生成
一般项目中会有两套接口:
| 定义文件 | 作用 | 通信方向 |
|---|---|---|
| .api | 定义对前端暴露的HTTP接口 | 前端→App网关 |
| .proto | 定义内部gRPC服务 | App网关→paymentRPC |
如CreatePayment的HTTP接口定义:
1
2
3
@handler CreatePayment
post /orders/:id/pay (MallCreatePaymentReq) returns
(MallCreatePaymentResp)描述:POST /api/mall/orders/:id/pay
protobuf定义:
1
2
3
service PaymentService {
rpc CreatePayment(CreatePaymentReq) returns (CreatePaymentResp);
}描述:/payment.PaymentService/CreatePayment
把这两套接口真正连接起来的是网关logic中的代码:
l.svcCtx.PaymentClient.CreatePayment(...)
API生成HTTP网关代码
HTTP接口定义在.api中,执行代码后,goctl会根据文件生成routes.go,handler和types.go
路由结构类似:
1
2
3
4
5
{
Method: http.MethodPost,
Path: "/orders/:id/pay",
Handler: mall.CreatePaymentHandler(serverCtx),
}再加上统一前缀: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
2
3
4
5
6
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
pb.RegisterPaymentServiceServer(
grpcServer,
paymentserviceServer.NewPaymentServiceServer(ctx),
)
})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
2
3
4
5
6
7
type PaymentService interface {
CreatePayment(
ctx context.Context,
in *CreatePaymentReq,
opts ...grpc.CallOption,
) (*CreatePaymentResp, error)
}最后把客户端保存到依赖容器
1
2
3
type ServiceContext struct {
PaymentClient paymentservice.PaymentService
}这样所有网关logic都可以通过l.svcCtx.PaymentClient调用支付服务,不用每次重新连接
HTTP请求进入网关
假设用户请求:
1
2
POST /api/mall/orders/order-001/pay
Authorization: Bearer <token>路由找到handler并解析
routes.go会将该URL映射到对应handler,位于cmd层的handler中
handler负责解析HTTP参数,通过httpx.Parse()将路径中的参数解析为types文件中之前定义好的请求体(通过api文件定义的)
1
2
3
types.MallCreatePaymentReq{
Id: "order-001",
}handler调用网关logic
handler通过
1
2
l := mall.NewCreatePaymentLogic(ctx, svcCtx)
resp, err := l.CreatePayment(in)进入网关执行CreatePayment逻辑的函数func (l *CreatePaymentLogic) CreatePayment(req *types.MallCreatePaymentReq,) (*types.MallCreatePaymentResp, error)进而执行
- 读取当前登录用户;
- 调用mall-rpc查询订单;
- 校验订单归属;
- 校验订单金额;
- 校验订单状态。
等操作
HTTP转换为Protobuf类型
网关完成校验后调用:
1
2
3
4
5
6
7
8
rpcResp, err := l.svcCtx.PaymentClient.CreatePayment(
l.ctx,
&paymentpb.CreatePaymentReq{
OrderId: order.Id,
UserId: userID,
Amount: order.PayAmount,
},
)这里由HTTP请求类型,通过网关读取订单补充数据转为RPC请求类型
因为请求中只携带了order_id,而发给rpc的数据又添加了userid和amount,前者来自登录状态,后者来自mall-rpc,而不是从前端获取数据
PaymentClient内部
PaymentClient实现于payment_service.go
l.svcCtx.PaymentClient.CreatePayment进入了该Client的实现:
1
2
3
4
5
6
7
8
func (m *defaultPaymentService) CreatePayment(
ctx context.Context,
in *CreatePaymentReq,
opts ...grpc.CallOption,
) (*CreatePaymentResp, error) {
client := pb.NewPaymentServiceClient(m.cli.Conn())
return client.CreatePayment(ctx, in, opts...)
}这里又包装了一层gozero的paymentService客户端,用于统一调用方式、对接zrpc连接、提供更方便的接口和集成服务发现、超时、拦截器等功能,执行RPC请求的是pb.NewPaymentServiceClient(m.cli.Conn())
发送网络请求
protobuf生成的_grpc.pb.go中的CreatePayment方法用于发送网络请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (c *paymentServiceClient) CreatePayment(
ctx context.Context,
in *CreatePaymentReq,
opts ...grpc.CallOption,
) (*CreatePaymentResp, error) {
out := new(CreatePaymentResp)
err := c.cc.Invoke(
ctx,
"/payment.PaymentService/CreatePayment",
in,
out,
opts...,
)
if err != nil {
return nil, err
}
return out, nil
}1
2
3
4
5
6
c.cc.Invoke(
ctx,
"/payment.PaymentService/CreatePayment",
in,
out,
)用于拼成gRPC方法,接下来gRPC会根据protobuf定义序列化req,通过gRPC连接发送到payment-rpc,等待服务端响应后将响应反序列化为resp
我们的请求对象如下:
1
2
3
4
5
&paymentpb.CreatePaymentReq{
OrderId: "order-001",
UserId: "user-001",
Amount: 480000,
}protobuf在网络传输时主要依赖字段编号,而不是字段名称,因此字段编号一旦发布就不能随意更改含义
拦截器传递身份
创建客户端时调用了
1
2
3
tokenPropagator := zrpc.WithUnaryClientInterceptor(
interceptor.UnaryClientInterceptor(),
)会从当前HTTP请求的context中读取JWT等身份信息,写入gRPC的metadata,链路如下:
1
2
3
4
5
6
7
8
9
10
11
HTTP Authorization Header
↓
AuthMiddleware解析JWT
↓
user_id/token放入context
↓
gRPC客户端拦截器读取context
↓
写入gRPC metadata
↓
RPC服务端认证拦截器校验因此调用RPC时需要传递当前请求的context:PaymentClient.CreatePayment(l.ctx, req)
RPC服务找到对应方法
服务器收到/payment.PaymentService/CreatePayment后,通过protobuf生成代码中注册的服务描述:
1
2
3
4
5
6
7
8
9
var PaymentService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "payment.PaymentService",
Methods: []grpc.MethodDesc{
{
MethodName: "CreatePayment",
Handler: _PaymentService_CreatePayment_Handler,
},
},
}找到对应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
2
3
4
5
6
7
func (s *PaymentServiceServer) CreatePayment(
ctx context.Context,
in *pb.CreatePaymentReq,
) (*pb.CreatePaymentResp, error) {
l := paymentservicelogic.NewCreatePaymentLogic(ctx, s.svcCtx)
return l.CreatePayment(in)
}它本身不写具体业务,主要负责通过gRPC请求构造和调用logic,使得业务进入l.CreatePayment,进而执行具体逻辑
响应回到前端
payment-rpc返回resp后,响应沿着反方向返回:
1
2
3
4
5
6
7
8
9
10
11
payment-rpc logic
↓
PaymentServiceServer
↓
protobuf序列化
↓
gRPC网络
↓
网关PaymentClient
↓
rpcResp网关再把protobuf响应转换为HTTP响应
1
2
3
4
5
return &types.MallCreatePaymentResp{
OutTradeNo: rpcResp.OutTradeNo,
QrCode: rpcResp.QrCode,
Status: rpcResp.Status,
}, nil最后执行
1
2
3
"out_trade_no": "abc123",
"qr_code": "https://qr.alipay.com/...",
"status": 0完整的数据类型转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HTTP JSON
↓
types.MallCreatePaymentReq
↓
pb.CreatePaymentReq
↓
protobuf二进制
↓
pb.CreatePaymentReq
↓
payment-rpc业务处理
↓
pb.CreatePaymentResp
↓
protobuf二进制
↓
pb.CreatePaymentResp
↓
types.MallCreatePaymentResp
↓
HTTP JSON