0%

kratos 源码分析 09:HTTP Transport(1)

前几篇文章我们已经详细分析了 kratos gRPC Transport 的实现原理,这篇文章我们将回到服务端,开始分析 kratos 的另一条核心传输链路:HTTP Transport。本文会先介绍 HTTP Transport 的整体设计,再深入 HTTP Server 的创建流程、路由注册、Filter 与 Middleware 的分层关系,以及一次 HTTP 请求在 kratos 中的完整处理链路。

Kratos 的 HTTP Transport

Go 标准库已经提供了非常成熟的 net/http,很多 Web 框架也都是基于它构建的。那么 kratos 为什么还需要再封装一层 HTTP Transport?直接使用 http.Serverhttp.ServeMux 不行吗?标准库解决的是 HTTP 协议处理问题,而 kratos 需要解决的是微服务框架中的统一传输抽象问题

在一个微服务框架中,HTTP Server 不只是监听端口、匹配路由、调用 Handler,它还需要和框架的其他能力协同工作:

需求 原生 net/http kratos HTTP Transport 提供的能力
生命周期管理 需要手动启动和关闭 http.Server 实现 transport.Server,由 kratos.App 统一启动和停止
服务注册端点 需要自己计算监听地址 实现 transport.Endpointer,自动生成可注册的 endpoint
中间件复用 只能使用 func(http.Handler) http.Handler 同时支持 HTTP Filter 和协议无关的 middleware.Middleware
请求上下文传播 *http.Request.Context() 只包含基础信息 注入 transport.Transporter,中间件和业务层可读取传输信息
统一编解码 每个 Handler 自己解析请求和写响应 提供统一的 RequestDecoder、ResponseEncoder、ErrorEncoder
路由分组 标准库能力较弱 基于 gorilla/mux 提供路径参数、方法匹配、分组 Filter

所以 kratos HTTP Transport 的核心目标不是替代 net/http,而是把 net/http 包装成符合 kratos 框架模型的 Transport Server:

1
2
3
4
5
6
7
8
9
10
原生 HTTP Server:
net.Listen → http.Server.Serve → mux 匹配路由 → http.Handler

Kratos HTTP Server:
kratos.App 管理生命周期
→ HTTP Server 实现 transport.Server / Endpointer
→ Filter 处理 HTTP 层逻辑
→ Transport 注入请求上下文
→ Middleware 处理协议无关逻辑
→ HandlerFunc 处理业务逻辑

这里有一个重要区别:HTTP Filter 和 kratos Middleware 是两套不同层次的机制

  • FilterFunc 面向 HTTP 协议本身,签名是 func(http.Handler) http.Handler
  • middleware.Middleware 面向 kratos 统一传输抽象,签名是 func(middleware.Handler) middleware.Handler

前者更适合处理 CORS、静态文件、HTTP Header、Rewrite 等 HTTP 专属逻辑;后者更适合处理 tracing、logging、recovery、auth、metrics 等 HTTP/gRPC 都能复用的通用逻辑。

HTTP Transport 整体架构

kratos HTTP Transport 的核心代码位于 transport/http 目录下,主要由以下几个部分组成:

文件 职责
server.go HTTP Server 的创建、配置、启动停止、Transport 注入
router.go 路由分组、方法注册、HandlerFunc 适配
filter.go HTTP Filter 定义与链式组合
context.go HTTP Context 封装,请求绑定、响应返回、中间件匹配
transport.go HTTP Transport 上下文,实现 transport.Transporter
codec.go 请求解码、响应编码、错误编码

整体结构可以简化为三层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────┐
│ 用户代码层 │
│ srv := http.NewServer(...) │
│ route := srv.Route("/api") │
│ route.GET("/users/{id}", handler) │
├─────────────────────────────────────────────────────────────┤
│ kratos HTTP 封装层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Server │ │ Router │ │ Context / Transport │ │
│ │ 生命周期管理 │ │ 路由注册分组 │ │ 请求上下文与编解码 │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
│ ┌──────────────┐ ┌────────────────────────────────────────┐ │
│ │ Filter │ │ middleware.Middleware │ │
│ │ HTTP 层包装 │ │ 协议无关中间件 │ │
│ └──────────────┘ └────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Go HTTP 原生层 │
│ net.Listener / http.Server / gorilla.mux / http.Handler │
└─────────────────────────────────────────────────────────────┘

几个核心对象之间的关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kratos.App
└─ transport.Server
└─ http.Server(kratos 封装)
├─ 内嵌 *net/http.Server
├─ 持有 *mux.Router
├─ 持有 HTTP Filter 链
├─ 持有 matcher.Matcher(kratos Middleware 匹配器)
├─ 实现 Start / Stop
├─ 实现 Endpoint
└─ 实现 ServeHTTP

