从http.ListenAndServe开始看源码(二)

本文深入探讨了GoLang中ServeMux的路由匹配机制,包括pattern的存储与匹配过程,揭示了pattern不以'/'结尾的重要性,以及如何提高存储与匹配效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

handler的存储及匹配

前言

我们在前一节的http ListenAndServe相关的源码中,我们简单提到了在HandleFunc及DefaultServeMux中完成pattern及handler的存储及匹配,具体的过程我们现在详细研究下。

一、ServerMux-结构

ServerMux是路由pattern及handler存储的结构,请求路由的匹配也是在此中的func中完成的。

type ServeMux struct {
    mu    sync.RWMutex//读写锁
    m     map[string]muxEntry//pattern及handler存储map
    hosts bool // whether any patterns contain hostnames(是否包含hostnames)
}

type muxEntry struct {
    explicit bool    //当前pattern是否注册
    h        Handler //我们自定义处理的handler
    pattern  string  //我们定义的pattern
}

ServeMux主要的存储机制就是一个map[string]muxEntry,key存储pattern,value 存储一个包含handler及pattern等信息的muxEntry。mu作为读写锁,为了保证map的读写安全。

二、HandleFunc-存储

http.HandleFunc->DefaultServeMux.HandleFunc->mux.Handle

这几步初步是完成了转入DefaultServeMux处理及handler的HandleFunc转换,然后调用mux.Handle具体处理。

mux.Handle的源码如下:

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()//写操作时加锁
    defer mux.mu.Unlock()//完成后解锁

    if pattern == "" {
        panic("http: invalid pattern " + pattern)
    }
    if handler == nil {
        panic("http: nil handler")
    }
    if mux.m[pattern].explicit {
        panic("http: multiple registrations for " + pattern)
    }

    if mux.m == nil {
        mux.m = make(map[string]muxEntry)//初始化map
    }
    mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}//添加pattern至mux.m中,并以pattern为key

    if pattern[0] != '/' {//如果不以'/'开头,则带有host
        mux.hosts = true
    }

    // Helpful behavior:
    // If pattern is /tree/, insert an implicit permanent redirect for /tree.
    // It can be overridden by an explicit registration.
    //处理'/'结尾的pattern,可以被结尾不带'/'的同名pattern覆盖
    n := len(pattern)
    if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
        // If pattern contains a host name, strip it and use remaining
        // path for redirect.
        path := pattern
        if pattern[0] != '/' {
            // In pattern, at least the last character is a '/', so
            // strings.Index can't be -1.
            path = pattern[strings.Index(pattern, "/"):]//path必须以'/'开头
        }
        url := &url.URL{Path: path}
        mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}//添加不带'/'的pattern的handler,并重定向至带'/'结尾的pattern。如果请求时不带'/',则会重定向至带'/',查看请求的header可以在Referer中查看到原始的请求url
    }
}

总结:

以上就是一个简单的map操作过程,不过添加了对pattern的处理:

1.针对pattern及handler的要求不可为零值,且同一pattern且不可重复添加。

2.直接添加pattern及对应的handler,pattern不以’/'开头则存在host。

3.如果pattern及’/‘结尾,且未存储不带’/‘的同样pattern,则添加不带’/‘的pattern的handler,并重定向至带’/‘结尾的pattern。注意,如果后面还存在不带’/'的pattern,会覆盖此条信息。

三、ServeHTTP-匹配

serverHandler{c.server}.ServeHTTP(w, w.req)->handler.ServeHTTP(rw, req)->(mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

以上几步主要是将server、请求r及响应w经过转换转入ServeMux的ServeHTTP处理(转换的逻辑见之前的分析文章)。

ServeMux的ServeHTTP源码如下:

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {//原始请求URI为*
        if r.ProtoAtLeast(1, 1) {//http协议版本1.1及以上时添加header参数
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)//返回400
        return
    }
    //正常请求
    h, _ := mux.Handler(r)//获取请求对应的handler
    h.ServeHTTP(w, r)//调用handler处理
}

Handler源码如下:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    if r.Method != "CONNECT" {//请求方式非CONNECT时
        if p := cleanPath(r.URL.Path); p != r.URL.Path {//cleanPath获取绝对路径,如果pattern不是绝对路径格式,则重定向绝对路径的格式处理
            _, pattern = mux.handler(r.Host, p)
            url := *r.URL
            url.Path = p
            return RedirectHandler(url.String(), StatusMovedPermanently), pattern
        }
    }
    //绝对路径或CONNECT请求
    return mux.handler(r.Host, r.URL.Path)
}

handler源码如下:

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

    // Host-specific pattern takes precedence over generic ones
    if mux.hosts {//存在host时,匹配路径
        h, pattern = mux.match(host + path)
    }
    if h == nil {//未匹配,则只匹配请求绝对路径
        h, pattern = mux.match(path)
    }
    if h == nil {//仍未匹配,则返回404
        h, pattern = NotFoundHandler(), ""
    }
    return
}

match源码如下:

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    var n = 0
    for k, v := range mux.m {
        if !pathMatch(k, path) {//path是否匹配存储map中的key
            continue
        }
        if h == nil || len(k) > n {//获取最长的匹配的pattern及对应的handler
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}

pathMatch源码如下:

//比较请求存储支持的pattern及请求的路径
// Does path match pattern?
func pathMatch(pattern, path string) bool {
    if len(pattern) == 0 {
        // should not happen
        return false
    }
    n := len(pattern)
    if pattern[n-1] != '/' {//不以'/'结尾则必须相同才能匹配
        return pattern == path
    }
    return len(path) >= n && path[0:n] == pattern//处理以'/'结尾的patten比较,path必须以pattern开头,注意此处,只要求请求的path以pattern为前缀即可,即/tree/list的请求会匹配到/tree/,所以如果要求精确匹配pattern尽量不以'/'结尾
}

总结:

以上调用过程虽然看起来长,实际上也还是map的取值过程O(∩_∩)O哈哈~。主要针对请求url的处理如下:

1.如果存在host,先匹配host+path;

2.如果不存在host或者host匹配结果为空,则匹配path。

具体的匹配逻辑:

1.如果存储的pattern不是以’/'结尾,则请求path必须与pattern完全一致才能匹配成功。

2.如果存储的pattern是以’/'结尾,则请求path必须以pattern为前缀即可,此处会存在多个匹配的情况,如:

/tree/list/及/tree/的请求都会匹配到/tree/这个pattern。

3.如果存在多个匹配的情况,则以匹配到最长的pattern(匹配度最高)为最终匹配结果。

如:存在/tree/list/这个pattern时,请求/tree/list/最终会匹配到/tree/list/,虽然中间也会匹配到/tree/这个pattern。

四、总结

golang底层的pattern的存储与匹配是基于简易的map结构来完成,其实第三方的基于golang的网络架构也是基于这一套,不一样的是map的复杂度及pattern的自由度等地方,如果你感兴趣的话,不妨去看看一些框架的源码。

从以上的源码分析中,我们可以了解到对我们代码有益的地方,如:

1.使用HandleFunc时pattern尽量不以’/'结尾,可以调高存储及匹配的效率,同时,可以避免误匹配的情况。

2.map存在读写都有的情况,使用读写锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值