Go的HTTP协议中Server端的具体实现源码

0.前言

该笔记为笔者第一次学习go的net/http包源码的时候所记,也许写的并不是很精确,希望大家多多包涵,一起讨论学习。

该笔记很大程度的参考了网名为“小徐先生”的前辈所分享的博客,推荐大家可以先看一看它的博客来一起学习,我的只是照葫芦画瓢而已。

当前笔者追溯的是go版本为1.22.2的源码,对于中间看不太懂的片段,大家可以先看在文末我画的总流程图,再跟着流程图一段段来看。

参考博客链接:Golang HTTP 标准库实现原理 (qq.com)

1.核心数据结构:ServerMux

ServeMux 是 Go 标准库中的 HTTP 请求多路复用器(HTTP request multiplexer),负责将传入的 HTTP 请求路由到相应的处理程序。ServeMux 会根据注册的 URL 模式匹配传入的请求,并调用最合适的处理程序。以下是它的详细作用和 Go 1.22 带来的变化:

1. ServeMux 的主要功能

  • 路由匹配ServeMux 根据请求的 URL 和注册的模式(patterns)进行匹配,并根据匹配结果调用对应的处理函数。模式可以包含方法、主机名和路径的组合。例如:
    • "/index.html" 匹配路径为 /index.html 的任何主机和方法的请求。
    • "GET /static/" 匹配方法为 GET,路径以 /static/ 开头的请求。
    • "example.com/" 匹配主机为 example.com 的任何请求。
  • 通配符支持ServeMux 允许使用通配符匹配 URL 路径的一部分,如 "/b/{bucket}/o/{objectname...}",可以匹配路径中特定的部分。
  • 优先级机制:当多个模式匹配同一个请求时,ServeMux 会选择最具体的模式。例如,"/images/thumbnails/""/images/" 更具体,因此会优先匹配。
  • 尾部斜杠重定向ServeMux 会自动处理路径末尾的斜杠,如果注册了某个路径下的子树,ServeMux 会将缺少尾部斜杠的请求重定向到带有尾部斜杠的路径。
  • 请求清理ServeMux 会清理 URL 路径,例如移除 ... 等特殊路径,确保路径的规范化。

2. Go 1.22 的变更

Go 1.22 对 ServeMux 的模式匹配规则做出了重要的改动,与 Go 1.21 版本相比有若干不兼容的变化。以下是主要变化:

  • 通配符:在 Go 1.21 中,通配符被当作普通的文字路径段处理,比如 "/{x}" 只匹配与之完全相同的路径,而在 Go 1.22 中,它可以匹配任意一个路径段。
  • 模式验证:Go 1.22 开始会对模式的语法进行严格检查,任何无效的模式在注册时会导致程序 panic。例如,"/{""/a{x}" 在 1.21 中是合法的模式,但在 1.22 中会被认为是无效的。
  • 转义处理:Go 1.22 开始,每个路径段都会被解码。之前的版本对整个路径进行转义,而不是逐段处理。例如,"/%61" 在 1.22 中会匹配路径 "/a",但在 1.21 中只会匹配 "/%2561"
  • 匹配方式的改变:Go 1.22 会逐段解码路径,并处理 %2F 等字符的转义,这对于处理带有 %2F 的路径来说,与之前的版本有很大的不同。

如果需要保留 Go 1.21 的行为,可以通过设置环境变量 GODEBUG=httpmuxgo121=1 来恢复旧的模式匹配逻辑。

具体例子

以下代码展示了如何使用 ServeMux 并注册不同的路由:

mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/static/", staticHandler)
mux.HandleFunc("/b/{bucket}/o/{objectname...}", objectHandler)

http.ListenAndServe(":8080", mux)

在这个例子中,不同的 URL 模式会被路由到相应的处理函数,Go 1.22 的变化会影响像 "/b/{bucket}/o/{objectname...}" 这种通配符模式的匹配方式。

这些变更旨在提高模式匹配的灵活性和一致性,同时使得 ServeMux 在复杂的路由需求下更具可扩展性。

3.ServeMux结构分析

先来看ServeMux的数据结构

type ServeMux struct {
	mu       sync.RWMutex //互斥锁
	tree     routingNode //存储pat对应的handler
	index    routingIndex //用于进行路由冲突检测
	patterns []*pattern  // TODO(jba): remove if possible(类似与pat的副本,在未来可能会删除)
	mux121   serveMux121 // used only when GODEBUG=httpmuxgo121=1
}

