golang http server 源码阅读

http 包怎么用

使用 golang 的 http 包可以很简易的实现一个 web 服务,如下

main.go

package main

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

func foo(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hi! babe~"))
}

func echo(w http.ResponseWriter, r *http.Request) {
    s := fmt.Sprintf("gorotines count: %d", runtime.NumGoroutine())
    w.Write([]byte(s))
}

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("/foo", foo)plainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplain
    mux.HandleFunc("/echo/goroutines", echo)

    log.Println("Listening...")
    http.ListenAndServe(":3000", mux)
}

那如果我想看看整个服务是怎么实现的,该怎么办呢?
ListenAndServe()接收一个地址和处理程序的参数,此函数的定义如下

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

然后调用了

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

然后上述函数又调用了 Serve 函数

func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    ...
    srv.trackListener(l, true)
    defer srv.trackListener(l, false)

    baseCtx := context.Background() // base is always background, per Issue 16220
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
    for {
        // Accept等待并返回listener的下一个连接
        rw, e := l.Accept()
        if e != nil { ... } // 省略一些代码
        tempDelay = 0
        // 使用rw创建一个新连接
        c := srv.newConn(rw)
        // 将链接置为激活状态,同时可指定在客户端连接更改状态时调用可选的回调函数
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

从上面的go c.serve(ctx)可以看出,http 包在 ctx 上下文组装好之后交给了 gorotine 来处理这个请求。在继续下一步之前,我们先看看这个 ctx 上下文,Context 被定义为一个接口,它在 golang 中被运用的非常广泛。

type Context interface {
    // Deadline 设置了两个参数deadline, ok
    // deadline 表示上下文被取消的截止时间
    // 如果没有设置deadline,Deadline的ok参数会返回false。
    // 连续调用返回结果相同
    Deadline() (deadline time.Time, ok bool) 
 
    // 如果上下文被取消,Done会返回一个被关闭的chan
    // 如果上下文从没被取消过,Done将返回nil
    // 连续调用返回结果相同
    Done() <-chan struct{}

    // Done 的 chan被关闭后,也就是上下文被取消时,Err会返回非零的错误值。
   // 当 Done 的 chan被关闭后,连续调用返回结果相同
    Err() error

    // 也就是通过key去获取该key上下文中的值,如果没有则为nil,可见ctx是一个键值对。该值是线程安全的
    Value(key interface{}) interface{} 
}

好了介绍完 context 之后,我们再来看看 Serve 函数中的baseCtx := context.Background()是干什么的。

// Background返回一个非零的空Context。它没有值也没有deadline,所以也不会被取消,
// 它通常在main函数被用来初始化,测试,以及作为请求传入的顶级Context
func Background() Context {
    return background
}

嗯,他其实就是初始化的一个作用。

接下来又碰到了 WithValue 函数,我们继续看看 WithValue 的定义。

// 生成一个绑定了一个键值对数据的Context,可以通过parent访问到上一层的context,这个绑定的数据可以通过Context.Value方法访问到
func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

// 一个valueCtx结构带有一个键值对。然后用来嵌套其他的Context。
type valueCtx struct {
    Context
    key, val interface{}
}

结合源码,那么这个 context 定义结构就可以了解了

    // 下面定义了两个context key,一个存储了type *Server,另一个存储了type net.Addr
    ServerContextKey = &contextKey{"http-server"}
    LocalAddrContextKey = &contextKey{"local-addr"}
   
    // 顶层context
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
   // parent 为 顶层的context
    ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())

那么为什么这么定义呢? 思考思考,对 context 的作用和细节还没系统了解过,context 是一个很重要的功能 TODO

继续往下看 go serve(ctx),可以看到这里用 gorotine 来处理每个链接来支撑并发,这也是支持并发的关键。

// 处理一个新链接
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()

    ...

    // HTTP/1.x from here on.
    // 这里又碰到WithCancel函数,WithCancel返回带有父context 的Done通道副本和一个cancelCtx函数。
    // 返回的上下文的Done通道在调用了返回的cancelCtx函数或父context的Done通道关闭时关闭,以先发生者为准。
    // 取消此上下文会释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用cancelCtx。所以可以看到使用了defer去调用cancelCtx释放资源
    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)

    for {
        // 从链接中读取请求
        w, err := c.readRequest(ctx)
        if c.r.remain != c.server.initialReadLimitSize() {
            // If we read any bytes off the wire, we're active.
            c.setState(c.rwc, StateActive)
        }

        ...
        // 核心点,该处就是处理请求的hanler
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        
        ...

        c.rwc.SetReadDeadline(time.Time{})
    }
}

