4

Author:Wzy_CC

全文一共10000字

阅读时间10~15分钟

前言

本文目标:

从路由注册到监听本地端口后请求路由的一系列动作的分析,基本上仅限于net/http server.go这个包文件的路由相关部分解读

写作目的:

在使用原生库进行web开发的时候,很多初学者很容易被mux.Handle()/mux.HandleFunc()/mux.Handler()/Handlerfunc/Handler/Handle()/Handlefunc()/handler给唬住,本身几个名称就相近,首字母有时候大写有时候小写,有时候是handle,有时候是handler,看起来相似但是类型和作用却完全不同。因为命名相似容易混淆,因此其真实含义也不容易搞清楚,对于开发者来说也不容易记忆 。有些命名甚至看不出来这个函数到底是干什么用的,有些属于设计库的时候的历史遗留问题,使得理解http库变得更加困难。

很多网上的教程只是讲了某些东西是什么,用来干什么的,而没有讲为什么是这样的,为什么要这样设计,这样设计有什么好处。更重要的是有些教程已经老了,2018年到现在已经两年了,很多函数都经过优化重写了,至少server.go中的很多函数变化都很大,2018年的教程很多已经过时了,可能2022年又需要重新写一篇http库的解读。不过有些东西是不变的,很多设计思想都是共通的,而这些思想才是初学者应该掌握的。事实上死记硬背掌握handle的四种写法对开发没有任何帮助,如果不深入理解以后还会经常性的把文档翻来翻去而一头雾水。

Go的有些设计哲学很有趣,不是简简单单几万字的篇幅就可以讲明白的。

阅读顺序:

本文按照顺序阅读会比较有帮助

目录

[TOC]

概述

官网示例

在官网示例中,使用go搭建一个稳定的高并发web服务器仅需要短短几行:

http.Handle("/foo", fooHandler)

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

log.Fatal(http.ListenAndServe(":8080", nil))

当然对于大部分开发来说,这几行代码已经足够在生产环境中使用了,但是如果对比较底层的一些原理不解,那么还需要继续深究。

一个go服务器正常运行起来的步骤大致有:注册函数、监听端口,接受请求,处理请求,提供服务,关闭链接

0.注册函数:首先往路由表中注册对应的路由规则

1.监听端口:创建listen socket,循环监听

2.接受请求:接受请求,创建网络链接conn对象,开启一个协程处理该链接(估计多路复用复用在这里了)每服务一个新的链接,在conn.connect()中就会调用serveHTTP来处理请求

3.处理请求:读取请求参数构造Request对象,根据请求路径在map路由表中查找对应的Handler。然后把请求分配给处理函数

4.提供服务:处理函数根据请求的参数等信息做处理,返回不同的信息

5.关闭链接:应用层处理完请求后关闭链接

前置知识

Go基础语法、web基础、*压缩字典树

源码分析范围/大纲

主要分析net/http库中的server.go文件,但是篇幅有限重点分析(使用mux.XX()简化代替ServeMux.XX()):

1.ServeMux结构体及其方法:mux.NewServeMux()mux.Handle()mux.HandleFunc()mux.Handler()/mux.handler()mux.ServeHTTP()

2.HandlerFunc结构体及其实现方法:HandlerFunc.ServeHTTP()

3.Handler接口类型

4.函数Handle()和函数HandleFunc()

路由部分就这么点东西

ServeMux

ServeMux是一个结构体

ServeMux定义

ServeMux是一个HTTP请求多路复用器。它根据注册模式列表(路由表)将每个传入请求的URL匹配,并为与URL最匹配的模式调用处理程序(handler)。

type ServeMux struct {
    // contains filtered or unexported fields
}

结构体内黑盒,包含已过滤和未导出的字段,其实就是不想让你知道里面的构造,事实上的构造如下:

type ServeMux struct {
    mu    sync.RWMutex          // 读写互斥锁
    m     map[string]muxEntry   // 路由表
    es    []muxEntry            // 有序数组,从最长到最短排序
    hosts bool                  // whether any patterns contain hostnames
}

ServeMux结构体本质上是由mu读写互斥锁、m路由表、es数组(很多老教程都没有这个更新字段)和hosts布尔值组成