Router
├─ 保存 prefix
├─ 保存分组 filters
└─ 将 HandlerFunc 适配为 http.Handler 后注册到 mux.Router

Transport
├─ 实现 transport.Transporter
├─ 保存 request / response
├─ 保存 request header / reply header
└─ 通过 context 传递给中间件和业务逻辑

从设计上看,kratos 并没有重新实现 HTTP 协议栈,而是复用了 Go 标准库和 gorilla/mux:

  • 底层监听、连接管理、请求解析由 net/http 完成
  • 路由匹配、路径模板、路径参数由 gorilla/mux 完成
  • 生命周期、endpoint、统一 Transport 上下文、中间件桥接由 kratos 完成

这也是 kratos Transport 层的一贯思路:尽量复用成熟协议库,只在框架边界处做统一抽象和能力注入

Server 创建流程

HTTP Server 的入口函数是 transport/http/server.go 中的 NewServer

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
func NewServer(opts ...ServerOption) *Server {
srv := &Server{
network: "tcp",
address: ":0",
timeout: 1 * time.Second,
middleware: matcher.New(),
decVars: DefaultRequestVars,
decQuery: DefaultRequestQuery,
decBody: DefaultRequestDecoder,
enc: DefaultResponseEncoder,
ene: DefaultErrorEncoder,
strictSlash: true,
router: mux.NewRouter(),
}
srv.router.NotFoundHandler = http.DefaultServeMux
srv.router.MethodNotAllowedHandler = http.DefaultServeMux
for _, o := range opts {
o(srv)
}
srv.router.StrictSlash(srv.strictSlash)
srv.router.Use(srv.filter())
srv.Server = &http.Server{
Handler: FilterChain(srv.filters...)(srv.router),
TLSConfig: srv.tlsConf,
}
return srv
}

第一步:创建 kratos Server 对象

Server 结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Server struct {
*http.Server
lis net.Listener
tlsConf *tls.Config
endpoint *url.URL
err error
network string
address string
timeout time.Duration
filters []FilterFunc
middleware matcher.Matcher
decVars DecodeRequestFunc
decQuery DecodeRequestFunc
decBody DecodeRequestFunc
enc EncodeResponseFunc
ene EncodeErrorFunc
strictSlash bool
router *mux.Router
}

它内嵌了标准库的 *http.Server,同时额外保存 kratos 需要的状态:

  • lis:底层监听器
  • endpoint:用于服务注册的地址
  • filters:HTTP Filter 链
  • middleware:kratos 中间件匹配器
  • decVars / decQuery / decBody:请求解码函数
  • enc / ene:响应和错误编码函数
  • router:底层 gorilla/mux 路由器

文件顶部还有三个接口断言:

1
2
3
4
5
var (
_ transport.Server = (*Server)(nil)
_ transport.Endpointer = (*Server)(nil)
_ http.Handler = (*Server)(nil)
)

这说明 kratos HTTP Server 同时扮演三个角色:

  1. kratos.App 来说,它是一个可启动、可停止的 transport.Server
  2. 对服务注册来说,它是一个可以返回 endpoint 的 transport.Endpointer
  3. 对标准库来说,它本身也是一个 http.Handler

第二步:设置默认值

NewServer 中给 HTTP Server 设置了一组默认值:

字段 默认值 说明
network "tcp" 默认使用 TCP 网络
address ":0" 默认随机端口
timeout 1s 每个请求默认超时时间
middleware matcher.New() 创建中间件匹配器
decVars DefaultRequestVars 默认路径参数解码器
decQuery DefaultRequestQuery 默认 query 解码器
decBody DefaultRequestDecoder 默认 body 解码器
enc DefaultResponseEncoder 默认响应编码器
ene DefaultErrorEncoder 默认错误编码器
strictSlash true 启用 mux StrictSlash
router mux.NewRouter() 创建 gorilla/mux 路由器

其中 address: ":0" 表示如果用户不显式指定地址,系统会自动分配一个可用端口。这对测试场景很友好,但实际服务通常会通过配置指定固定端口。

第三步:应用 ServerOption

和 kratos 其他模块一样,HTTP Server 也使用选项模式配置:

1
type ServerOption func(*Server)

用户传入的每个 option 都会修改 Server 对象:

1
2
3
for _, o := range opts {
o(srv)
}

例如:

1
2
3
4
5
6
7
8
srv := http.NewServer(
http.Address(":8000"),
http.Timeout(2*time.Second),
http.Middleware(
tracing.Server(),
recovery.Recovery(),
),
)

