Go - http.Server源码分析

hsabalaaaC · · 661 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

?

本文将从如下几部分分析net/http模块中关于server部分的源码:

  • Handler类型和HandlerFunc类型是什么?

  • ServeMuxHTTP handler的注册管理和分发

  • Server启动流程

1. Handler

1.1 从注册HTTP handler入手

原生注册HTTP handler有如下两种写法,它们有什么区别呢?

func handler(w http.ResponseWriter, r *http.Request) {}

http.HandleFunc("/some-pattern", handler)
http.Handle("/some-pattern", http.HandlerFunc(handler))
复制代码

两个方法对应的源码如下:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

func Handle(pattern string, handler Handler) { 
    DefaultServeMux.Handle(pattern, handler) 
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}
复制代码

ServeMux类型是什么暂时可以不用理会,会在后文提到。

可以发现,差异体现在HandlerFunc(handler)这一语句,一个在内部调用,一个在外部调用。 而这一语句的作用在于将一个普通的函数转换成为Handler类型,最终只有实现了Handler接口的对象可以注册到HTTP服务端,为特定的路径及其子树提供服务, 它起到一个适配器的作用。

源码如下

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

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}
复制代码

假设f是一个有着正确签名的函数, 那么HandlerFunc(f)就代表一个HTTP handler,除此之外,ServeHTTP方法的调用也代表着请求的处理, 它的调用时机将在后文提到

1.2 内置的几个Handler源码以及使用

1.2.1 NotFoundHandler

返回一个请求处理器,该处理其对每个请求都返回404 page not found

http.Handle("/some-pattern", http.NotFoundHandler())
复制代码

源码

func NotFound(w ResponseWriter, r *Request) { Error(w, "404 page not found", StatusNotFound) }

func NotFoundHandler() Handler { return HandlerFunc(NotFound) }
复制代码

1.2.2 RedirectHandler

返回一个请求处理器,该处理器对每个请求都使用状态码code重定向到网址url

http.Handle("/some-pattern", http.RedirectHandler("/", 301))
复制代码

源码,有一说一,其中的Redirect函数看起来没什么分析价值,跳过

type redirectHandler struct {
	url  string
	code int
}

func RedirectHandler(url string, code int) Handler {
	return &redirectHandler{url, code}
}

func (rh *redirectHandler) ServeHTTP(w ResponseWriter, r *Request) {
	Redirect(w, r, rh.url, rh.code)
}
复制代码

1.2.3 StripPrefix

在将请求定向到你通过参数指定的请求处理器之前,将特定的prefix从URL中过滤出去

http.Handle("/api/some-pattern", http.StripPrefix("/api", handler))
复制代码

源码

func StripPrefix(prefix string, h Handler) Handler {
	if prefix == "" {
		return h
	}
	return HandlerFunc(func(w ResponseWriter, r *Request) {
		if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) {
			// 浅拷贝request
			r2 := new(Request)
			*r2 = *r
			// r.URL是一个引用,需要再拷贝一次
			r2.URL = new(url.URL)
			*r2.URL = *r.URL
			// 重置请求路径
			r2.URL.Path = p
			h.ServeHTTP(w, r2)
		} else {
			NotFound(w, r)
		}
	})
}
复制代码

理解该部分只需要了解两个点:

  • new(T)为类型申请一片内存空间,并返回指向这片内存的指针

  • 对指针变量进行取值*操作,可以获得指针变量指向的原变量的值

1.2.4 TimeoutHandler

返回一个采用指定时间限制的请求处理器,如果某一次调用耗时超过了时间限制,该处理器会回复请求状态码503 Service Unavailable,并将msg作为回复的主体。

func handler(w http.ResponseWriter, r *http.Request) {
	time.Sleep(2 * time.Second)
}

http.Handle("/some-pattern", http.TimeoutHandler(http.HandlerFunc(handler), 1 * time.Second, "Timeout"))
复制代码

定义

type timeoutHandler struct {
	handler Handler
	body    string
	dt      time.Duration
}

func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler {
	return &timeoutHandler{
		handler: h,
		body:    msg,
		dt:      dt,
	}
}
复制代码

当触发请求处理器时,ServeHTTP方法会执行下面的操作(为了保证可读性简化非关键代码)

func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
    // 初始化一个可被取消的上下文
    ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt)
    defer cancelCtx()
    // 设置r.ctx
    r = r.WithContext(ctx)
    done := make(chan struct{})
    tw := &timeoutWriter{
        w: w,
        h: make(Header),
    }
    // 起一个goroutine来执行原本的逻辑
    go func() {
        h.handler.ServeHTTP(tw, r)
        close(done)
    }()
    // 等待一个通信
    select {
    // 如果没有超时正常返回
    case <-done:
        tw.mu.Lock()
        defer tw.mu.Unlock()
        dst := w.Header()
        for k, vv := range tw.h {
            dst[k] = vv
        }
        if !tw.wroteHeader {
            tw.code = StatusOK
        }
        w.WriteHeader(tw.code)
        w.Write(tw.wbuf.Bytes())
    // 如果超时
    case <-ctx.Done():
        tw.mu.Lock()
        defer tw.mu.Unlock()
        w.WriteHeader(StatusServiceUnavailable)
        io.WriteString(w, h.errorBody())
        tw.timedOut = true
    }
}
复制代码

2. 多路转接器ServeMux

ServeMux是HTTP请求的多路转接器,它会将每一个接收请求的URL与一个注册模式的列表进行匹配,并调用最匹配的模式的处理器, 第一部分的注册HTTP handler的过程实际上是在ServeMux内部的一个哈希表中添加一条记录

2.1 注册流程