好了到这我们知道是用serverHandler{c.server}.ServeHTTP(w, w.req)来处理请求的。我们回过头去看看,路由和 handler 是怎么绑定到一起的

    // ServeMux是一个HTTP请求多路复用器,说白了就承担了路由功能呗
    // 在ServeMux 的注释中,我们可以了解到整个路由的一些机制。
    // 模式名称固定,带根的路径,如"/favicon.ico",或带根的子树,如"/images/"(请注意尾部斜杠)。
    // 较长的模式优先于较短的模式,因此如果存在"/images/"和"/images/thumbnails/"注册的handler,则"/images/thumbnails/"开头的路径将调用后者的handler,然后前者将接收"/images/"子树中任何其他路径的请求,比方说"/images/xxxx"等等。

    mux := http.NewServeMux()

    // 往mux上绑定了两个handler
    mux.HandleFunc("/foo", foo)
    mux.HandleFunc("/echo/goroutines", echo)

我们看到 mux 调用了 HandleFunc,来看看他们的定义

// HandleFunc为给定pattern注册handler
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    mux.Handle(pattern, HandlerFunc(handler))
}

// 如果pattern已经存在handler了,将会panic
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    
    ...

    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}

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

    // 如果pattern是/tree/,则为/tree插入隐式永久重定向
    // 通过显式注册可以覆盖
    n := len(pattern)
    if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
        // 如果pattern包含host name,将其删除并使用剩余的路径进行重定向。
        path := pattern
        if pattern[0] != '/' {
            // strings.Index 返回子串 sep "/" 在字符串 pattern 中第一次出现的位置
            // 如果找不到,则返回 -1,如果 sep 为空,则返回 0。
            path = pattern[strings.Index(pattern, "/"):]
        }
        url := &url.URL{Path: path}
        // 在我们的例子中pattern为"/echo/gorotine/",则会为"/echo/gorotine" 添加一个重定向
        mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
    }
}

看完上面的定义,我们知道路由和 handler 是怎么存储的了。

再看看 ServeMux 结构,m 是一个字典形式的,当我们调用 HandlerFunc 会把 pattern 即"/echo/goroutines"作为 key,muxEntry 作为 value,muxEntry 为一个包含 pattern,handler 和 explicit 的结构。

type ServeMux struct {
    mu    sync.RWMutex // 读写锁
    m     map[string]muxEntry // 存储结构
    hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
    explicit bool  // 该pattern是否完全匹配handler
    h        Handler
    pattern  string
}

好了,上述把 handler 绑定到了 server 上。那么是如何通过 url 查找 handler 的呢?先看看 ServeHTTP

// ServeHTTP将请求分派给handler,该handler的pattern与请求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
    }
    // 通过Handler找到最匹配的handler来处理该请求
    h, _ := mux.Handler(r)
    // 调用处理请求
    h.ServeHTTP(w, r)
}

从上面代码可以看到,使用 handler 的 ServeHTTP 方法去处理请求,这里又有一个疑问了,为什么 ServeHTTP 的 w 是值传递,而 r 是引用传递呢?
先看看 w,r 的定义,通过观察 ResponseWriter,和 Request 的定义就知道为什么这么做了。

// HTTP处理程序使用ResponseWriter接口来构造HTTP响应。
// Handler.ServeHTTP方法返回后,可能就无法使用ResponseWriter了。
type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(int)
}

// 请求表示由服务器接收或由客户端发送的HTTP请求。
type Request struct {
    ...
}

可以看到,ResponseWriter 是一个接口,Request 是一个结构。我们往回拨一下,看看这个接口是什么。

w, err := c.readRequest(ctx)
...
// 正是readRequest返回的
serverHandler{c.server}.ServeHTTP(w, w.req)

// 再看看 readRequest的函数签名,其实也是一个指针来的。
func (c *conn) readRequest(ctx context.Context) (w *response, err error)
{
    ...
    w = &response{
        conn:          c,
        cancelCtx:     cancelCtx,
        req:           req,
        reqBody:       req.Body,
        handlerHeader: make(Header),
        contentLength: -1,
        closeNotifyCh: make(chan bool, 1),

        // We populate these ahead of time so we're not
        // reading from req.Header after their Handler starts
        // and maybe mutates it (Issue 14940)
        wants10KeepAlive: req.wantsHttp10KeepAlive(),
        wantsClose:       req.wantsClose(),
    }
    if isH2Upgrade {
        w.closeAfterReply = true
    }
    // 这里有个令人窒息的操作,对于vegetable的我来说有点难以理解。w.cw.res的res其实也是一个response,w.w的第一个w是response结构,第二个w是一个*bufio.Writer结构。
    w.cw.res = w
    // newBufioWriterSize返回一个Writer结构的指针,而w的Writer是一个方法,注意区分
    w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
    return w, nil
}