这会把监听地址改为 :8000,请求超时时间改为 2 秒,并注册一组 kratos 协议无关中间件。

第四步:注册 kratos 内部 filter

NewServer 中有一行非常关键:

1
srv.router.Use(srv.filter())
  • 这里的 srv.filter() 是 kratos 注入 Transport 上下文的内部中间件
  • 它注册到 gorilla/mux 上,会在路由匹配之后、业务 Handler 执行之前运行
  • gorilla/mux 的 router.Use() 是注册 gorilla/mux 的全局中间件,给当前路由下所有匹配的请求统一前置处理逻辑

它的核心职责包括:

  1. 为每个请求创建带超时的 context
  2. 获取当前路由的 path template,例如 /users/{id}
  3. 创建 http.Transport 对象
  4. 将 Transport 写入 server context
  5. 用新的 request context 替换原请求

第五步:创建标准库 http.Server

最后,kratos 创建真正的标准库 http.Server

1
2
3
4
srv.Server = &http.Server{
Handler: FilterChain(srv.filters...)(srv.router),
TLSConfig: srv.tlsConf,
}

这里最关键的是这一行:

1
Handler: FilterChain(srv.filters...)(srv.router)

它看起来有点绕,因为这里连续调用了两次函数。我们可以把它拆开看。首先,FilterChain(srv.filters...) 会返回一个 FilterFunc

1
2
3
4
5
6
7
8
9
10
type FilterFunc func(http.Handler) http.Handler

func FilterChain(filters ...FilterFunc) FilterFunc {
return func(next http.Handler) http.Handler {
for i := len(filters) - 1; i >= 0; i-- {
next = filters[i](next)
}
return next
}
}

所以 chain := FilterChain(srv.filters...) 得到的是一个函数:

1
func(next http.Handler) http.Handler

第二步再把 srv.router 传进去:

1
handler := chain(srv.router)

这里的 srv.router*mux.Router,而 gorilla/mux.Router 实现了标准库的 http.Handler 接口:

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

因此 srv.router 可以作为最内层的 next http.Handler 传给 Filter 链。最终得到的 handler 仍然是一个 http.Handler,可以赋值给标准库 http.Server.Handler

如果把这行代码完全展开,大致等价于:

1
2
3
4
5
6
7
chain := FilterChain(srv.filters...)
wrappedRouter := chain(srv.router)

srv.Server = &http.Server{
Handler: wrappedRouter,
TLSConfig: srv.tlsConf,
}

请求进来时,执行顺序就是:

1
2
3
4
5
6
7
A 进入
B 进入
C 进入
srv.router.ServeHTTP
C 返回
B 返回
A 返回

这就是典型的 HTTP 洋葱模型。每个 Filter 都可以在调用 next.ServeHTTP(w, r) 之前做前置处理,也可以在调用之后做后置处理。

因此 HTTP 请求进入后的第一层并不是 mux 路由,而是全局 HTTP Filter 链:

1
2
3
4
5
HTTP 请求
→ 全局 FilterChain(srv.filters...)
→ mux.Router
→ srv.filter() 注入 Transport
→ 业务 Handler

这解释了为什么 kratos 同时需要 FilterMiddlewareFilter 更靠近 HTTP 原生层,包裹的是 http.HandlerMiddleware 更靠近 kratos 业务调用层,包裹的是 middleware.Handler

ServerOption 配置项

HTTP Server 提供了较多配置项,可以按职责分成几类。

监听与端点配置

1
2
3
4
func Network(network string) ServerOption
func Address(addr string) ServerOption
func Endpoint(endpoint *url.URL) ServerOption
func Listener(lis net.Listener) ServerOption
  • Network 指定监听网络,默认是 tcp
  • Address 指定监听地址,例如 :8000
  • Endpoint 直接指定服务注册使用的 endpoint
  • Listener 允许外部传入已经创建好的 net.Listener

大多数业务项目只需要使用 AddressEndpointListener 更多用于测试、自定义监听器、或者服务注册地址和监听地址不一致的场景。

请求超时与 TLS

1
2
func Timeout(timeout time.Duration) ServerOption
func TLSConfig(c *tls.Config) ServerOption

Timeout 控制每个请求的 context 超时时间。它不是 http.ServerReadTimeoutWriteTimeout,而是在 kratos 内部 filter 中通过 context.WithTimeout 创建请求上下文:

1
2
3
4
5
if s.timeout > 0 {
ctx, cancel = context.WithTimeout(req.Context(), s.timeout)
} else {
ctx, cancel = context.WithCancel(req.Context())
}

