go-zero--api层通过metadata方式向rpc传公共参数
一、应用场景
api层通过metaData方式向rpc传公共参数,如token、访问来源ip、地址接口地址等等,这些信息传到rpc后,存日志等操作。
context.Context说明:
gRPC中的context.Context 也符合Go语言的使用习惯:通常情况下我们在函数首个参数放置context.Context用来传递一次RPC中有关的上下文,借助context.WithValue()或ctx.Value()往context添加变量或读取变量
数据流:
metadata就是gRPC中可以传递的上下文信息之一,所以metadata的使用方式就是:metadata记录到context,从context读取metadata
二、api层
2.1 封装一个全局的上下文 RootCtx
作用:用来存储上下文件需要传递的数据
common/utils/helper.go
package utils
import (
"bytes"
"context"
)
type KeyValueContext struct {
context.Context
values map[string]interface{}
mutex *sync.Mutex // 用于保护values的互斥锁
}
func NewKeyValueContext(ctx context.Context) *KeyValueContext {
return &KeyValueContext{
Context: ctx,
values: make(map[string]interface{}),
mutex: &sync.Mutex{}, // 初始化互斥锁
}
}
func (kvc *KeyValueContext) Set(key string, value interface{}) {
kvc.mutex.Lock() // 加锁
defer kvc.mutex.Unlock() // 确保在函数结束时解锁
kvc.values[key] = value // 设置值
}
func (kvc *KeyValueContext) Get(key string) (interface{}, bool) {
kvc.mutex.Lock() // 加锁
defer kvc.mutex.Unlock() // 确保在函数结束时解锁
value, ok := kvc.values[key] // 获取值
return value, ok
}
2.2 编写中间件headerMiddleware.go
作用:请求头中需要的数据,存入上下文件中,示如:token
common/middleware/headerMiddleware.go
package middleware
import (
"context"
"net/http"
"ytss_go_zero/common/consts"
"ytss_go_zero/common/utils"
)
// HeaderMiddleware 请求处理header头信息中间件
type HeaderMiddleware struct {
rootCtx *utils.KeyValueContext
}
// NewHeaderMiddleware ...
func NewHeaderMiddleware(rootCtx *utils.KeyValueContext) *HeaderMiddleware {
return &HeaderMiddleware{
rootCtx: rootCtx,
}
}
// Handle 请求处理
func (m *HeaderMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
//fmt.Println("HeaderMiddleware start")
// 上下文设置更多键值对 header信息
setContextWithHeader(&(m.rootCtx.Context), r)
// 处理请求
//next(w, r)
next(w, r.WithContext(m.rootCtx.Context))
//fmt.Println("HeaderMiddleware end")
}
}
// 设置上下文 header信息
func setContextWithHeader(ctx *context.Context, r *http.Request) {
// 上下文设置更多键值对
*ctx = context.WithValue(*ctx, consts.Authorization, r.Header.Get(consts.Authorization))
*ctx = context.WithValue(*ctx, consts.Platform, r.Header.Get(consts.Platform))
*ctx = context.WithValue(*ctx, consts.ClientVersion, r.Header.Get(consts.ClientVersion))
*ctx = context.WithValue(*ctx, consts.ClientIp, utils.GetClientIP(r))
*ctx = context.WithValue(*ctx, consts.Os, r.Header.Get(consts.Os))
*ctx = context.WithValue(*ctx, consts.UserAgent, r.Header.Get(consts.UserAgent))
*ctx = context.WithValue(*ctx, consts.Method, r.Method)
*ctx = context.WithValue(*ctx, consts.ContentType, r.Header.Get(consts.ContentType))
*ctx = context.WithValue(*ctx, consts.RequestURI, r.RequestURI)
}
2.3 api的main.go中使用全局中间件调用上面的中间件
package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"ytss_go_zero/app/api/admin/internal/config"
"ytss_go_zero/app/api/admin/internal/handler"
"ytss_go_zero/app/api/admin/internal/svc"
"ytss_go_zero/common/middleware"
"ytss_go_zero/common/utils"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx"
)
var configFile = flag.String("f", "etc/admin-api-dev.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(
c.RestConf,
rest.WithCors(c.Cors.Domains...), // 跨域 允许跨域的域名
// rest.WithCorsHeaders("Access-Control-Allow-Headers", "OS"),
rest.WithCustomCors(func(header http.Header) {
// 允许跨域的请求头 key不区分大小写
header.Add("Access-Control-Allow-Headers", c.Cors.AccessControlAllowHeaders)
header.Set("Access-Control-Allow-Methods", c.Cors.AccessControlAllowMethods)
header.Set("Access-Control-Expose-Headers", "Content-Length, Content-Type")
}, nil, "*"),
// 在 `/static/public` 目录下有需要对外提供的文件目录,比如有个文件 `1.txt`,
// 访问地: http://127.0.0.1:8888/public/1.txt 即可访问/static/public/1.txt文件
rest.WithFileServer("/public", http.Dir("./static/public")),
)
logx.MustSetup(c.Log) // 设置日志配置
logx.AddWriter(logx.NewWriter(os.Stdout)) // 添加控制台输出 文件和控制台同时输出
defer server.Stop()
// 创建一个根Context
rootCtx := utils.NewKeyValueContext(context.Background())
//c.CompanyRpcConf.Token = "123123"
ctx := svc.NewServiceContext(c, rootCtx)
handler.RegisterHandlers(server, ctx)
// print routes
server.PrintRoutes()
// 设置成功处理代码
httpx.SetOkHandler(utils.HttpOkHandler)
// 设置错误处理代码
httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
//http状态码, 返回内容
return http.StatusOK, utils.WrapErrorRes(err)
})
// 全局中间件 设置header头信息存入上下文 主要看这里
server.Use(middleware.NewHeaderMiddleware(rootCtx).Handle)
fmt.Printf("Starting admin-api server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
2.4 封装metadata使用的函数
作用:
- 组装api调用rpc之前需要传递的metadata信息
- rpc在上下文件中获取api传递过来的metadata信息
common/token/erp_token_user.go
package token
import (
"context"
"encoding/json"
"errors"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"strconv"
"ytss_go_zero/common/consts"
"ytss_go_zero/common/errorx"
"ytss_go_zero/common/utils"
"github.com/zeromicro/go-zero/core/logx"
"go.uber.org/zap"
)
// HeaderStreamInterceptor api请求rpc之前,设置metaData要传过去的信息
func HeaderStreamInterceptor(ctx context.Context, methed string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
//fmt.Println("发送rpc请求之前")
//authorization := ctx.Value(consts.Authorization)
//fmt.Println("admin HeaderStreamInterceptor Authorization--->", authorization)
//
//platform := ctx.Value(consts.Platform)
//fmt.Println("admin HeaderStreamInterceptor platform--->", platform)
//
//clientVersion := ctx.Value(consts.ClientVersion)
//fmt.Println("admin HeaderStreamInterceptor clientVersion--->", clientVersion)
//
//clientIp := ctx.Value(consts.ClientIp)
//fmt.Println("admin HeaderStreamInterceptor clientIp--->", clientIp)
//
//os := ctx.Value(consts.Os)
//fmt.Println("admin HeaderStreamInterceptor os--->", os)
//
//
md := metadata.New(map[string]string{
consts.Authorization: ctx.Value(consts.Authorization).(string),
consts.Platform: ctx.Value(consts.Platform).(string),
consts.ClientVersion: ctx.Value(consts.ClientVersion).(string),
consts.ClientIp: ctx.Value(consts.ClientIp).(string),
consts.Os: ctx.Value(consts.Os).(string),
consts.UserAgent: ctx.Value(consts.UserAgent).(string),
consts.Method: ctx.Value(consts.Method).(string),
consts.RequestURI: ctx.Value(consts.RequestURI).(string),
consts.ContentType: ctx.Value(consts.ContentType).(string),
})
ctx = metadata.NewOutgoingContext(ctx, md)
err := invoker(ctx, methed, req, reply, cc, opts...)
if err != nil {
return err
}
//fmt.Println("发送rpc请求之后")
//请求之后
return nil
}
type MetaData struct {
Authorization string `json:"authorization"`
Platform string `json:"PLATFORM"` // App(手机端)/ Pad (PAD端) / Web(WEB端)
ClientVersion string `json:"version"` // 客户端版本:3.3.3
ClientIp string `json:"clientIp"` // 客户端版本:3.3.3
Os string `json:"Os"` // Android / Ios
UserAgent string `json:"userAgent"` // 客户端版本:3.3.3
Method string `json:"method"` // 客户端版本:3.3.3
ContextType string `json:"contextType"` //客户端版本 传参方式
RequestURI string `json:"requestURI"` // 客户端版本:3.3.3
}
// rpc从metadata信息中获取api传传过来的数据,返回接收到的metadata信息
func NewMetaDataWithContext(ctx context.Context) *MetaData {
var m MetaData
if _, ok := metadata.FromIncomingContext(ctx); ok {
m.Authorization = GetMetaDataWithKey(ctx, consts.Authorization)
m.Platform = GetMetaDataWithKey(ctx, consts.Platform)
m.ClientVersion = GetMetaDataWithKey(ctx, consts.ClientVersion)
m.ClientIp = GetMetaDataWithKey(ctx, consts.ClientIp)
m.Os = GetMetaDataWithKey(ctx, consts.Os)
m.UserAgent = GetMetaDataWithKey(ctx, consts.UserAgent)
m.Method = GetMetaDataWithKey(ctx, consts.Method)
m.ContextType = GetMetaDataWithKey(ctx, consts.ContentType)
m.RequestURI = GetMetaDataWithKey(ctx, consts.RequestURI)
}
return &m
}
// GetMetaData rpc中获取上下文件中的metadata信息
func GetMetaDataWithContext(ctx context.Context) (metaData *MetaData) {
// 获取metadata信息
return ctx.Value(consts.MetaData).(*MetaData)
}
// GetUserTokenWithContext rpc中获取用户信息 从token中获取用户信息,并解析为结构造体
func GetUserTokenWithContext(ctx context.Context) (userToken TokenUser, err error) {
// 获取metadata信息
metaData := GetMetaDataWithContext(ctx)
// step1 解析token信息
return AnalyzeToken(metaData.Authorization)
}
2.5 调用rpc时传入metadata参数
app/api/admin/internal/svc/service_context.go
package svc
import (
"ytss_go_zero/app/api/admin/internal/config"
"ytss_go_zero/app/api/admin/internal/middleware"
"ytss_go_zero/app/rpc/company/client/testservice"
"ytss_go_zero/common/token"
"ytss_go_zero/common/utils"
"ytss_go_zero/models"
"github.com/jinzhu/copier"
"github.com/redis/go-redis/v9"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
"gorm.io/gorm"
)
type ServiceContext struct {
Config config.Config
RootCtx *utils.KeyValueContext
Mysql *gorm.DB
RDB *redis.Client
TokenAuth rest.Middleware
FaAreaService faareaservice.FaAreaService
}
func NewServiceContext(c config.Config, rootCtx *utils.KeyValueContext) *ServiceContext {
// 初始化业务redis驱动
var rdbConf redis.Options
copier.Copy(&rdbConf, &c.RedisDB)
return &ServiceContext{
Config: c,
TokenAuth: middleware.NewTokenAuthMiddleware(c, rootCtx).Handle,
RootCtx: rootCtx,
RDB: models.InitRedis(rdbConf),
// 测试服务 主要看这里 zrpc.WithUnaryClientInterceptor(token.HeaderStreamInterceptor)
CoreRpcTestService: testservice.NewTestService(zrpc.MustNewClient(c.CompanyRpcConf, zrpc.WithUnaryClientInterceptor(token.HeaderStreamInterceptor))),
}
}
三、rpc层
这里以order rpc为例说明
3.1 main.go主函数中,全局中间获取metadata信息
在orderrpc.go主函数中,全局中间件中获取metadata信息,并存入上下文件中
package main
import (
"flag"
"fmt"
"os"
"ytss_go_zero/app/rpc/order/internal/config"
fadeliveryorderserviceServer "ytss_go_zero/app/rpc/order/internal/server/fadeliveryorderservice"
"ytss_go_zero/app/rpc/order/internal/svc"
"ytss_go_zero/app/rpc/order/pb"
"ytss_go_zero/common/interceptor/rpcserver"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/service"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
var configFile = flag.String("f", "etc/orderrpc.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
logx.MustSetup(c.Log) // 设置日志配置
logx.AddWriter(logx.NewWriter(os.Stdout)) // 添加控制台输出 文件和控制台同时输出
ctx := svc.NewServiceContext(c)
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
pb.RegisterFaDeliveryOrderServiceServer(grpcServer, fadeliveryorderserviceServer.NewFaDeliveryOrderServiceServer(ctx))
if c.Mode == service.DevMode || c.Mode == service.TestMode {
reflection.Register(grpcServer)
}
})
//rpc log
s.AddUnaryInterceptors(rpcserver.LoggerInterceptor)
//获取metaData信息 主要看这里
s.AddUnaryInterceptors(rpcserver.HeaderInterceptor)
defer s.Stop()
fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
s.Start()
}
3.2 编写中间件 headerInterceptor.go
作用:
从上下文件中获取api传过来的matadata信息,并存入上下文件中
common/interceptor/rpcserver/headerInterceptor.go
package rpcserver
import (
"context"
"ytss_go_zero/common/consts"
"ytss_go_zero/common/token"
"google.golang.org/grpc"
)
/**
* @Description rpc service header interceptor
* @Author haima
* @Date 2024/11/24 13:35
* @Version 1.0
**/
func HeaderInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
//fmt.Println("HeaderInterceptor start")
metaData := token.NewMetaDataWithContext(ctx)
ctx = context.WithValue(ctx, consts.MetaData, metaData)
resp, err = handler(ctx, req)
//fmt.Println("HeaderInterceptor end")
return resp, err
}
3.3 在逻辑中获取metadata信息
app/rpc/company/internal/logic/testservice/pong_logic.go
package testservicelogic
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/zeromicro/go-zero/core/logx"
"ytss_go_zero/app/rpc/company/internal/svc"
"ytss_go_zero/app/rpc/company/pb"
"ytss_go_zero/common/token"
"ytss_go_zero/common/utils"
)
type PongLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewPongLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PongLogic {
return &PongLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *PongLogic) Pong(in *pb.PongReq) (*pb.PongResp, error) {
var err error
metaData := token.GetMetaDataWithContext(l.ctx) # 主要看这里
fmt.Println("value123:", metaData.UserAgent)
if in.Id == 0 {
err = errors.New("req.id is required")
if err != nil {
l.Logger.Errorf("id不能为空: req:%+v,", in)
return nil, errors.New("id不能为空")
}
return nil, err
}
dataBytes, _ := json.Marshal(data)
return &pb.PongResp{
Msg: fmt.Sprintf("data:%v", string(dataBytes)),
}, nil
}
到此完成api向rpc传递公共数据。感谢阅读到这里的同学,如果错误,请留言,看到后我看更正。
编辑者:海马
源创文章,转载请注明来源
https://www.cnblogs.com/haima/p/18566176
参考文档
写给go开发者的gRPC教程-metadata
https://juejin.cn/post/7202409558592782373
go-zero实现metadata从当前RPC服务传递到下游RPC服务上下文context
https://blog.csdn.net/qq_40726812/article/details/117525474