最近在学Go,想找个项目来做,加上对Gin框架的实现原理很好奇,于是跟着7天用Go从零实现Web框架Gee教程系列动手写一个简化版。这是学习笔记,记录对net/http的一些理解,顺便借这个机会看和对比一下Gin的源码实现。
这篇文章我想搞清楚的主要问题是,当我启动一个服务器,一个请求进来之后,标准库是怎么把它交到我写的函数手里的?搞清楚这个交接点在哪里,才能明白Web框架需要实现什么。
用net/http标准库启动服务器:
用net/http启动一个最简单的HTTP服务器:
package main
import (
"fmt"
"net/http"
)
func main() {
// 1.注册路由函数
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hello!")
})
http.HandleFunc("/", homeHandler)
// 2.启动服务器
log.Fatal(http.ListenAndServe(":12345", nil))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Home!")
}
在这一步,代码做了两件事:
- 注册路由:HandleFunc函数
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
它的作用是注册路由,当客户端访问对应的URL(pattern)时,就会调用处理函数handler - 启动服务器:ListenAndServe函数
func ListenAndServe(addr string, handler Handler) error
它的作用是启动服务器,会监听端口,并循环处理请求;如果第二个参数为nil,默认使用标准库中内置的默认路由器 http.DefaultServeMux。
运行上述代码时,标准库在干什么:
从ListenAndServe到Handler:
net/http库中的调用链:
这一部分的所有代码都来自于net/http库的server.go文件。
当我们调用ListenAndServe函数时,其中创建了一个server对象,而真正干活的是server对象中的同名ListenAndServe方法。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
而同名ListenAndServe方法中,它主要创建了一个TCP监听器ln,并绑定到指定端口(传入的端口号);然后把这个ln传入到Serve函数中去干活。
func (s *Server) ListenAndServe() error {
if s.shuttingDown() {
return ErrServerClosed
}
addr := s.Addr
if addr == "" {
addr = ":http"
}
// 创建了一个TCP监听器,并绑定到指定端口
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(ln)
}
在Serve函数中,核心是一个for循环,其中主要干了两件相关的事:
- 接受新的TCP连接
- 为每个连接开goroutine处理
func (s *Server) Serve(l net.Listener) error {
// 略
// 核心的循环
for {
// 接受新的连接
rw, err := l.Accept()
// 略,大概是错误处理
// 略,大概是为每个连接创建独立的Context,方便后续取消和传递消息
// 为每个连接开goroutine处理
c := s.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
那么下一步干活的就是serve函数,它是上面开的每个连接的主处理函数,在每个goroutine里跑。
这个函数最重要的部分,同样也是一个循环:这个循环在一个连接上,不断处理HTTP请求。
在这个循环中,有一行serverHandler{c.server}.ServeHTTP(w, w.req),它是标准库和我们的代码的交接点:
- 它创建了一个serverHandler对象,并把server传进去
- 调用了serverHandler的ServeHTTP函数
func (c *conn) serve(ctx context.Context) {
// 略,大概是用defer进行错误处理和关闭连接
// 略,大概是TLS握手
// 略,大概是缓冲区和HTTP版本处理
// 这个循环实现了HTTP/1.1的KeepAlive机制,一个连接可以处理多个请求,直到关闭
for {
// 略,大概是读取请求
// 分发给Handler
serverHandler{c.server}.ServeHTTP(w, w.req)
// 略,大概是完成响应
}
}
在serveHandler的ServeHTTP函数里,就是net/http库与我们写的代码的交接点。
它只做一件事,就是找出正确的handler,然后把请求交给它处理。
这个handler就是在使用net/http启动一个简单服务器时,函数ListenAndServe的第二个入参。
而handler.ServeHTTP(rw, req)就是,对接口Handler的ServeHTTP的方法的实现。
如果当时函数ListenAndServe的第二个入参为nil,那么调用的就是ServeMux.ServeHTTP方法;如果传入的是自己实现的mux,或其他任何实现了ServeHTTP方法的自定义对象,那么它就会调用mux.ServeHTTP。
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
这里由于我们ListenAndServe的第二个入参为nil,那么调用的就是ServeMux的ServeHTTP函数,这个函数的功能主要是查路由表找到对应的handler,并且调用它。
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
var h Handler
if use121 {
h, _ = mux.mux121.findHandler(r)
} else {
h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
}
h.ServeHTTP(w, r)
}
上面的三个ServeHTTP容易让人疑惑:它们其实都是对Handler接口的实现,但是为啥会一层一层套娃调用?
- serverHandler.ServeHTTP是“交接点”。它在这里把请求"送出去",决定交给谁处理。
- ServeMux.ServeHTTP 是“默认接收方”。如果没有传入自己的handler,标准库就把请求交给它,由它去查路由表。
- handler.ServeHTTP(ServeMux.ServeHTTP最后一行) 这是“最终处理”。调用的是注册的具体路由函数,是由HandleFunc函数的第二个入参的函数类型转换而来。
这里有两个在函数中出现的handler也容易混淆:
- 第一个是ListenAndServe函数的第二个参数,是ServeMux对象本身
- 另一个是HandleFunc函数的第二个入参,存在ServeMux的路由表里。
ServeMux结构如下,routingNode中的handler就是就是handler.ServeHTTP的handler。
type ServeMux struct {
mu sync.RWMutex
tree routingNode
index routingIndex
mux121 serveMux121
}
type routingNode struct {
// HandleFunc的两个入参存在这
pattern *pattern
handler Handler
children mapping[string, *routingNode]
multiChild *routingNode
emptyChild *routingNode
}
结论
ListenAndServe在函数内部主要干了三件有关的事:
- 创建TCP监听器,绑定到指定端口
- 进入循环,不断Accept新的TCP连接
- 为每个连接开一个goroutine,在每个goroutine中调用了注册处理函数HandleFunc传入的handler。
而ServeHTTP就是net/http库与我们代码的交接点。
Handler接口
Handler接口,它有唯一的一个方法ServeHTTP。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
而不论是默认的ServeMux,还是Gin、Echo之类的Web框架,都通过实现了ServeHTTP方法,以此来和net/http库产生关联、处理路由。
那么从这里来看,这个接口是很重要的:想要在net/http库上实现自己的Web框架,一定要实现Handler接口。
普通函数是如何变成Handler接口的
不过在正式开始实现Handler接口之前,还是有一个问题困扰我:在启动服务器那一部分调用http.HandleFunc时传入的参数handler明明是个函数,为什么到最后在ServeMux里变成了Handler接口?这就是Go中的适配器在发力了。
适配器:
Go中的适配器,目的就是将一个接口可以转换为另一个接口,让原本不兼容的类型可以一起工作。
个人觉得适配器能够工作的本质是:在Go里满足了接口中的所有方法,就相当于实现了这个接口。
type Animal interface {
Speak() string
}
func dogSpeak() string {
return "wangwang"
}
假设有一个接口Animal和一个普通函数dogSpeak(),但函数不是接口,不能直接用在需要Animal的地方,所以我们需要一个适配器AnimalFunc。
type AnimalFunc func() string
func (f AnimalFunc) Speak() string {
return f()
}
接下来就可以使用AnimalFunc,完成转换。
// 类型转换
var a Animal = AnimalFunc(dogSpeak)
// 输出:wangwang
fmt.Println(a.Speak))
函数适配器HandlerFunc:
那么同样地,在net/http里定义了一个函数类型HandlerFunc,并关联了ServeHTTP方法,它就是适配器:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
所以只要是函数签名匹配的函数f,经过HandlerFunc(f)的类型转换,就满足了Handler接口,可以使用f.ServeHTTP。
而当我们调用HandleFunc时,它就会调用HandlerFunc来进行类型转换。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
// use121是兼容Go1.21旧路由逻辑的判断
if use121 {
DefaultServeMux.mux121.handleFunc(pattern, handler)
} else {
// 新逻辑HandlerFunc转换在这
DefaultServeMux.register(pattern, HandlerFunc(handler))
}
}
func (mux *serveMux121) handleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
// 旧逻辑HandlerFunc转换在这
mux.handle(pattern, HandlerFunc(handler))
}
概念对比:
上面出现了很多Handle/Handler开头的名字,一开始我没看仔细,弄混了,导致看得云里雾里, 在这里区分一下。
Handle:是函数,作用为为指定的路由模式注册一个handler。
HandleFunc:是函数,作用为将指定的处理函数注册到指定的路由。其实是对函数Handle的进一步封装,区别是http.Handle的入参为Handle接口,http.HandleFunc的入参为函数。
HandlerFunc:是一个适配器,让签名相同的函数转换为Handler接口。
Handler:接口。
自己做路由:
手动解析请求:
从上面知道了,想要在net/http库上实现自己的Web框架,一定要实现Handler接口,其实就是要在自己的结构体上实现Handler接口唯一的ServeHTTP方法。
我们需要在ServeHTTP中手动解析请求:
package main
import (
"fmt"
"log"
"net/http"
)
// 实现了Handler,处理所有请求
type Engine struct{}
// 实现ServeHTTP,做自己的路由处理
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/":
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
case "/hello":
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
default:
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
func main() {
engine := new(Engine)
log.Fatal(http.ListenAndServe(":9999", engine))
}
但这样的话,有一些问题:
- 虽然可以将每个case提取成独立函数,但是switch会越来越长
- 受限于必须实现了ServeHTTP的对象,用户注册路由的时候需要操作Engine或其他满足条件的对象,而不是像net/http启动服务器的例子一样可以是任意函数
抽象路由表,实现灵活的请求分发
那么为了解决上面的问题,主要要做四件事:
1.类型定义:
// 自定义函数适配器
type HandlerFunc func(http.ResponseWriter, *http.Request)
// 自定义Engine,用来处理所有请求
type Engine struct {
router map[string]HandlerFunc
}
func New() *Engine {
return &Engine{router: make(map[string]HandlerFunc)}
}
HandlerFunc就是我们自定义的函数适配器,它让我们可以将某个相同签名的函数作为具体的路由处理逻辑,而不需要为每个函数实现Handler接口。
Engine实现Handler接口,用来统一处理所有请求,目前只有一个哈希表作为路由映射表,Key由请求方法和静态地址组成,形式如“GET-/hello“,Value为具体的处理方法。
2.路由注册
// 添加路由
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
engine.router[key] = handler
}
// 添加各种方法处理
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
3.启动服务器
其实就是对ListenAndServe进行了一层包装:
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
4.请求分发
实现Handler的ServeHTTP接口,并且根据解析路径查找Engine中的路由表来寻找处理方法:
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
key := req.Method + "-" + req.URL.Path
if handler, ok := engine.router[key]; ok {
handler(w, req)
} else {
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
对比Gin
Engine
在Gin中,Engine同样是框架的实例,包含了路由器、中间件和配置信息。
type Engine struct {
// 路由
RouterGroup
trees methodTrees
// 其他略
}
其中与当前有关的大概是RouterGroup和trees两个字段。
RouterGroup是嵌入的路由组,提供路由注册能力(GET/POST 等方法)。
trees实现了Gin的路由树,存储所有已注册的路由,这在教程里是用map存储的。
说句题外话,我在看这部分代码的时候发现,net/http库里经常出现if use121的判断语句,这里的use121指的其实是是否使用Go1.21的旧路由逻辑,相关的结构体为ServeMux121,这里就不细说了。
但有意思的是,Go1.21标准库中不是用tree来存储路由,而是用map,也经历过改变。
RouterGroup里的Engine指针
RouterGroup的结构为:
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
type HandlersChain []HandlerFunc
第一眼看到这个*Engine的时候,还在想Engine中有RouterGroup,RouterGroup中有Engine,它们不会互相嵌套吗?因为你中有我、我中有你,这会导致一个无限循环,内存无法计算出大小。
后来发现这是个Engine指针,不是Engine的值,指针存的是Engine的地址,在64位平台下,Go中指针大小是8字节,所以不会有上面的问题。
还有一个小细节,就是,HandlersChain是HandlerFunc的切片,而不是HandlerFunc。
疑问
还是有一些疑问困扰着我,但是教程后面好像有相关的内容。所以我决定让子弹飞一会,先记下来,如果到最后我还不明白再看。
- HandlerFunc的入参是Context
- 为什么教程里是map存储,而Gin是tree存储
- 在Gin中注册路由和存储路由也分开了
- 为啥要弄一个HandlerChain