这意味着业务逻辑和 kratos 中间件可以通过 ctx.Done() 感知请求超时。

TLSConfig 则决定 Server 启动时调用 Serve 还是 ServeTLS

Filter 与 Middleware

1
2
func Filter(filters ...FilterFunc) ServerOption
func Middleware(m ...middleware.Middleware) ServerOption

这两个选项很容易混淆,但它们处在不同层级。

Filter 保存到 s.filters

1
2
3
4
5
func Filter(filters ...FilterFunc) ServerOption {
return func(o *Server) {
o.filters = filters
}
}

最终用于包裹整个 mux.Router

1
Handler: FilterChain(srv.filters...)(srv.router)

Middleware 保存到 s.middleware

1
2
3
4
5
func Middleware(m ...middleware.Middleware) ServerOption {
return func(o *Server) {
o.middleware.Use(m...)
}
}

它不会直接包裹 http.Handler,而是在 Context.Middleware 中根据 operation 匹配并组合:

1
return middleware.Chain(c.router.srv.middleware.Match(tr.Operation())...)(h)

简单来说:

类型 签名 作用对象 典型用途
FilterFunc func(http.Handler) http.Handler HTTP Handler CORS、静态资源、Header、Redirect
middleware.Middleware func(middleware.Handler) middleware.Handler kratos Handler tracing、logging、recovery、auth、metrics

编解码配置

1
2
3
4
5
func RequestVarsDecoder(dec DecodeRequestFunc) ServerOption
func RequestQueryDecoder(dec DecodeRequestFunc) ServerOption
func RequestDecoder(dec DecodeRequestFunc) ServerOption
func ResponseEncoder(en EncodeResponseFunc) ServerOption
func ErrorEncoder(en EncodeErrorFunc) ServerOption

这些 option 控制 HTTP 请求和响应如何被转换:

  • RequestVarsDecoder:路径参数解码,例如 /users/{id} 中的 id
  • RequestQueryDecoder:query 参数解码,例如 ?page=1
  • RequestDecoder:body 解码,例如 JSON 请求体
  • ResponseEncoder:正常响应编码
  • ErrorEncoder:错误响应编码

在手写路由时,业务 Handler 可以通过 Context.BindContext.BindVarsContext.BindQuery 使用这些解码器;在 protoc 生成的 HTTP 代码中,也会使用这些能力完成请求绑定。

路由行为配置

1
2
3
4
func StrictSlash(strictSlash bool) ServerOption
func PathPrefix(prefix string) ServerOption
func NotFoundHandler(handler http.Handler) ServerOption
func MethodNotAllowedHandler(handler http.Handler) ServerOption

这些 option 本质上是对 gorilla/mux 的配置封装:

  • StrictSlash 控制 /path/path/ 是否自动重定向
  • PathPrefix 给整个 router 增加统一前缀
  • NotFoundHandler 自定义 404 处理
  • MethodNotAllowedHandler 自定义 405 处理

路由注册机制:Router、Handle、HandleFunc

HTTP Server 创建完成后,业务代码需要注册路由。kratos 提供了两种层次的注册方式。

Server 暴露了一组接近 net/http 风格的方法:

1
2
3
4
func (s *Server) Handle(path string, h http.Handler)
func (s *Server) HandlePrefix(prefix string, h http.Handler)
func (s *Server) HandleFunc(path string, h http.HandlerFunc)
func (s *Server) HandleHeader(key, val string, h http.HandlerFunc)

这些方法直接操作底层 mux.Router

1
2
3
func (s *Server) HandleFunc(path string, h http.HandlerFunc) {
s.router.HandleFunc(path, h)
}

这种方式适合注册一些非常原生的 HTTP Handler,例如健康检查、静态资源、pprof 等。

使用 kratos Router 注册 HandlerFunc

更常见的方式是先创建 Router

1
2
3
4
5
6
7
8
r := srv.Route("/api")
r.GET("/users/{id}", func(ctx http.Context) error {
var req GetUserRequest
if err := ctx.BindVars(&req); err != nil {
return err
}
return ctx.JSON(200, &GetUserReply{ID: req.ID})
})

Server.Route 很简单:

1
2
3
func (s *Server) Route(prefix string, filters ...FilterFunc) *Router {
return newRouter(prefix, s, filters...)
}

Router 保存三个字段:

1
2
3
4
5
type Router struct {
prefix string
srv *Server
filters []FilterFunc
}
  • prefix:当前路由分组前缀
  • srv:所属的 HTTP Server
  • filters:当前路由分组上的 HTTP Filter

Router 支持继续分组:

1
2
3
4
5
6
func (r *Router) Group(prefix string, filters ...FilterFunc) *Router {
var newFilters []FilterFunc
newFilters = append(newFilters, r.filters...)
newFilters = append(newFilters, filters...)
return newRouter(path.Join(r.prefix, prefix), r.srv, newFilters...)
}

这里会把父分组的 filters 和子分组的 filters 合并起来,所以分组 Filter 具备继承关系

Router.Handle:从 kratos HandlerFunc 到 http.Handler

理解 Router.Handle 之前,要先明确一点:kratos 的 Router 本身并不直接参与 HTTP 请求分发,它只是一个路由注册辅助对象。真正接收 HTTP 请求的是标准库 http.Server

1
2
3
srv.Server = &http.Server{
Handler: FilterChain(srv.filters...)(srv.router),
}

这里的 srv.router 是底层的 *mux.Router。当请求进入 http.Server 后,最终会调用到:

1
srv.router.ServeHTTP(w, req)

也就是说,HTTP 请求处理链路中真正负责匹配 URL 和 Method 的是 gorilla/mux.Router。kratos 自己定义的 Router 只是保存了 prefix、分组 filters 和 *Server 指针,方便业务代码用更简洁的方式注册路由:

1
2
3
4
5
type Router struct {
prefix string
srv *Server
filters []FilterFunc
}

当我们调用 r.GET("/users/{id}", getUserHandler) 实际上等价于:

1
r.Handle(http.MethodGet, "/users/{id}", getUserHandler)

Router.Handle 的核心作用不是 处理请求,而是在服务启动前把 kratos 风格的 Handler 注册到底层 mux.Router 上。注册完成后,请求运行时就不再经过 kratos Router 做二次分发,而是由 mux.Router 直接找到之前注册进去的 http.Handler

因此,真正完成 组装 Handler 并注册到底层 mux.Router 的公共逻辑,都集中在 Router.Handle 中:

1
2
3
4
5
6
7
8
9
10
11
12
func (r *Router) Handle(method, relativePath string, h HandlerFunc, filters ...FilterFunc) {
next := http.Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
ctx := &wrapper{router: r}
ctx.Reset(res, req)
if err := h(ctx); err != nil {
r.srv.ene(res, req, err)
}
}))
next = FilterChain(filters...)(next)
next = FilterChain(r.filters...)(next)
r.srv.router.Handle(path.Join(r.prefix, relativePath), next).Methods(method)
}

这段代码是 HTTP Server 路由注册的核心,可以分成四步:

  1. 把 kratos 的 HandlerFunc func(Context) error 包装成标准库 http.Handler
  2. 为每次请求创建 wrapper,它实现了 kratos 的 Context 接口
  3. 执行业务 Handler,如果返回 error,则调用 ErrorEncoder
  4. 将路由级 Filter、分组级 Filter 包裹到 Handler 外层,再注册到 mux

kratos 的 HandlerFunc 的定义是:

1
type HandlerFunc func(Context) error

相比标准库的 func(http.ResponseWriter, *http.Request),这个签名有两个明显变化:

  • 入参变成了 kratos 封装的 Context
  • 返回值变成了 error

这使得 Handler 可以用更统一的方式处理请求绑定、响应编码和错误返回。

Filter 与 Middleware 的分层

前面已经多次提到 Filter 和 Middleware,这里集中梳理一下它们在请求链路中的位置。

Filter:HTTP 原生 Handler 链

FilterFunc 的定义非常简单:

1
type FilterFunc func(http.Handler) http.Handler

它和 Go 标准库常见的 HTTP middleware 写法完全一致。FilterChain 的实现也和 kratos middleware 的 Chain 类似,都是从后往前包裹:

1
2
3
4
5
6
7
8
func FilterChain(filters ...FilterFunc) FilterFunc {
return func(next http.Handler) http.Handler {
for i := len(filters) - 1; i >= 0; i-- {
next = filters[i](next)
}
return next
}
}

如果注册:

1
http.Filter(A, B, C)

最终调用顺序是:

1
A → B → C → next → C 返回 → B 返回 → A 返回

在 kratos HTTP Server 中,Filter 有三种层级:

层级 注册位置 生效范围
全局 Filter http.NewServer(http.Filter(...)) 整个 mux.Router
分组 Filter srv.Route("/api", filters...) / Group 当前路由分组
路由 Filter r.GET("/path", handler, filters...) 单个路由

它们大致形成这样的包裹关系:

1
2
3
4
5
6
全局 Filter
→ mux.Router
→ kratos 内部 srv.filter()
→ 分组 Filter
→ 路由 Filter
→ HandlerFunc