// response的Write方法正是掉用了第二个w结构的Write方法,把数据写入了缓冲区
// 在main.go中向response里写数据的方法 w.Write([]byte("hi! babe~"))
func (w *response) Write(data []byte) (n int, err error) {
    return w.write(len(data), data, "")
}

// Write的具体实现
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
    ...
    if dataB != nil {
        return w.w.Write(dataB)
    } else {
        return w.w.WriteString(dataS)
    }
}

// w.w 也就是 *bufio.Writer结构 的方法。可以看到通过copy把p写入了write结构。
func (b *Writer) Write(p []byte) (nn int, err error) {
    ...
    n := copy(b.buf[b.n:], p)
    b.n += n
    nn += n
    return nn, nil
}

从上面的代码可以看到 ResponseWriter 接口,其实也是传入了一个 response 结构的指针,又解决一个疑问,nice

// Handler返回用于给定请求的handler,返回依据参考r.Method,r.Host和r.URL.Path等参数。它总是返回一个非空的handler(如果没有则返回NotFound的handler)。
// 如果路径不规范,则处理程序将会走内部生成的handler,重定向到它的规范路径。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    if r.Method != "CONNECT" {
        if p := cleanPath(r.URL.Path); p != r.URL.Path {
            _, pattern = mux.handler(r.Host, p)
            url := *r.URL
            url.Path = p
            return RedirectHandler(url.String(), StatusMovedPermanently), pattern
        }
    }

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

// handler函数是Handler的主要实现,host参数传入请求的r.Host, path参数传入r.URL.Path
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.ServeHTTP -> func (mux *ServeMux) Handler(r *Request) -> func (mux *ServeMux) handler(r *Request)

当到达func (mux *ServeMux) handler的时候,一切逻辑就清晰了起来。先抛一个问题,"/echo/"和"/echo/goroutines/"这俩怎么区分 handler?从前面的注释,我们知道"/echo/"会处理它所有的子树,而"/echo/goroutines/"就是它的子树,匹配的时候会根据最长原则,也就是会先匹配"/echo/goroutines/"的 handler,那我们来看看这个具体实现。

// 上层调用match函数path传入r.URL.Path
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    var n = 0
    // m里的存储规则是 m['/echo/gorotines/'] = EntryMux{}
    for k, v := range mux.m {
        if !pathMatch(k, path) {
            continue
        }
        // 从pathMatch函数可以知道"/echo/"会匹配所有它的子树,也就是类似"/echo/xxx"这些,该函数都会返回true。
        // 所以下面这段逻辑就是上面问题的答案。即最长原则,如果满足len(k) > n 的情况,h会被替换成更长path的那个handler。
        // 这段代码的时间复杂度是O(n),其他的更高效的web框架会不会实现一个O(lgn)的算法呢?我们知道trie树可以做的,下次看看其他的框架怎么实现的
        if h == nil || len(k) > n {
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}

// 可以看到这个pathMatch是拿pattern和path做比较,
func pathMatch(pattern, path string) bool {
    if len(pattern) == 0 {
        // should not happen
        return false
    }
    n := len(pattern)
    // 如果pattern不以'/'结尾 直接比较
    if pattern[n-1] != '/' {
        return pattern == path
    }
    // 关键部位,path比pattern要长
    // 截取path[0:n] 和 pattern匹配,也就是如果我们的path为"/echo/goroutines/" 我们注册的handler只有"/echo/"的话,那么"/echo/goroutines/" 会匹配到"/echo/"
    return len(path) >= n && path[0:n] == pattern
}

带着一些问题,阅读了整个 http 请求的一些源码,其中确实很复杂,通过了解代码能搞大概搞清楚怎么处理的,当然作者为什么这么写脑海里仍然有一个疑问,等姿势水平再高一点再来探究。整篇分析到此结束,怎么把这些条理化展示的水平还有待提高。

回顾一下提出的几个问题
  • "/echo/"和"/echo/goroutines/"这怎么区分匹配 handler?
  • 为什么 ServeHTTP 的 w 是值传递,而 r 是引用传递呢?
  • http server 怎么处理并发请求?
  • 了解 context 在golang中的应用?TODO
参考:

傅小黑的这篇文章框架很清晰
http://fuxiaohei.me/2016/9/20/go-and-http-server.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容