其中:

1.mu是读写互斥锁,详情见设计思想

2.m是路由表,路由表本质上就是一个map[string]muxEntry变量,键是路径字符串(由method和传入参数拼接字符串组成),值是对应的处理结构体muxEntry

3.es是一个有序数组,由长到短维护所有后缀为/的路由地址,至于为什么要这样设计,见设计思想

4.布尔类型的hosts属性标记路由中是否带有主机名,若hosts值为true,则路由的起始不能为/

m路由表中的muxEntry的结构体如下:

type muxEntry struct {
    h        Handler // 处理程序
    pattern  string  // 路由路径
}

muxEntry本质上是由Handler类和路由路径字符串组成,其中:

1.h是Handler类型,Handler类型要求实现接口中的ServeHTTP方法

2.pattern实际上和路由表中的key相同

默认多路复用器

net/http包中规定了默认的多路复用器,如果不自己手写指定则使用默认mux:

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

这里为什么可以在声明前使用变量?Dave Cheney告诉我包级别的变量与声明顺序无关,还告诉我这种问题以后去slack上自己问,编译器做初始化工作的时候会首先初始化包级别的变量,因此无论声明在哪里都可以使用。

ServeMux方法

公有方法

mux.NewServeMux()

// NewServeMux allocates and returns a new ServeMux.
func NewServeMux() *ServeMux { return new(ServeMux) }

新建并返回一个ServeMux结构体,为结构体内字段分配空间。值得注意的是,初始化并返回的结构体字段hosts默认值为false

mux.Handler()

对于给定的请求,mux.Handler()总是返回非空Handler来使用。如果方法是CONNECT则见私有方法mux.redirectToPathSlash()

mux.Handler()调用私有方法mux.handler(),在mux.handler()内部调用了mux.match()方法来返回匹配pattern的handler

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

    // CONNECT requests are not canonicalized.
    if r.Method == "CONNECT" {
        // If r.URL.Path is /tree and its handler is not registered,
        // the /tree -> /tree/ redirect applies to CONNECT requests
        // but the path canonicalization does not.
        if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
            return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
        }

        return mux.handler(r.Host, r.URL.Path)
    }

    // All other requests have any port stripped and path cleaned
    // before passing to mux.handler.
    host := stripHostPort(r.Host)
    path := cleanPath(r.URL.Path)

    // If the given path is /tree and its handler is not registered,
    // redirect for /tree/.
    if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
        return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
    }

    if path != r.URL.Path {
        _, pattern = mux.handler(host, path)
        url := *r.URL
        url.Path = path
        return RedirectHandler(url.String(), StatusMovedPermanently), pattern
    }

    return mux.handler(host, r.URL.Path)
}

对于r.hostr.URL.Path进行了简单处理,简要说明一下两个函数cleanPath()stripHostPort()分别做了什么工作:

cleanPath()

1.处理无效路由

2.对于斜杠的处理,代替无效的多个斜杠

3.移除所有的.替换为等效path

简单来说就是对路径进行处理为等效最短路径,使之可以在后续查找路由表的过程中可以查找到相应键值对。

// cleanPath returns the canonical path for p, eliminating . and .. elements.
func cleanPath(p string) string {
    if p == "" {
        return "/"
    }
    if p[0] != '/' {
        p = "/" + p
    }
    np := path.Clean(p)
    // path.Clean removes trailing slash except for root;
    // put the trailing slash back if necessary.
    if p[len(p)-1] == '/' && np != "/" {
        // Fast path for common case of p being the string we want:
        if len(p) == len(np)+1 && strings.HasPrefix(p, np) {
            np = p
        } else {
            np += "/"
        }
    }
    return np
}

stripHostPort()

就是对host格式的规范

// stripHostPort returns h without any trailing ":<port>".
func stripHostPort(h string) string {
    // If no port on host, return unchanged
    if strings.IndexByte(h, ':') == -1 {
        return h
    }
    host, _, err := net.SplitHostPort(h)
    if err != nil {
        return h // on error, return unchanged
    }
    return host
}

mux.ServeHTTP()