其中全局 Filter 包裹整个 router,分组和路由 Filter 则在 Router.Handle 中包裹具体 Handler。

Middleware:协议无关业务链

middleware.Middleware 是 kratos 的统一中间件抽象:

1
2
type Handler func(ctx context.Context, req any) (any, error)
type Middleware func(Handler) Handler

它不依赖 http.ResponseWriter*http.Request,因此可以同时用于 HTTP 和 gRPC。

TTP 场景下,Context 提供了一个 Middleware 方法:

1
2
3
4
5
6
func (c *wrapper) Middleware(h middleware.Handler) middleware.Handler {
if tr, ok := transport.FromServerContext(c.req.Context()); ok {
return middleware.Chain(c.router.srv.middleware.Match(tr.Operation())...)(h)
}
return middleware.Chain(c.router.srv.middleware.Match(c.req.URL.Path)...)(h)
}

它会从请求 context 中取出 Transport,根据 tr.Operation() 匹配应该生效的 middleware,然后组合成调用链。注意这里的 Operation 在 HTTP Server 中通常是路由模板,例如:

1
/users/{id}

而在 gRPC Server 中则是完整方法名,例如:

1
/helloworld.v1.Greeter/SayHello

这就是 kratos 用 operation 统一 HTTP 和 gRPC 中间件匹配的关键。

Server.Use:按 operation 注册 Middleware

HTTP Server 还提供了 Use 方法,可以按 selector 注册中间件:

1
2
3
func (s *Server) Use(selector string, m ...middleware.Middleware) {
s.middleware.Add(selector, m...)
}

比如可以给某一类路径注册额外中间件:

1
srv.Use("/admin/*", authMiddleware)

这样做的好处是:全局中间件和局部中间件都可以使用同一套 matcher 机制,而不是在每个 Handler 中手动判断路径。

一次 HTTP 请求的完整处理链路

理解完上面的组件后,我们把一次 HTTP 请求串起来看。假设服务这样注册:

1
2
3
4
5
6
7
8
srv := http.NewServer(
http.Address(":8000"),
http.Filter(corsFilter),
http.Middleware(tracing.Server(), recovery.Recovery()),
)

r := srv.Route("/api", groupFilter)
r.GET("/users/{id}", getUserHandler, routeFilter)

当请求 GET /api/users/123 进入时,执行链路大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. net/http 接收请求
2. 进入全局 corsFilter
3. 进入 gorilla/mux Router
4. mux 匹配 GET /api/users/{id}
5. 执行 kratos 内部 srv.filter()
6. 创建带 timeout 的 context
7. 创建 http.Transport,并写入 server context
8. 执行 groupFilter
9. 执行 routeFilter
10. 创建 wrapper Context
11. 调用 getUserHandler(ctx)
12. 如果 Handler 内部调用 ctx.Middleware(...),则执行 kratos middleware 链
13. Handler 返回正常响应或 error
14. 正常响应通过 ResponseEncoder 写出
15. error 通过 ErrorEncoder 写出

其中第 5 到第 7 步由 srv.filter() 完成,是 HTTP Transport 接入 kratos 统一上下文体系的关键。源码如下:

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
28
29
30
31
32
33
34
35
func (s *Server) filter() mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var (
ctx context.Context
cancel context.CancelFunc
)
if s.timeout > 0 {
ctx, cancel = context.WithTimeout(req.Context(), s.timeout)
} else {
ctx, cancel = context.WithCancel(req.Context())
}
defer cancel()

pathTemplate := req.URL.Path
if route := mux.CurrentRoute(req); route != nil {
pathTemplate, _ = route.GetPathTemplate()
}

tr := &Transport{
operation: pathTemplate,
pathTemplate: pathTemplate,
reqHeader: headerCarrier(req.Header),
replyHeader: headerCarrier(w.Header()),
request: req,
response: w,
}
if s.endpoint != nil {
tr.endpoint = s.endpoint.String()
}
tr.request = req.WithContext(transport.NewServerContext(ctx, tr))
next.ServeHTTP(w, tr.request)
})
}
}

这段代码有几个关键点。

创建请求级超时 Context

1
2
3
4
5
6
if s.timeout > 0 {
ctx, cancel = context.WithTimeout(req.Context(), s.timeout)
} else {
ctx, cancel = context.WithCancel(req.Context())
}
defer cancel()

每个请求都会得到一个新的 context。默认情况下,请求超时时间是 1 秒。业务逻辑、中间件、下游调用都应该沿用这个 context,这样才能正确响应取消和超时。

获取路由模板作为 Operation

1
2
3
4
pathTemplate := req.URL.Path
if route := mux.CurrentRoute(req); route != nil {
pathTemplate, _ = route.GetPathTemplate()
}

