Day1:从net/http到Web框架

6 阅读11分钟

最近在学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!")
}

在这一步,代码做了两件事:

  1. 注册路由:HandleFunc函数
    func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
    它的作用是注册路由,当客户端访问对应的URL(pattern)时,就会调用处理函数handler
  2. 启动服务器: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循环,其中主要干了两件相关的事:

  1. 接受新的TCP连接
  2. 为每个连接开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),它是标准库和我们的代码的交接点:

  1. 它创建了一个serverHandler对象,并把server传进去
  2. 调用了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在函数内部主要干了三件有关的事:

  1. 创建TCP监听器,绑定到指定端口
  2. 进入循环,不断Accept新的TCP连接
  3. 为每个连接开一个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))
}

但这样的话,有一些问题:

  1. 虽然可以将每个case提取成独立函数,但是switch会越来越长
  2. 受限于必须实现了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。

疑问

还是有一些疑问困扰着我,但是教程后面好像有相关的内容。所以我决定让子弹飞一会,先记下来,如果到最后我还不明白再看。

  1. HandlerFunc的入参是Context
  2. 为什么教程里是map存储,而Gin是tree存储
  3. 在Gin中注册路由和存储路由也分开了
  4. 为啥要弄一个HandlerChain