mux.ServeHTTP()给最能匹配请求URL的handler发出请求,最后调用实现ServeHTTP()方法的Handler类型的Handler.ServeHTTP()来处理请求

有点绕,总之HTTP的请求首先由mux.ServeHTTP()进行处理,在该函数内部调用了mux.Handler()(见私有方法mux.handler())来选择处理程序handler(在这个过程中调用了mux.handler()/mux.RedirectHandler()来查找路由表,最后在mux.handler()的内部调用了mux.match()来最后对路由进行匹配查找返回Handler类型的h)

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
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
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

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")
    }
    if handler == nil {
        panic("http: nil handler")
    }
    if _, exist := mux.m[pattern]; exist {
        panic("http: multiple registrations for " + pattern)
    }

    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    e := muxEntry{h: handler, pattern: pattern}
    mux.m[pattern] = e
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e)
    }

    if pattern[0] != '/' {
        mux.hosts = true
    }
}

对于路由在表中重复会引发panic,对于后缀为slash/的路径,按照长度大小写入mux.es中,之前分析结构体mux时也提到过这一点。

简单看一下其实现的排序函数:

func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
    n := len(es)
    i := sort.Search(n, func(i int) bool {
        return len(es[i].pattern) < len(e.pattern)
    })
    if i == n {
        return append(es, e)
    }
    // we now know that i points at where we want to insert
    es = append(es, muxEntry{}) // try to grow the slice in place, any entry works.
    copy(es[i+1:], es[i:])      // Move shorter entries down
    es[i] = e
    return es
}

mux.HandleFunc()

mux.HandleFunc()是我认为最重要的一个方法,同样是将handler注册到路由表中,我们应该对比mux.HandleFunc()mux.Handle()的区别,其实从函数体来看mux.HandleFunc()算是对mux.Handle()函数的一个再封装,调用了HandlerFunc())这个适配器函数,本质上是将一个普通函数作为HTTP请求handler的语法糖,我们不再需要实现ServeHTTP()方法,取而代之的是传入的普通函数只要为func(ResponseWriter, *Request)类型的,就可以进行函数的路由,基本上一行代码就可以搞定,这也是为什么在官网示例中我们可以轻而易举的构建简单的web程序的原因。在官网示例的HandleFunc()函数中调用了默认复用器的DefaultServeMux.HandleFunc()方法,开发者只需要自己定义普通函数即可:

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

私有方法

mux.match()

当调用mux.Handler()返回Handler类时在mux.handler()内部会调用mux.match()函数,本质上可以看作是路由查找的过程(Handle()是路由注册的过程)

// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // Check for exact match first.
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // Check for longest valid match.  mux.es contains all patterns
    // that end in / sorted from longest to shortest.
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

在进行匹配的过程中:

1.首先在路由表中进行精确匹配,匹配到muxEntry后返回

2.如果在路由表中没有查询到,则在有序数组es中进行匹配,从strings.HasPrefix()可以看出,本质上这是一种模糊匹配,只匹配了相应的前缀,就认定匹配成功

3.如果相应前缀无法查询,则认为匹配失败,返回nil handler

总结匹配规则一句话描述是: Longer patterns take precedence over shorter ones,长字符串模式优先级大于短字符串模式,优先匹配长字符串

mux.shouldRedirectRLocked()

mux.shouldRedirectRLocked()方法的作用较为简单,判断是否需要对像"/tree/"这种路由的重定向(在ServeMux中对于"/tree"会自动重定向到"/tree/",除非路由表中已有"/tree",此过程在mux.Handler()中调用mux.redirectToPathSlash()完成)

1.判断路由表中是否存在host+path或者path的组合,如果存在则不需要重定向

2.如果path为空字符串,则不需要重定向

3.如果当前路由表中存在path+“/”,则需要重定向(例如在注册时将"/tree/"注册到表中,则对于"/tree"的路由重定向到了"/tree/",)

func (mux *ServeMux) shouldRedirectRLocked(host, path string) bool {
    p := []string{path, host + path}

    for _, c := range p {
        if _, exist := mux.m[c]; exist {
            return false
        }
    }

    n := len(path)
    if n == 0 {
        return false
    }
    for _, c := range p {
        if _, exist := mux.m[c+"/"]; exist {
            return path[n-1] != '/'
        }
    }

    return false
}