如果请求路径是:

1
/api/users/123

路由模板可能是:

1
/api/users/{id}

kratos 使用模板而不是实际路径作为 operation,这是一个重要设计。否则每个用户 ID 都会变成不同 operation,日志、指标、tracing、中间件匹配都会产生高基数问题。

创建 HTTP Transport

HTTP Transport 定义在 transport/http/transport.go

1
2
3
4
5
6
7
8
9
type Transport struct {
endpoint string
operation string
reqHeader headerCarrier
replyHeader headerCarrier
request *http.Request
response http.ResponseWriter
pathTemplate string
}

它实现了通用的 transport.Transporter 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (tr *Transport) Kind() transport.Kind {
return transport.KindHTTP
}

func (tr *Transport) Endpoint() string {
return tr.endpoint
}

func (tr *Transport) Operation() string {
return tr.operation
}

func (tr *Transport) RequestHeader() transport.Header {
return tr.reqHeader
}

func (tr *Transport) ReplyHeader() transport.Header {
return tr.replyHeader
}

同时它还扩展了 HTTP 专属能力:

1
2
3
func (tr *Transport) Request() *http.Request
func (tr *Transport) Response() http.ResponseWriter
func (tr *Transport) PathTemplate() string

这样,中间件可以只依赖通用 transport.Transporter 读取协议无关信息;确实需要 HTTP 原生对象时,也可以通过 HTTP Transporter 获取 *http.Requesthttp.ResponseWriter

写入 Server Context

1
2
tr.request = req.WithContext(transport.NewServerContext(ctx, tr))
next.ServeHTTP(w, tr.request)

这一行把 Transport 写入 context,并创建新的 *http.Request 继续向后传递。后续任何代码都可以通过以下方式取出 Transport:

1
tr, ok := transport.FromServerContext(ctx)

这就是 kratos 中间件能够在 HTTP 和 gRPC 中使用同一套接口获取请求信息的基础。

Start/Stop 生命周期管理

HTTP Server 实现了 transport.Server 接口,因此可以交给 kratos.App 统一管理:

1
2
3
4
type Server interface {
Start(context.Context) error
Stop(context.Context) error
}

Endpoint:计算服务注册地址

在启动之前,HTTP Server 需要确定监听地址和 endpoint:

1
2
3
4
5
6
func (s *Server) Endpoint() (*url.URL, error) {
if err := s.listenAndEndpoint(); err != nil {
return nil, err
}
return s.endpoint, nil
}

真正的逻辑在 listenAndEndpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *Server) listenAndEndpoint() error {
if s.lis == nil {
lis, err := net.Listen(s.network, s.address)
if err != nil {
s.err = err
return err
}
s.lis = lis
}
if s.endpoint == nil {
addr, err := host.Extract(s.address, s.lis)
if err != nil {
s.err = err
return err
}
s.endpoint = endpoint.NewEndpoint(endpoint.Scheme("http", s.tlsConf != nil), addr)
}
return s.err
}

它做了两件事:

  1. 如果还没有 listener,则调用 net.Listen 创建
  2. 如果还没有 endpoint,则根据监听地址和 TLS 配置生成 endpoint

endpoint.NewEndpoint(endpoint.Scheme("http", s.tlsConf != nil), addr) 会根据是否配置 TLS 决定 scheme:

  • 未配置 TLS:http://host:port
  • 配置 TLS:https://host:port

这个 endpoint 后续会被 kratos.App 收集,并用于服务注册。

Start:启动 HTTP Server

Start 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *Server) Start(ctx context.Context) error {
if err := s.listenAndEndpoint(); err != nil {
return err
}
s.BaseContext = func(net.Listener) context.Context {
return ctx
}
log.Infof("[HTTP] server listening on: %s", s.lis.Addr().String())
var err error
if s.tlsConf != nil {
err = s.ServeTLS(s.lis, "", "")
} else {
err = s.Serve(s.lis)
}
if !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}

启动流程很清晰:

  1. 调用 listenAndEndpoint 确保 listener 和 endpoint 已创建
  2. 设置 BaseContext,让标准库 HTTP Server 的连接上下文继承 App 传入的 context
  3. 根据是否配置 TLS,选择 ServeTLSServe
  4. 如果返回的是 http.ErrServerClosed,说明是正常关闭,不作为错误返回

这里的 Start 是阻塞方法,因此在 kratos.App 中通常会被放到 goroutine 中启动。App 会统一管理多个 Transport Server,例如同时启动 HTTP 和 gRPC。