为了进一步了解它内嵌的结构,我们先来观看routingNode的源码实现。

2.routingNode

// A routingNode is a node in the decision tree.
// The same struct is used for leaf and interior nodes.
type routingNode struct {
	// 一个页子节点持有一个pat和它注册的handler。
	pattern *pattern
	handler Handler
	// special children keys:
	//     "/"	trailing slash (resulting from {$})
	//	   ""   single wildcard
	//	   "*"  multi wildcard
	children   mapping[string, *routingNode] //将路径的一部分映射到子节点中,形成一个路由树。
	emptyChild *routingNode // 一个优化字段,用于快速访问键为 "" 的子节点,减少查找开销。
}

对于三个special children keys的解释:

  • 对于“/”,它处理了路径中以斜杠为结尾的情况,例如当一个路由模式以斜杠结尾(如 "/images/"),ServeMux 会自动处理不带斜杠的请求(如 "/images") 并将其重定向到带有斜杠的路径("/images/"
  • “”作为一个匹配单个路径段的通配符
  • “*”作为一个匹配多个路径段的通配符

2.1addPattern

//addPattern方法添加一个pattern和它的handler到树中
func (root *routingNode) addPattern(p *pattern, h Handler) {
	// First level of tree is host.
	n := root.addChild(p.host) //先获取host对应的routingNode
	// Second level of tree is method.
	n = n.addChild(p.method) //接着再获取method对应的routingNode
	// Remaining levels are path.
	n.addSegments(p.segments, p, h)
}

2.2addChild

//向一个 routingNode (路由节点)添加一个子节点,并返回该子节点。如果节点已经存在,则直接返回;如果不存在,则创建一个新的子节点并返回。
func (n *routingNode) addChild(key string) *routingNode {
	if key == "" { //路径为空,如果存在emptyChild则直接返回作为一个单项通配符,如果不存在则创建并返回。
		if n.emptyChild == nil {
			n.emptyChild = &routingNode{}
		}
		return n.emptyChild
	}//查找是否存在对应的pat
	if c := n.findChild(key); c != nil {
		return c
	}
	c := &routingNode{}
	n.children.add(key, c)
	return c
}

我们再来看对应的findChild方法的实现

// findChild returns the child of n with the given key, or nil
// if there is no child with that key.
func (n *routingNode) findChild(key string) *routingNode {
	if key == "" {
		return n.emptyChild
	}
	r, _ := n.children.find(key)
	return r
}

可以看到,最终是直接调用了map去寻找是否存在对应的value。

2.3addSegements

// addSegments 的作用是将路径中的各个段(segments)逐步添加到路由树中,并最终将模式 (pattern) 和处理器 (handler) 设置在路由树的叶子节点。
func (n *routingNode) addSegments(segs []segment, p *pattern, h Handler) {
	if len(segs) == 0 { //如果segs为空,表示当前为一个叶子节点,此时就将p和h绑定在该节点上
		n.set(p, h)
		return
	}
	seg := segs[0]
	if seg.multi { //如果此时当前的segment是多段通配符,通常为*
		if len(segs) != 1 { //检查当前是否为最后一段,因为多段通配符必须是路径的最后一段。
			panic("multi wildcard not last")
		}
		n.addChild("*").set(p, h) //绑定
	} else if seg.wild { //若是单段通配符,则进一步递归
		n.addChild("").addSegments(segs[1:], p, h)
	} else { //同理
		n.addChild(seg.s).addSegments(segs[1:], p, h)
	}
}

在这里插入图片描述

3.routingIndex

//routingIndex是一个用于优化路由冲突检测的数据结构
//它的检测思想是通过排除对于给定的pattern,不可能与之冲突的部分,只检测可能产生冲突的部分来达到快速检测的目的。
type routingIndex struct {
	segments map[routingIndexKey][]*pattern
	multis []*pattern
}
type routingIndexKey struct {
	pos int    // 0-based segment position
	s   string // literal, or empty for wildcard
}
  • sgements:记录了每一个routingIndexKey注册的pattern组,routingIndexKey包含两个字段,pos标识了s出现在pattern中的段的位置。例如,对于一个key{1,“b”},它对应了pattern为"/a/b"、“/a/b/c”,因为b处在第1段。
  • multis:用于存储所有以多段通配符为结尾的patterns

3.1addPattern

func (idx *routingIndex) addPattern(pat *pattern) {
	if pat.lastSegment().multi { //若以多段通配符结尾,则直接添加
		idx.multis = append(idx.multis, pat)
	} else {
		if idx.segments == nil { //若第一次注册,先初始化
			idx.segments = map[routingIndexKey][]*pattern{}
		}
		for pos, seg := range pat.segments {
			key := routingIndexKey{pos: pos, s: ""}
			if !seg.wild {
				key.s = seg.s //非通配符就赋值
			}
			idx.segments[key] = append(idx.segments[key], pat) //添加pat
		}
	}
}

3.2possiblyConflictingPatterns

//该函数会调用方法f去对所有可能与pat冲突的pattern进行检测,如果f返回一个非空的错误,那么possiblyConflictingPatterns会立即返回这个错误
func (idx *routingIndex) possiblyConflictingPatterns(pat *pattern, f func(*pattern) error) (err error) {
	//一个辅助函数,用于对所有的pats进行检测
	apply := func(pats []*pattern) error {
		if err != nil {
			return err
		}
		for _, p := range pats {
			err = f(p)
			if err != nil {
				return err
			}
		}
		return nil
	}

	//首先对多段通配符进行进测,因为这些pattern可能与任何传入的pat冲突
	if err := apply(idx.multis); err != nil {
		return err
	}
    //处理以/为结尾的路径段
	if pat.lastSegment().s == "/" {
		return apply(idx.segments[routingIndexKey{s: "/", pos: len(pat.segments) - 1}])
	}
	//如果模式不是以/为结尾,那么函数会检测与它的相同位置有相同的字面值或是单段通配符的模式,
    //通过遍历模式的每一个段,函数寻找可能冲突的模式,并在可能冲突最少的段上进行匹配
    //函数在查找时会计算每个段位置的匹配模式数量,并选择pattern数量最少的段进行匹配,这样可以优化冲突检测的效率。
	var lmin, wmin []*pattern //分别存储literal匹配的pattern集合和通配符wildcard匹配的pattern集合
	min := math.MaxInt
	hasLit := false //标识是否遇到了literal
	for i, seg := range pat.segments {
		if seg.multi { //跳过多段匹配符,因为已经在上面检测过
			break
		}
		if !seg.wild { //对于非通配符
			hasLit = true
            //索引中查找所有与当前字面值 seg.s 和位置 i 匹配的模式集合(lpats)
			lpats := idx.segments[routingIndexKey{s: seg.s, pos: i}]
            //查找所有与位置 i 匹配的通配符模式集合(wpats)
			wpats := idx.segments[routingIndexKey{s: "", pos: i}]
			if sum := len(lpats) + len(wpats); sum < min {
				lmin = lpats
				wmin = wpats
				min = sum
			}
		}
	}
	if hasLit {
        //对可能的冲突进行匹配检测
		apply(lmin)
		apply(wmin)
		return err
	}

	//该pattern是由通配符组成,需要检查和任意的pat会不会冲突
	for _, pats := range idx.segments {
		apply(pats)
	}
	return err
}

该方法的主要作用是优化了冲突检测的效率,当我们需要为一个路由注册handler的时候,会先检查是否存在路由模式匹配冲突,这时候使用该方法可以加快检测效率。

4.循序渐进学Serve服务

在main函数中,使用以下几行代码即可启动一个Server

func main() {
    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("pong"))
    })


    http.ListenAndServe(":8091", nil)
}