ServeMux结构体如下

type ServeMux struct {
	// 读写锁
	mu    sync.RWMutex
	// 管理所有注册路由哈希表
	m     map[string]muxEntry
	// 按pattern长度降序排列的匹配列表, 记录值均以/结尾
	es    []muxEntry
	// 是否存在hosts, 即不以'/'开头的pattern
	hosts bool
}

type muxEntry struct {
	h       Handler
	pattern string
}

var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux
复制代码

上文提到了,每次注册一个HTTP handler最终调用的都是DefaultServeMux.Handle(pattern, handler)方法, 这个方法做的事情很简单,就是维护内部哈希表m,省略部分错误处理代码后源码如下:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()
	
	// 如果注册一个已注册的处理器,将panic
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}
	// 注册
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	// 以斜杠结尾的pattern将存入es切片并按pattern长度降序排列
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}
	// 不以"/"开头的模式将视作存在hosts
	if pattern[0] != '/' {
		mux.hosts = true
	}
}
复制代码

注册流程结束,所以说http.Handle(pattern, handler)只是单纯的在DefaultServeMux的哈希表m中执行注册, 并没有开始分发

2.2 分发

ServeMux结构体也实现了Handler接口,因此它才是真正的分发者!

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		// 对于http协议小于1.1的处理
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set("Connection", "close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
	// 寻找到最接近的HTTP handler
	h, _ := mux.Handler(r)
	h.ServeHTTP(w, r)
}
复制代码

mux.Handler(r)方法始终会返回一个不为空的HTTP handler,其中包含较多的对特殊情况的处理,从源码学习的角度来说,陷入这些分支是不正确的,只应该 考虑最主要的情况进行分析,因此,Handler方法简化为:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
	// 当请求地址为/tree,但只注册了/tree/未注册/tree时,301重定向
	// 此处redirectToPathSlash并没有分析价值,检测一下两者是否在mux.m哈希表中即可
	if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
		return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
	}
    
	return mux.handler(host, r.URL.Path)
}
复制代码

最后的handler才是Handler方法实现的核心,源码如下:

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// 指定主机的模式优于一般的模式
	if mux.hosts {
		h, pattern = mux.match(host + path)
	}
	if h == nil {
		h, pattern = mux.match(path)
	}
	// 如果没有匹配到任何Handler,将返回404 handler
	if h == nil {
		h, pattern = NotFoundHandler(), ""
	}
	return
}
复制代码

假设此时DefaultServeMux注册了两个模式: /a/, /a/b/,此时DefaultServeMux的结构为

{
    m: {
        "/a/": { h: HandlerA, pattern: "/a/" },
        "/a/b/": { h: HandlerB, pattern: "/a/b" },
    },
    es: [{ h: HandlerB, pattern: "/a/b" }, { h: HandlerA, pattern: "/a/" }]
}
复制代码

当请求路径为/a/b/c,将进入第二个if语句,在match方法中进行匹配:

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	// 直接匹配成功的情况
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}
	// 寻找最接近的最长匹配,mux.es切片中包含了所有子树,并降序排列,因此遍历一次即可找出最接近的模式
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}
复制代码

最终路径/a/b/c将返回handlerB

3. Server

Server的部分结构如下:

type Server struct {
    // 监听的地址和端口
    Addr string
    // 所有请求都要调用的Handler
    Handler Handler
    // 读的最大超时时间
    ReadTimeout time.Duration
    // 写的最大超时时间
    WriteTimeout time.Duration
    // 请求头的最大长度
    MaxHeaderBytes int
    ...
}
复制代码

3.1 从启动HTTP server入手

实例代码

http.ListenAndServe(":8001", nil)
复制代码

源码中实际上是创建一个server实例,当handlernil时,将使用DefaultServerMux作为默认的handler, 也就是第二节中提到的"多路转接器",这也是最常见的做法

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}
复制代码

server.ListenAndServe方法中,将调用net.Listen("tcp", addr)监听端口, 不过关于TCP的内容不在分析范围内,直接进入srv.Serve方法

func (srv *Server) ListenAndServe() error {
	addr := srv.Addr
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
复制代码

srv.Serve方法中则会在一个for循环中,完成如下的工作:

  • 调用l.Accept()获得一个新的连接,进行后续操作

  • TCP conn转换为服务端的HTTP conn

  • 启动一个goroutine来处理这个HTTP conn

func (srv *Server) Serve(l net.Listener) error {
	l = &onceCloseListener{Listener: l}
	defer l.Close()

	// 获得根context
	baseCtx := context.Background()
	// 返回一个在根context的基础上添加键为ServerContextKey,值为当前Server引用的context
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
		// 接收一个请求
		rw, e := l.Accept()
		// 将tcp conn转换为http conn
		c := srv.newConn(rw)
		// 启动一个goroutine处理这个请求
		go c.serve(ctx)
	}
}
复制代码

c.serve(ctx)则会进行最后的处理, 此部分比较复杂,其实只需要关心serverHandler{c.server}.ServeHTTP(w, w.req)这一行

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	// HTTP/1.x from here on.
	c.r = &connReader{conn: c}
	c.bufr = newBufioReader(c.r)
	c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
	
	ctx, cancelCtx := context.WithCancel(ctx)
	defer cancelCtx()

	for {
		w, err := c.readRequest(ctx)
		......

		serverHandler{c.server}.ServeHTTP(w, w.req)
		......
	}
}
复制代码

最终也就是调用DefaultServeMux作为整体的HTTP handler

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req)
}
复制代码

OVER

3.2 整体流程


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:hsabalaaaC

查看原文:Go - http.Server源码分析

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

661 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传