单份 Thrift 实现 Hertz 网关 + Kitex RPC 全流程(含统一参数验证)

2024-01-17T14:21:02+08:00 | 5分钟阅读 | 更新于 2024-01-17T14:21:02+08:00

@

一、前置环境准备

先安装所有必需的工具,确保版本匹配:

# 1. 安装Thrift代码生成核心
go install github.com/cloudwego/thriftgo@latest

# 2. 安装Kitex RPC代码生成工具
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest

# 3. 安装Hertz HTTP代码生成工具
go install github.com/cloudwego/hertz/cmd/hz@latest

# 4. 安装参数验证代码生成插件给Kitex用与Hertz验证注解兼容
go install github.com/cloudwego/thrift-gen-validator@latest

初始化项目(替换为你自己的模块名):

mkdir user-demo && cd user-demo
go mod init github.com/yourname/user-demo

二、编写共用 Thrift IDL(用户模块示例)

创建 idl/user.thrift,一份文件同时包含三层定义:

  1. RPC 服务接口定义(给 Kitex 用)
  2. HTTP 路由与参数绑定规则(给 Hertz 用,api.*注解)
  3. 统一参数验证规则(api.vd注解,两边工具都可识别)
namespace go user

// 用户实体
struct User {
    1: i64 id       (api.vd="min=1,message=用户ID必须大于0")
    2: string username (api.vd="required,min=3,max=20,message=用户名长度3-20位")
    3: string email (api.vd="required,email,message=邮箱格式不正确")
}

// 查询用户请求
struct GetUserReq {
    // api.query:HTTP层从query参数取值,RPC层正常传参
    1: i64 user_id (api.query="user_id", api.vd="min=1,message=用户ID必须大于0")
}

// 创建用户请求
struct CreateUserReq {
    // api.body:HTTP层从JSON Body取值
    1: string username (api.body="username", api.vd="required,min=3,max=20")
    2: string password (api.body="password", api.vd="required,min=6,message=密码至少6位")
    3: string email (api.body="email", api.vd="required,email")
}

struct CreateUserResp {
    1: User user
}

// 统一服务定义:Kitex生成RPC服务,Hertz生成HTTP路由
service UserService {
    // HTTP GET /user/get?user_id=123
    GetUser(1: GetUserReq req) (api.get="/user/get")

    // HTTP POST /user/create,Body为JSON
    CreateUser(1: CreateUserReq req) (api.post="/user/create")
}

三、基于同一份 IDL 生成两层代码

1. 生成 Kitex RPC 层代码(带验证标签)

执行命令生成 RPC 服务骨架、客户端 SDK、带validate标签的结构体:

kitex -module github.com/yourname/user-demo \
  -service user-rpc \
  -plugin validator \
  -out ./cmd/user-rpc \
  idl/user.thrift
  • module:必须和 go.mod 的模块名完全一致,避免导入路径错误
  • plugin validator:启用验证插件,自动给结构体生成validate标签
  • out:RPC 服务骨架输出目录,kitex_gen代码会自动生成在项目根目录

2. 生成 Hertz HTTP 层代码(带验证标签)

执行命令生成 HTTP 网关的路由、Handler 骨架、带vd标签的结构体:

hz new -module github.com/yourname/user-demo \
  -out ./cmd/gateway \
  -idl ../../idl/user.thrift

后续修改 IDL 后,用hz update -idl ../../idl/user.thrift增量更新代码,不会覆盖你写的业务逻辑。

最终项目结构

user-demo/
├── idl/
│   └── user.thrift          # 唯一的接口定义文件
├── kitex_gen/               # Kitex生成的RPC结构体、客户端、服务端代码
│   └── user/
├── cmd/
│   ├── user-rpc/            # Kitex RPC服务端入口与Handler
│   │   ├── main.go
│   │   └── handler.go
│   └── gateway/             # Hertz HTTP网关入口与路由
│       ├── main.go
│       └── biz/
│           ├── handler/     # HTTP业务Handler
│           └── router/      # 自动生成的路由
└── go.mod

四、实现 Kitex RPC 服务端(带全局参数验证)

1. 编写全局验证中间件

RPC 层做兜底校验,避免内部调用绕过网关导致非法参数。创建 cmd/user-rpc/middleware/validator.go:

package middleware

import (
    "context"
    "github.com/cloudwego/kitex/pkg/endpoint"
    "github.com/go-playground/validator/v10"
)

// 全局单例validator,避免重复初始化
var validate = validator.New()

// ValidatorMiddleware RPC服务端参数验证中间件
func ValidatorMiddleware() endpoint.Middleware {
    return func(next endpoint.Endpoint) endpoint.Endpoint {
        return func(ctx context.Context, req, resp interface{}) (err error) {
            // 自动读取结构体的validate标签验证
            if err = validate.Struct(req); err != nil {
                return err
            }
            return next(ctx, req, resp)
        }
    }
}

2. 注册中间件并实现业务 Handler

修改 cmd/user-rpc/main.go,注册验证中间件:

package main

import (
    "github.com/cloudwego/kitex/pkg/rpcinfo"
    "github.com/cloudwego/kitex/server"
    "github.com/yourname/user-demo/cmd/user-rpc/middleware"
    "github.com/yourname/user-demo/kitex_gen/user/userservice"
    "log"
)

func main() {
    svr := userservice.NewServer(
        new(UserServiceImpl),
        server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: "user-rpc"}),
        server.WithMiddleware(middleware.ValidatorMiddleware()), // 注册全局验证中间件
    )

    err := svr.Run()
    if err != nil {
        log.Println(err.Error())
    }
}