mux.redirectToPathSlash()

mux.redirectToPathSlash()函数确定是否需要在其路径后附加"/",一旦判断需要添加"/"则返回新的url和被重定向后的handler:

func (mux *ServeMux) redirectToPathSlash(host, path string, u *url.URL) (*url.URL, bool) {
    mux.mu.RLock()
    shouldRedirect := mux.shouldRedirectRLocked(host, path)
    mux.mu.RUnlock()
    if !shouldRedirect {
        return u, false
    }
    path = path + "/"
    u = &url.URL{Path: path, RawQuery: u.RawQuery}
    return u, true
}

判断应该重定向后,返回结尾带有/的路径

mux.Handler()中,如果Http.Method为CONNECT,则会返回RedirectHandler(也是Handler类型的一种)写入StatusMovedPermanently(见status.go中的定义),调用RedirectHandler.ServeHTTP()来对HTTP请求进行处理

    if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
            return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
        }

        return mux.handler(r.Host, r.URL.Path)

mux.handler()

mux.handler()函数是mux.Handler()的一种实现

// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
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 {
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}

进行两种匹配后都没有找到相应的handler后,返回NotFoundHandler()

总结

Go 其实支持外部实现的路由器 ListenAndServe 的第二个参数就是用以配置外部路由器的,它是一个 Handler 接口,即外部路由器只要实现了 Handler 接口就可以,我们可以在自己实现的路由器的 ServeHTTP 里面实现自定义路由功能

HandleFunc()

HandleFunc()是函数

HandleFunc定义

对于给定的模式字符串,HandleFunc将handler函数注册到相应的路由上。换句话说,当对不同的url路径请求时,给出不同的处理逻辑,而HandleFunc可以实现这种将处理逻辑和url绑定的关系。

函数定义:

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

第一个参数是字符串,第二个参数是handler,HandleFunc处理匹配到的url路径请求。

HandleFunc()本质上调用了默认复用器的mux.HandleFunc()

例子:

package main

import (
    "io"
    "log"
    "net/http"
)

func main() {
    h1 := func(w http.ResponseWriter, _ *http.Request) {
        io.WriteString(w, "Hello from a HandleFunc #1!\n")
    }
    h2 := func(w http.ResponseWriter, _ *http.Request) {
        io.WriteString(w, "Hello from a HandleFunc #2!\n")
    }

    http.HandleFunc("/", h1)
    http.HandleFunc("/endpoint", h2)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

HandleFunc优势

HandleFunc函数的存在使得我们可以直接将一个func(ResponseWriter, *Request)类型的函数作为handler,而不再需要实现Handler这个接口和自定义一个实现ServeHTTP函数的类型了,HandleFunc可以非常简便的为url注册路径。

Handle()

Handle()是函数

Handle()定义

和HandleFunc类似,本质上也是调用了默认mux的mux.Handle()方法

函数定义:

// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

第一个参数是待匹配的字符串,第二个参数是Handler类型的handler,和上面例子的handler不同,需要自己实现一个新的类,并且实现类中的方法ServeHTTP

例子:

package main

import (
    "fmt"
    "log"
    "net/http"
    "sync"
)

type countHandler struct {
    mu sync.Mutex // guards n
    n  int
}

func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    h.mu.Lock()
    defer h.mu.Unlock()
    h.n++
    fmt.Fprintf(w, "count is %d\n", h.n)
}