我们调用了HandleFunc函数,为"/ping"这一个路由注册了一个处理函数,然后通过ListenAndServe启动监听,通过这个实例,我们跟进HandleFunc查看具体的源码过程。

4.1HandlerFunc函数实现

// HandleFunc registers the handler function for the given pattern in [DefaultServeMux].
// The documentation for [ServeMux] explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if use121 {
		DefaultServeMux.mux121.handleFunc(pattern, handler)
	} else {
		DefaultServeMux.register(pattern, HandlerFunc(handler))
	}
}

当调用公开的函数HandleFunc时,会默认将路由注册到context包下的DefaultServeMux中,它是一个ServeMux的具体实现。

本笔记探讨的是1.22版本开始的Server模式,于是跟进register函数查看。

func (mux *ServeMux) register(pattern string, handler Handler) {
	if err := mux.registerErr(pattern, handler); err != nil {
		panic(err)
	}
}

func (mux *ServeMux) registerErr(patstr string, handler Handler) error {
	if patstr == "" {
		return errors.New("http: invalid pattern")
	}
	if handler == nil {
		return errors.New("http: nil handler")
	}
	if f, ok := handler.(HandlerFunc); ok && f == nil {
		return errors.New("http: nil handler")
	}

	pat, err := parsePattern(patstr) //将patstr转换为标准的pattern结构
	if err != nil {
		return fmt.Errorf("parsing %q: %w", patstr, err)
	}

	// Get the caller's location, for better conflict error messages.
	// Skip register and whatever calls it.
    
    //这里使用了runtime.Caller函数来获取调用这个函数的代码位置(即调用者的信息),主要是为了在发生错误时,能在错误信息中提供更准确的源代码位置。这可以帮助开发者更方便地定位问题。
    //runtime.Caller()中的参数3表示获取调用栈中第三层的调用信息,在这里层级的顺序大概是
    //1.runtimer.Caller本身
    //2.当前方法registerErr的调用
    //3.调用registerErr的函数
	_, file, line, ok := runtime.Caller(3)
	if !ok {
		pat.loc = "unknown location"
	} else {
		pat.loc = fmt.Sprintf("%s:%d", file, line)
	}

    //以下方法用于检测新注册的路由pat是否会和已有的路由pat2存在冲突的逻辑
	mux.mu.Lock()
	defer mux.mu.Unlock()
	// Check for conflict.
	if err := mux.index.possiblyConflictingPatterns(pat, func(pat2 *pattern) error {
		if pat.conflictsWith(pat2) {
			d := describeConflict(pat, pat2)
			return fmt.Errorf("pattern %q (registered at %s) conflicts with pattern %q (registered at %s):\n%s",
				pat, pat.loc, pat2, pat2.loc, d)
		}
		return nil
	}); err != nil {
		return err
	}
    //若不存在冲突,则为对应的pat注册handler方法
	mux.tree.addPattern(pat, handler) 
	mux.index.addPattern(pat)
	mux.patterns = append(mux.patterns, pat)
	return nil
}