修改 cmd/user-rpc/handler.go 实现业务逻辑(验证已经由中间件完成,Handler 只处理业务):

package main

import (
    "context"
    "github.com/yourname/user-demo/kitex_gen/user"
)

type UserServiceImpl struct{}

func (s *UserServiceImpl) GetUser(ctx context.Context, req *user.GetUserReq) (resp *user.User, err error) {
    // 业务逻辑:查询数据库等,参数已经验证完成
    return &user.User{
        Id:       req.UserId,
        Username: "test_user",
        Email:    "test@example.com",
    }, nil
}

func (s *UserServiceImpl) CreateUser(ctx context.Context, req *user.CreateUserReq) (resp *user.CreateUserResp, err error) {
    // 业务逻辑:创建用户,参数已经验证完成
    return &user.CreateUserResp{
        User: &user.User{
            Id:       1001,
            Username: req.Username,
            Email:    req.Email,
        },
    }, nil
}

五、实现 Hertz HTTP 网关(带参数验证 + RPC 调用)

1. 初始化 RPC 客户端

创建 cmd/gateway/biz/rpc/user.go,封装 Kitex RPC 客户端:

package rpc

import (
    "github.com/cloudwego/kitex/client"
    "github.com/yourname/user-demo/kitex_gen/user/userservice"
    "log"
)

var UserClient userservice.Client

func InitRPC() {
    c, err := userservice.NewClient(
        "user-rpc",
        client.WithHostPorts("127.0.0.1:8888"), // RPC服务地址,生产用服务发现
    )
    if err != nil {
        log.Fatalf("RPC客户端初始化失败: %v", err)
    }
    UserClient = c
}

2. 实现 HTTP Handler(入口验证 + RPC 调用)

修改 cmd/gateway/biz/handler/user_service.go,Hertz 层做入口参数校验,拦截非法请求:

package handler

import (
    "context"
    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/common/utils"
    "github.com/cloudwego/hertz/pkg/protocol/consts"
    "github.com/yourname/user-demo/cmd/gateway/biz/rpc"
    "github.com/yourname/user-demo/cmd/gateway/hertz_gen/user"
    kitexUser "github.com/yourname/user-demo/kitex_gen/user"
)

type UserServiceHandler struct{}

func NewUserServiceHandler() *UserServiceHandler {
    return &UserServiceHandler{}
}

func (h *UserServiceHandler) GetUser(ctx context.Context, c *app.RequestContext) {
    // 1. HTTP层自动绑定参数+验证(读取vd标签)
    var req user.GetUserReq
    if err := c.BindAndValidate(&req); err != nil {
        c.JSON(consts.StatusBadRequest, utils.H{"code": 400, "msg": err.Error()})
        return
    }

    // 2. 调用RPC服务(同一份IDL,字段一一对应直接转换)
    rpcResp, err := rpc.UserClient.GetUser(ctx, &kitexUser.GetUserReq{UserId: req.UserId})
    if err != nil {
        c.JSON(consts.StatusInternalServerError, utils.H{"code": 500, "msg": err.Error()})
        return
    }

    c.JSON(consts.StatusOK, utils.H{"code": 0, "data": rpcResp})
}

func (h *UserServiceHandler) CreateUser(ctx context.Context, c *app.RequestContext) {
    var req user.CreateUserReq
    if err := c.BindAndValidate(&req); err != nil {
        c.JSON(consts.StatusBadRequest, utils.H{"code": 400, "msg": err.Error()})
        return
    }

    rpcResp, err := rpc.UserClient.CreateUser(ctx, &kitexUser.CreateUserReq{
        Username: req.Username,
        Password: req.Password,
        Email:    req.Email,
    })
    if err != nil {
        c.JSON(consts.StatusInternalServerError, utils.H{"code": 500, "msg": err.Error()})
        return
    }

    c.JSON(consts.StatusOK, utils.H{"code": 0, "data": rpcResp})
}

3. 网关启动入口初始化 RPC

修改 cmd/gateway/main.go,启动前初始化 RPC 客户端:

package main

import (
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/yourname/user-demo/cmd/gateway/biz/rpc"
    "github.com/yourname/user-demo/cmd/gateway/biz/router"
)

func main() {
    rpc.InitRPC() // 初始化RPC客户端
    h := server.Default(server.WithHostPorts(":8080"))
    router.Register(h)
    h.Spin()
}

六、运行与验证

  1. 启动 RPC 服务:go run ./cmd/user-rpc(默认监听 8888 端口)
  2. 启动 HTTP 网关:go run ./cmd/gateway(监听 8080 端口)
  3. 测试验证效果:
# 测试参数校验失败(user_id为0不符合min=1规则)
curl "http://localhost:8080/user/get?user_id=0"
# 返回:{"code":400,"msg":"Key: 'GetUserReq.UserId' Error:Field validation for 'UserId' failed on the 'min' tag"}

# 测试正常请求
curl "http://localhost:8080/user/get?user_id=123"

七、最佳实践

  • IDL 唯一原则:接口变更只修改 Thrift 文件,重新生成代码,彻底避免 HTTP 与 RPC 接口不一致的问题。
  • 分层验证职责:HTTP 层做格式校验(必填、长度、正则),RPC 层做业务校验(如用户唯一性、权限),边界清晰且双重兜底。
  • 自定义验证规则:复杂验证逻辑(跨字段、业务规则)编写统一的 validator 自定义函数,在两层同时注册,保证规则完全一致。
  • 生产环境优化:RPC 服务注册到服务发现中心(如 etcd、nacos),网关通过服务名发现实例,不要硬编码地址。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!