func main() {
    http.Handle("/count", new(countHandler))
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Handler

Handler是接口

Handler定义

在go package的官网文档中是这样定义Handler的“Handler响应HTTP请求”,理解成中文就是“处理程序”。

Handler是一个接口,定义在net/http原生包中:

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

对于任何实现了ServeHTTP方法的都属于Handler。

Handler性质

1.Handler只能用来读取request的body,而不能修改请求

2.先读取body,然后再写入resp

3.事实上在真正的开发过程中,我们不太会经常使用Handler,因为net/http给我们提供了更方便的HandleFunc()函数,而这个函数可以让我们直接将一个函数作为handler,在这里handler是函数类型而非此Handle接口,这种实现较实现ServeHTTP()来说更加方便。

Handler用于处理请求并给予响应,更严格地说,用来读取请求体、并将请求对应的响应字段(respones header)写入ResponseWriter中,然后返回

HandlerFunc

http还提供了HandlerFunc类,和HandleFunc()函数仅有一字之差

HandlerFunc本质是一个适配器函数,这个类在内部实现了ServeHTTP()函数,因此这个类本质上是一个Handler类型

HandlerFunc()定义

HandlerFunc(f) 是调用f函数的处理程序

函数原型:

type HandlerFunc func(ResponseWriter, *Request)

源码

// 调用默认ServerMux的HandleFunc方法
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {                                          
        DefaultServeMux.HandleFunc(pattern, handler)
}
// 把方法handler转换成HandlerFunc类型,即实现了Handler接口;再执行Handle方法
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
        mux.Handle(pattern, HandlerFunc(handler))
}

// 路由器注册一个handler给指定的parttern
func (mux *ServeMux) Handle(pattern string, handler Handler) {
        ....
}

执行HandleFunc()其实就是为某一规则的请求注册处理器。

Handler/HandlerFunc区别

Handler是一个接口

HandlerFunc是Handler类型,是一个适配器函数

Handle()/HandleFunc()区别

HandleFunc()Handle()都是调用DefaultServeMux对应的方法,而DefaultServeMux是ServeMux的实例,ServeMux实现了Handler接口。事实上,HandleFunc()最终还是会调用Handle方法,之前也提到过了:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    ...
    mux.Handle(pattern, HandlerFunc(handler))
}

路由注册过程

路由注册过程是由mux结构体内的两个方法mux.Handle()mux.HandleFunc()实现的

mux.Handle()

之前大致了解了函数体,路由注册过程主要由ServeMux 结构体内的Handle()方法实现的,如果该路由已经存在在路由表内,则会引发Panic

ServeMux结构体内的Handle() 方法大致上干了这样几件事:

1.检查路由合法性,若不合法则给出报错信息,报错信息multiple registrations for /xxx 就属于路由重复注册而引发的问题

2.若多路复用器中没有路由表,则创建路由表

3.若路由合法则创建路由条目muxEntry添加至路由表中

4.若路由最后一个字符带有"/",则按照自定义排序函数appendSorted()的规则定位索引、插入并排序(尽管我对此函数的执行效率充满疑惑),返回排序后的数组。

5.若路由的首字母不为"/",则包含主机名

mux.HandleFunc()

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

Helpful behavior

在之前版本的server.go中,注册函数mux.Handle是存在一些辅助行为的,当你将路由路径设置为/tree/时,Helpful behavior会隐式永久的帮你将/tree注册到注册表中,当然也可以显式指定路由进行覆盖,在对/tree/进行访问时,/tree的handler会自动将请求重定向到/tree/:

 状态代码: 301 / Moved Permanently

在现在server.go中,ServeMux结构体内维护了一个es类型的数组,就是从长到短记录最后一个字母是'/'路由字符串的

在使用mux.match()对路由path进行匹配的时候(详情见“路由查找过程”),首先查找路由表,当路由表中不存在该路由时,遍历es数组,匹配其最大长度:

// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // Check for exact match first.
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // Check for longest valid match.  mux.es contains all patterns
    // that end in / sorted from longest to shortest.
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

服务请求过程

参考官网的文档,我们可以使用golang原生的库net/http来实现一个简单的web路由示例:

// serve.go
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", HelloWorld)
    http.ListenAndServe(":8080", nil)
}

func HelloWorld(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello World")
}

其实主函数中只有两个方法,HandleFunc()ListenAndServe(),整个请求响应的执行流程如下:

  1. 注册路由过程:首先调用HandleFunc():调用了默认复用器DefaultServeMuxHandleFunc(),调用了DefaultServeMuxHandle,往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则。
  2. 实例化server并调用server.ListenAndServe(),调用net.Listen("tcp", addr)监听端口。启动一个for循环,在循环体中Accept请求
  3. 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve()
  4. 读取每个请求的内容w, err := c.readRequest()
  5. 进入serveHandler.ServeHTTP若有自己实现的mux则使用自己的mux。判断handler是否为空,如果没有设置handler(此例子中为nil handler),handler就设置为DefaultServeMux
  6. 调用handler的ServeHttp,进入到DefaultServeMux.ServeHttp
  7. 根据request选择匹配handler,并且进入到这个handler的ServeHTTP