4.2启动Serve服务的ListenAndServe

调用net/http包下的公开的ListenAndServe函数,就可以实现对服务端的一键启动功能,跟进该函数看看它都做了什么

1.ListenAndServe

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

该方法创建了一个Server实例,并且调用该server的ListenAndServe方法,Server实例中若handler为nil,则自动调用DefaultServeMux。

2.server.ListenAndServe

func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http" //若srv.Addre为“”,则默认为此
	}
	ln, err := net.Listen("tcp", addr)//启用TCP监听
	if err != nil {
		return err
	}
	return srv.Serve(ln)
}

该方法使用TCP连接Server中的地址,并且使用该实例中注册的Handler去处理业务

3.server.Serve

var ServerContextKey = &contextKey{"http-server"}

type contextKey struct {
    name string
}
func (srv *Server) Serve(l net.Listener) error {
	// ...
   ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, err := l.Accept()
        // ...
        connCtx := ctx
        // ...
        c := srv.newConn(rw)
        // ...
        go c.serve(connCtx)
    }
}

Serve方法是go进行服务端处理连接的核心方法,在这段核心片段中,首先先创建了一个带有键值对的上下文ctx,其中键是ServerContextKey,对应着一个服务器实例srv。

接着使用for循环的方式,负责监听并接受来自Listener的连接(l.Accept()),每一次的循环都会尝试从监听器中获取一个新的连接rw。

每一个连接都会关联一个connCtx,在默认的情况它就是之前创建的ctx

使用srv.newConn创建了一个新的连接对象c,封装了rw作为http连接,最后启动了一个新的协程异步处理该连接的请求,这个c.serve方法会处理来自客户端HTTP的请求。

可以看到,整体方法的思路是采用for循环监听连接,并且为之分配一个协程进行处理。

在这里插入图片描述

4.conn.Serve

接着我们来看conn.Serve,它作为实际处理请求的核心,非常重要,它的整个方法代码比较长,我们分成几部分,着重看请求处理的部分。

第一个部分,进行设置上下文ctx和异常处理

ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
var inFlightResponse *response
defer func() {
    if err := recover(); err != nil && err != ErrAbortHandler {
        const size = 64 << 10
        buf := make([]byte, size)
        buf = buf[:runtime.Stack(buf, false)]
        c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
    }
    if inFlightResponse != nil {
        inFlightResponse.cancelCtx()
    }
    if !c.hijacked() {
        if inFlightResponse != nil {
            inFlightResponse.conn.r.abortPendingRead()
            inFlightResponse.reqBody.Close()
        }
        c.close()
        c.setState(c.rwc, StateClosed, runHooks)
    }
}()

