前几篇文章我们已经详细分析了 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.Server、http.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 | 原生 HTTP Server: |
这里有一个重要区别:HTTP Filter 和 kratos Middleware 是两套不同层次的机制。
FilterFunc面向 HTTP 协议本身,签名是func(http.Handler) http.Handlermiddleware.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 | ┌─────────────────────────────────────────────────────────────┐ |
几个核心对象之间的关系如下:
1 | kratos.App |
从设计上看,kratos 并没有重新实现 HTTP 协议栈,而是复用了 Go 标准库和 gorilla/mux:
- 底层监听、连接管理、请求解析由
net/http完成 - 路由匹配、路径模板、路径参数由
gorilla/mux完成 - 生命周期、endpoint、统一 Transport 上下文、中间件桥接由 kratos 完成
这也是 kratos Transport 层的一贯思路:尽量复用成熟协议库,只在框架边界处做统一抽象和能力注入。
Server 创建流程
HTTP Server 的入口函数是 transport/http/server.go 中的 NewServer:
1 | func NewServer(opts ...ServerOption) *Server { |
第一步:创建 kratos Server 对象
Server 结构体定义如下:
1 | type Server struct { |
它内嵌了标准库的 *http.Server,同时额外保存 kratos 需要的状态:
lis:底层监听器endpoint:用于服务注册的地址filters:HTTP Filter 链middleware:kratos 中间件匹配器decVars/decQuery/decBody:请求解码函数enc/ene:响应和错误编码函数router:底层 gorilla/mux 路由器
文件顶部还有三个接口断言:
1 | var ( |
这说明 kratos HTTP Server 同时扮演三个角色:
- 对
kratos.App来说,它是一个可启动、可停止的transport.Server - 对服务注册来说,它是一个可以返回 endpoint 的
transport.Endpointer - 对标准库来说,它本身也是一个
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 | for _, o := range opts { |
例如:
1 | srv := http.NewServer( |
这会把监听地址改为 :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的全局中间件,给当前路由下所有匹配的请求统一前置处理逻辑
它的核心职责包括:
- 为每个请求创建带超时的 context
- 获取当前路由的 path template,例如
/users/{id} - 创建
http.Transport对象 - 将 Transport 写入 server context
- 用新的 request context 替换原请求
第五步:创建标准库 http.Server
最后,kratos 创建真正的标准库 http.Server:
1 | srv.Server = &http.Server{ |
这里最关键的是这一行:
1 | Handler: FilterChain(srv.filters...)(srv.router) |
它看起来有点绕,因为这里连续调用了两次函数。我们可以把它拆开看。首先,FilterChain(srv.filters...) 会返回一个 FilterFunc:
1 | type FilterFunc func(http.Handler) http.Handler |
所以 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 | type Handler interface { |
因此 srv.router 可以作为最内层的 next http.Handler 传给 Filter 链。最终得到的 handler 仍然是一个 http.Handler,可以赋值给标准库 http.Server.Handler。
如果把这行代码完全展开,大致等价于:
1 | chain := FilterChain(srv.filters...) |
请求进来时,执行顺序就是:
1 | A 进入 |
这就是典型的 HTTP 洋葱模型。每个 Filter 都可以在调用 next.ServeHTTP(w, r) 之前做前置处理,也可以在调用之后做后置处理。
因此 HTTP 请求进入后的第一层并不是 mux 路由,而是全局 HTTP Filter 链:
1 | HTTP 请求 |
这解释了为什么 kratos 同时需要 Filter 和 Middleware:Filter 更靠近 HTTP 原生层,包裹的是 http.Handler;Middleware 更靠近 kratos 业务调用层,包裹的是 middleware.Handler。
ServerOption 配置项
HTTP Server 提供了较多配置项,可以按职责分成几类。
监听与端点配置
1 | func Network(network string) ServerOption |
Network指定监听网络,默认是tcpAddress指定监听地址,例如:8000Endpoint直接指定服务注册使用的 endpointListener允许外部传入已经创建好的net.Listener
大多数业务项目只需要使用 Address。Endpoint 和 Listener 更多用于测试、自定义监听器、或者服务注册地址和监听地址不一致的场景。
请求超时与 TLS
1 | func Timeout(timeout time.Duration) ServerOption |
Timeout 控制每个请求的 context 超时时间。它不是 http.Server 的 ReadTimeout 或 WriteTimeout,而是在 kratos 内部 filter 中通过 context.WithTimeout 创建请求上下文:
1 | if s.timeout > 0 { |
这意味着业务逻辑和 kratos 中间件可以通过 ctx.Done() 感知请求超时。
而 TLSConfig 则决定 Server 启动时调用 Serve 还是 ServeTLS。
Filter 与 Middleware
1 | func Filter(filters ...FilterFunc) ServerOption |
这两个选项很容易混淆,但它们处在不同层级。
Filter 保存到 s.filters:
1 | func Filter(filters ...FilterFunc) ServerOption { |
最终用于包裹整个 mux.Router:
1 | Handler: FilterChain(srv.filters...)(srv.router) |
Middleware 保存到 s.middleware:
1 | func Middleware(m ...middleware.Middleware) ServerOption { |
它不会直接包裹 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 | func RequestVarsDecoder(dec DecodeRequestFunc) ServerOption |
这些 option 控制 HTTP 请求和响应如何被转换:
RequestVarsDecoder:路径参数解码,例如/users/{id}中的idRequestQueryDecoder:query 参数解码,例如?page=1RequestDecoder:body 解码,例如 JSON 请求体ResponseEncoder:正常响应编码ErrorEncoder:错误响应编码
在手写路由时,业务 Handler 可以通过 Context.Bind、Context.BindVars、Context.BindQuery 使用这些解码器;在 protoc 生成的 HTTP 代码中,也会使用这些能力完成请求绑定。
路由行为配置
1 | func StrictSlash(strictSlash bool) ServerOption |
这些 option 本质上是对 gorilla/mux 的配置封装:
StrictSlash控制/path和/path/是否自动重定向PathPrefix给整个 router 增加统一前缀NotFoundHandler自定义 404 处理MethodNotAllowedHandler自定义 405 处理
路由注册机制:Router、Handle、HandleFunc
HTTP Server 创建完成后,业务代码需要注册路由。kratos 提供了两种层次的注册方式。
Server 暴露了一组接近 net/http 风格的方法:
1 | func (s *Server) Handle(path string, h http.Handler) |
这些方法直接操作底层 mux.Router:
1 | func (s *Server) HandleFunc(path string, h http.HandlerFunc) { |
这种方式适合注册一些非常原生的 HTTP Handler,例如健康检查、静态资源、pprof 等。
使用 kratos Router 注册 HandlerFunc
更常见的方式是先创建 Router:
1 | r := srv.Route("/api") |
Server.Route 很简单:
1 | func (s *Server) Route(prefix string, filters ...FilterFunc) *Router { |
Router 保存三个字段:
1 | type Router struct { |
prefix:当前路由分组前缀srv:所属的 HTTP Serverfilters:当前路由分组上的 HTTP Filter
Router 支持继续分组:
1 | func (r *Router) Group(prefix string, filters ...FilterFunc) *Router { |
这里会把父分组的 filters 和子分组的 filters 合并起来,所以分组 Filter 具备继承关系。
Router.Handle:从 kratos HandlerFunc 到 http.Handler
理解 Router.Handle 之前,要先明确一点:kratos 的 Router 本身并不直接参与 HTTP 请求分发,它只是一个路由注册辅助对象。真正接收 HTTP 请求的是标准库 http.Server:
1 | srv.Server = &http.Server{ |
这里的 srv.router 是底层的 *mux.Router。当请求进入 http.Server 后,最终会调用到:
1 | srv.router.ServeHTTP(w, req) |
也就是说,HTTP 请求处理链路中真正负责匹配 URL 和 Method 的是 gorilla/mux.Router。kratos 自己定义的 Router 只是保存了 prefix、分组 filters 和 *Server 指针,方便业务代码用更简洁的方式注册路由:
1 | type Router struct { |
当我们调用 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 | func (r *Router) Handle(method, relativePath string, h HandlerFunc, filters ...FilterFunc) { |
这段代码是 HTTP Server 路由注册的核心,可以分成四步:
- 把 kratos 的
HandlerFunc func(Context) error包装成标准库http.Handler - 为每次请求创建
wrapper,它实现了 kratos 的Context接口 - 执行业务 Handler,如果返回 error,则调用
ErrorEncoder - 将路由级 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 | func FilterChain(filters ...FilterFunc) FilterFunc { |
如果注册:
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 | 全局 Filter |
其中全局 Filter 包裹整个 router,分组和路由 Filter 则在 Router.Handle 中包裹具体 Handler。
Middleware:协议无关业务链
middleware.Middleware 是 kratos 的统一中间件抽象:
1 | type Handler func(ctx context.Context, req any) (any, error) |
它不依赖 http.ResponseWriter、*http.Request,因此可以同时用于 HTTP 和 gRPC。
TTP 场景下,Context 提供了一个 Middleware 方法:
1 | func (c *wrapper) Middleware(h middleware.Handler) middleware.Handler { |
它会从请求 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 | func (s *Server) Use(selector string, m ...middleware.Middleware) { |
比如可以给某一类路径注册额外中间件:
1 | srv.Use("/admin/*", authMiddleware) |
这样做的好处是:全局中间件和局部中间件都可以使用同一套 matcher 机制,而不是在每个 Handler 中手动判断路径。
一次 HTTP 请求的完整处理链路
理解完上面的组件后,我们把一次 HTTP 请求串起来看。假设服务这样注册:
1 | srv := http.NewServer( |
当请求 GET /api/users/123 进入时,执行链路大致如下:
1 | 1. net/http 接收请求 |
其中第 5 到第 7 步由 srv.filter() 完成,是 HTTP Transport 接入 kratos 统一上下文体系的关键。源码如下:
1 | func (s *Server) filter() mux.MiddlewareFunc { |
这段代码有几个关键点。
创建请求级超时 Context
1 | if s.timeout > 0 { |
每个请求都会得到一个新的 context。默认情况下,请求超时时间是 1 秒。业务逻辑、中间件、下游调用都应该沿用这个 context,这样才能正确响应取消和超时。
获取路由模板作为 Operation
1 | pathTemplate := req.URL.Path |
如果请求路径是:
1 | /api/users/123 |
路由模板可能是:
1 | /api/users/{id} |
kratos 使用模板而不是实际路径作为 operation,这是一个重要设计。否则每个用户 ID 都会变成不同 operation,日志、指标、tracing、中间件匹配都会产生高基数问题。
创建 HTTP Transport
HTTP Transport 定义在 transport/http/transport.go:
1 | type Transport struct { |
它实现了通用的 transport.Transporter 接口:
1 | func (tr *Transport) Kind() transport.Kind { |
同时它还扩展了 HTTP 专属能力:
1 | func (tr *Transport) Request() *http.Request |
这样,中间件可以只依赖通用 transport.Transporter 读取协议无关信息;确实需要 HTTP 原生对象时,也可以通过 HTTP Transporter 获取 *http.Request 或 http.ResponseWriter。
写入 Server Context
1 | tr.request = req.WithContext(transport.NewServerContext(ctx, tr)) |
这一行把 Transport 写入 context,并创建新的 *http.Request 继续向后传递。后续任何代码都可以通过以下方式取出 Transport:
1 | tr, ok := transport.FromServerContext(ctx) |
这就是 kratos 中间件能够在 HTTP 和 gRPC 中使用同一套接口获取请求信息的基础。
Start/Stop 生命周期管理
HTTP Server 实现了 transport.Server 接口,因此可以交给 kratos.App 统一管理:
1 | type Server interface { |
Endpoint:计算服务注册地址
在启动之前,HTTP Server 需要确定监听地址和 endpoint:
1 | func (s *Server) Endpoint() (*url.URL, error) { |
真正的逻辑在 listenAndEndpoint:
1 | func (s *Server) listenAndEndpoint() error { |
它做了两件事:
- 如果还没有 listener,则调用
net.Listen创建 - 如果还没有 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 | func (s *Server) Start(ctx context.Context) error { |
启动流程很清晰:
- 调用
listenAndEndpoint确保 listener 和 endpoint 已创建 - 设置
BaseContext,让标准库 HTTP Server 的连接上下文继承 App 传入的 context - 根据是否配置 TLS,选择
ServeTLS或Serve - 如果返回的是
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 | s.BaseContext = func(net.Listener) context.Context { |
这里的 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 | func (s *Server) Stop(ctx context.Context) error { |
它优先调用标准库的 Shutdown(ctx) 做优雅停机:
- 停止接受新连接
- 等待已有请求处理完成
- 如果 context 超时或取消,则返回错误
如果优雅停机超时,kratos 会调用 Close() 强制关闭连接。这和 App 生命周期管理配合起来,就形成了完整的服务关闭流程:
1 | 收到 SIGTERM / SIGINT |
与 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 | transport.Transporter |
这就是 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 | 请求进入 |
这也是 Transport 层存在的意义:屏蔽协议差异,让上层治理能力建立在统一抽象之上。
小结
本文我们分析了 kratos HTTP Transport 的总体设计和 HTTP Server 的核心实现。其核心是基于标准库 net/http,同时路由使用 gorilla/mux,不重复造 HTTP 协议栈。它把原生 HTTP 请求转换成 kratos 统一的 Transport + Middleware + Context 模型,而让 HTTP 和 gRPC 可以共享同一套生命周期管理、上下文传播和服务治理能力。