对于每一个HTTP请求,服务端都会起一个协程来进行服务,这部分不在本文讨论范围。对于官网示例:

http.ListenAndServe(":8080", nil)

传入handler是nil,则使用默认复用器DefaultServeMux,调用HandleFunc时,就是向默认复用器注册了handler

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
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
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

鉴于本文篇幅太小,有机会再分析一下server.go中server.Serve()函数

路由查找过程

如何保证确保访问''/path/subpath'的时候是先匹配'/path/subpath'而不是匹配'/path/'',是因为在路由查找过程中的查找规则(之前同样提到过):

mux.ServerHTTP() -> mux.Handler() -> mux.handler() -> mux.match()

得到了处理请求的handler,再调用h.ServeHTTP(w, r),去执行相应的handler方法:

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

f(w,r)就实现了handler的执行

设计思想

mu读写锁:请求设计并发处理,到底哪些地方用到了并发?

没时间写了

hasPrefix() 可以做到精准匹配么?

可以,但没必要。因为需要对于模糊路由进行匹配,如果实现上只有路由表m,则对于访问URL的人来说极不友好,无法做到有效的辅助提示

为什么需要单独维护一个后缀为/的数组es?

1.同上,为了模糊匹配

2.最重要的是,方便插入排序,尽管时间复杂度没有那么乐观

总结

本文仅仅只是对http原生库的一小部分进行了解读,对于生产来说其实并没有特别大的帮助,但是掌握原生库的一些设计思想和设计模式对于理解其他框架是一定有很大帮助的,毕竟原生库的作者都是真正的大神。

很多舍本逐末的程序员只注重框架的学习而不注重这种基础,那和培训班出来的又有什么区别呢?还是应该真正理解一下原生库,毕竟后人开发的第三方还是借鉴了这些设计哲学的。

未来展望

因为原生库自带的默认多路请求路由器功能有限,而催生了很多路由框架,例如比较有名的框架httprouter,这篇文章比较详细的讲解了httprouter框架,感兴趣的可以先看看和原生库有什么不同,未来如果有时间,也会更新对这些框架的学习。

对于目前已经存在的web路由框架,已经有人比较过这些框架的优劣了,看这篇文章可以提前预习一下:

超全的Go Http路由框架性能比较

很早就想系统的讲解一下智能爬虫和爬虫框架了,因为爬虫和http这个库关系非常大。基本上star较多的框架我也都多多少少使用过,部分源码也阅读过,当然因为框架本身的复杂性,手撕源码会更加困难和耗费时间。之前也陆陆续续的简单介绍过colly等常用爬虫框架,未来会更新更多的关于爬虫框架的源码解读。爬虫本身的技术含量并不高,但是理解这个框架为什么好、哪里好,这个技术含量就高很多了,毕竟还是要和其他爬虫框架对比一下才知道各自的优劣。

但是受限于每个开发者自身的水平,这些框架或多或少都有这那的问题,它们并没有github主页上宣传的那么“万能”,所以在这种比较底层的地方,掌握和使用的区别就很大了,大多数人只会使用而没有掌握。

当然如果熟悉http库你就会发现,使用原生函数来写爬虫一样非常流畅。

我坚持认为:爬虫使用框架是最后的妥协。

Go的有些设计哲学很有趣,不是简简单单几万字的篇幅就可以讲明白的

我不想把自己标榜成Go布道者或者卷入语言圣战,Gopher应当是有趣的地鼠:

Gopher is moe, but confusing

参考链接

Go Web:Handler

Package http

golang中ServeMux解析

http.ServeMux解析

Golang web路由实现方式整理总结

GOLANG 中HTTP包默认路由匹配规则阅读笔记

*

handle、handlefunc和handler、handlerfunc的关系

Golang ServeMux 是如何实现多路处理的

[Golang 路由匹配浅析[1]](https://segmentfault.com/a/11...


Wzy_CC
40 声望3 粉丝