使用了defer设置了一个错误处理机制,如果在请求期间发生了panic,会捕获错误并且记录日志。

接着第二个部分为处理TLS(HTTPS)连接

if tlsConn, ok := c.rwc.(*tls.Conn); ok {
    tlsTO := c.server.tlsHandshakeTimeout()
    if tlsTO > 0 {
        dl := time.Now().Add(tlsTO)
        c.rwc.SetReadDeadline(dl)
        c.rwc.SetWriteDeadline(dl)
    }
    if err := tlsConn.HandshakeContext(ctx); err != nil {
        if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
            io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
            re.Conn.Close()
            return
        }
        c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
        return
    }
    ...
}

该代码段进行TLS握手,若握手成功便会记录TLS的状态。

接着来到了读取并且处理HTTP请求的阶段

ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()

c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

对于每一个连接,都会分配一个新的读取器bufr和写入器bufw,提高读取和写入的效率

请求处理循环

for {
    w, err := c.readRequest(ctx)
    if err != nil {
        // 错误处理,比如请求过大或不支持的传输编码
    }

    // 如果 Expect: 100 continue,支持继续
    if req.expectsContinue() { ... }

    // 处理请求,调用服务器的处理函数
    serverHandler{c.server}.ServeHTTP(w, w.req)

    // 完成请求并判断是否需要复用连接
    w.finishRequest()
    if !w.shouldReuseConnection() {
        return
    }

    // 设置连接为空闲状态
    c.setState(c.rwc, StateIdle, runHooks)
    c.curReq.Store(nil)
}

  • 这个循环会反复读取请求、处理请求、并且发送响应
  • 使用serverHandler.ServeHTTP来调用对应的处理程序,比如用户设置的Server或者DefaultServerMux

最终处理完请求会检查是否可以复用连接,如果不能复用(例如客户端请求 Connection: close),则关闭连接。

5.serverHandler.ServeHTTP

在ServeHTTP方法中,会对Handler做判断,倘若没有声明,则取全局单例DefaultServeMux进行路由匹配

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)
}

6.ServeMux.ServeHTTP

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.pat, r.matches = mux.findHandler(r)
	}
	h.ServeHTTP(w, r)
}
  • 首先处理了特殊的RequestURI为“*”的请求
  • 接着对于1.21版本开始的go,它会去查找request对应的Handler,返回对应的pattern和匹配结果

一旦找到了与请求路径匹配的处理器 h,调用该处理器的 ServeHTTP 方法来处理当前请求。

7.ServeMux.findHandler

// ...
host = stripHostPort(r.Host)
path = cleanPath(path)
n, matches, u = mux.matchOrRedirect(host, r.Method, path, r.URL)
// ...
return n.handler, n.pattern.String(), n.pattern, matches
  • 通过 stripHostPort 去掉主机名中的端口信息,然后使用 cleanPath 净化请求路径,去除多余的 /.
  • 调用 mux.matchOrRedirect 进行匹配,如果匹配成功,则返回相应的路由节点 (n) 和匹配的参数 (matches)
  • 最终会返回对应的handler

8.ServeMux.matchOrRedirect

func (mux *ServeMux) matchOrRedirect(host, method, path string, u *url.URL) (_ *routingNode, matches []string, redirectTo *url.URL) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()
	n, matches := mux.tree.match(host, method, path)
	//...
	return n, matches, nil
}

核心是调用了routingNode的match方法去找到对应的node节点。

5.回顾总结(可视化总流程)

我们以下面的main函数为例子,再重新回顾一下启动服务器的具体流程

func main() {
	http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("pong"))
	})

	http.ListenAndServe(":8091", nil)
}

首先我们为"/ping"路径注册了一个handler方法,go会自动将我们写的func转换为实现了Handler接口的HandlerFunc类型,并且将其添加到默认分路器的节点中。

DefaultServeMux.register(pattern, HandlerFunc(handler)) //将我们写的func转换为HandlerFunc

在这里插入图片描述

HandlerFunc和ServeMux都实现了Handler接口,所以都可以被看作为handler,handler用于处理一个request。

在这里插入图片描述

接着,我们启用监听,假设在监听中,我们访问了localhost:8091/ping,最终流程如下。

在这里插入图片描述

最终我们启动main服务,在网页打开localhost:8091/ping,就能看到网页输出pong啦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值