关于 BaseContext,额外解释一下。BaseContext 是 Go 标准库 http.Server 的一个字段,签名为 func(net.Listener) context.Context。它的作用是为每个新连接提供一个基础 context,http.Server 内部会以它为父 context,再派生出每个请求的 *http.Request.Context()。

默认情况下 http.Server 用 context.Background() 作为新连接的根 context,这意味着即使 App 发起关闭,连接层面也感知不到上层 context 被取消。kratos 在 Start 中做了一行覆盖:

1
2
3
s.BaseContext = func(net.Listener) context.Context {
return ctx
}

这里的 ctx 就是 kratos.App 传入 Start(ctx) 的那个 context。kratos.App 在 Stop 流程中会取消这个 ctx。因此:

  • 正常运行时:BaseContext 返回 App 的 ctx,所有 HTTP 连接都从这个 ctx 派生,和 App 生命周期绑定
  • 关闭时:App 取消 ctx → BaseContext 派生出的所有连接的根 context 也被取消 → 标准库会拒绝新连接、中断空闲长连接 → 配合 Shutdown(ctx) 完成优雅停机

简单说就是:把每个 HTTP 连接的根 context 从无主的 Background() 换成 App 的生命周期 context,让 App 的关闭信号能传导到连接层

Stop:优雅停机

Stop 方法如下:

1
2
3
4
5
6
7
8
9
10
11
func (s *Server) Stop(ctx context.Context) error {
log.Info("[HTTP] server stopping")
err := s.Shutdown(ctx)
if err != nil {
if ctx.Err() != nil {
log.Warn("[HTTP] server couldn't stop gracefully in time, doing force stop")
err = s.Close()
}
}
return err
}

它优先调用标准库的 Shutdown(ctx) 做优雅停机:

  • 停止接受新连接
  • 等待已有请求处理完成
  • 如果 context 超时或取消,则返回错误

如果优雅停机超时,kratos 会调用 Close() 强制关闭连接。这和 App 生命周期管理配合起来,就形成了完整的服务关闭流程:

1
2
3
4
5
6
收到 SIGTERM / SIGINT
→ kratos.App 进入 Stop 流程
→ 调用 HTTP Server.Stop(ctx)
→ http.Server.Shutdown(ctx)
→ 等待请求完成
→ 超时则 Close 强制关闭

与 gRPC Server 的设计对比

前面我们已经分析过 kratos gRPC Server。HTTP Server 和 gRPC Server 在设计目标上高度一致,但实现方式不同。

相同点

设计点 HTTP Server gRPC Server
生命周期 实现 transport.Server 实现 transport.Server
服务注册 实现 transport.Endpointer 实现 transport.Endpointer
请求上下文 注入 HTTP Transport 注入 gRPC Transport
中间件抽象 使用 middleware.Middleware 使用 middleware.Middleware
Operation 路由模板,如 /users/{id} RPC 方法名,如 /pkg.Service/Method
Header 抽象 http.Header 适配为 transport.Header gRPC metadata 适配为 transport.Header

也就是说,HTTP 和 gRPC 虽然底层协议完全不同,但在 kratos 中都会被统一成:

1
2
3
transport.Transporter
middleware.Handler
middleware.Middleware

这就是 kratos 中间件可以跨协议复用的原因。

不同点

差异点 HTTP Server gRPC Server
底层协议库 net/http + gorilla/mux google.golang.org/grpc
扩展机制 http.Handler / Filter Unary/Stream Interceptor
路由来源 HTTP method + path Protobuf service + method
请求绑定 path/query/body/form protobuf message
响应编码 JSON/XML/自定义 codec protobuf codec
Filter 概念 有 HTTP 专属 Filter 无对应概念,主要靠 interceptor

HTTP Server 需要处理更多 HTTP 协议层的细节,比如 path、query、form、Content-Type、ResponseWriter 等;gRPC Server 则更多围绕 protobuf、metadata、interceptor 展开。

但它们最终都会把请求转换成 kratos 的统一模型:

1
2
3
4
5
6
请求进入
→ 创建 Transport
→ 写入 Context
→ 匹配 Middleware
→ 调用业务 Handler
→ 编码响应或错误

这也是 Transport 层存在的意义:屏蔽协议差异,让上层治理能力建立在统一抽象之上。

小结

本文我们分析了 kratos HTTP Transport 的总体设计和 HTTP Server 的核心实现。其核心是基于标准库 net/http,同时路由使用 gorilla/mux,不重复造 HTTP 协议栈。它把原生 HTTP 请求转换成 kratos 统一的 Transport + Middleware + Context 模型,而让 HTTP 和 gRPC 可以共享同一套生命周期管理、上下文传播和服务治理能力。