Go语言入门【六】:源码学习-net/http

package net/http是Go语言的主要应用场景之一web应用的基础,从中可以学习到大量前文提到的io,以及没有提到的sync包等一系列基础包的知识,代码量也相对较多,是一个源码学习的宝库。本文主要从一个http server开始,讲解Go是如何实现一个http协议服务器的。

主要涉及以下源码文件:
net/net.go
net/server.go
net/http.go
net/transfer.go
sync/pool.go
sync/mutex.go

0.引子:从最简单的http server说起

func main() {
    http.HandleFunc("/hi", hi)
    http.ListenAndServe(":9999", nil)
    fmt.Printf("hello, world\n")
}

func hi(res http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(res, "hi")
}

以上就是最简单的服务器代码,运行后监听本机的9999端口,在浏览器中打开http://localhost:9999可以看到返回的hi,接下来就从此入手,开始分析net/http模块。

1.Handler: 从路由开始上路

先来分析http.HandleFunc("/hi", hi) 这一句,查看源码发现:

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

首先我们了解到handler的定义是这样的func(ResponseWriter, *Request)。这个定义很关键,先提一下。
然后看到了DefaultServeMux,这个类是来自于ServeMux结构的一个实例,而后者是一个『路由器』的角色,在后面讲到的请求处理过程中,ServeMux用来匹配请求的地址,分配适合的handler来完成业务逻辑。
完整的来讲,我们应该先定义一个自己的ServeMux,并向他分配路由,像这样:

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "Welcome to the home page!")
})
http.ListenAndServe(":9999", mux)

1.生成一个路由器
2.向路由器注册路由
3.由路由器以及服务地址建立底层连接并提供服务

而之前的简写方式只是省略了建立路由的过程,实际上用了系统自带的DefaultServeMux作为路由器而已。

2.向net包匆匆一瞥:一切的基础在net.Conn

接下来看到http.ListenAndServe(":9999", nil)这句代码的源码。

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

首先生成了一个server对象,并调用了它的ListenAndServe方法。Server对象顾名思义,封装了有关提供web服务相关的所有信息,是一个比较重要的类。

// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
    Addr         string        // TCP address to listen on, ":http" if empty
    Handler      Handler       // handler to invoke, http.DefaultServeMux if nil
    ReadTimeout  time.Duration // maximum duration before timing out read of the request
    WriteTimeout time.Duration // maximum duration before timing out write of the response
    TLSConfig    *tls.Config   // optional TLS config, used by ListenAndServeTLS

    MaxHeaderBytes int

    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

    ConnState func(net.Conn, ConnState)

    ErrorLog *log.Logger

    disableKeepAlives int32     // accessed atomically.
    nextProtoOnce     sync.Once // guards setupHTTP2_* init
    nextProtoErr      error     // result of http2.ConfigureServer if used
}

1.handler即路由器(实际上路由器本身作为handler,其中有注册了很多handler),见Handler定义:

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

和之前注册的函数几乎一样。
2.ErrorLog默认以stdErr作为输出,也可以提供其他的logger形式。
3.其他的是一些配置以及https,http2的相关支持,暂搁一边。

初始化一个Server必须要的是地址(端口)以及路由,其他都可以按照默认值。生成好Server之后,进入ListenAndServe,源码主要有:

ln, err := net.Listen("tcp", addr)
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})

重要的有两句,首先调用底层的net模块对地址实现监听,返回的ln是一个Listener类型,这个类型有三个方法:

  • Accept() (Conn, error)
  • Close() error
  • Addr() Addr

我们先不碰net模块,只要知道ln可以通过accept()返回一个net.Conn就够了,获取一个连接的上下文意味着和客户端建立了通道,可以获取数据,并把处理的结果返回给客户端了。接下来srv.Serve()方法接受了ln,在这里程序被分为了两层:ln负责连接的底层建立,读写,关闭;Server负责数据的处理。

补充说明一下net.Conn,这个Conn区别于后文要讲的server.conn,是比较底层的,有

  • Read(b []byte) (n int, err error)
  • Write(b []byte) (n int, err error)

两个方法,也意味着实现了io.Reader, io.Writer接口。

3.回到server:建立一个服务器,用goroutine 优雅处理并发

接着前面说,建立好ln之后,用tcpKeepAliveListener类型简单包装,作为参数传给srv.Serve()方法,该方法十分重要,值得放出全部代码:

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// For HTTP/2 support, srv.TLSConfig should be initialized to the
// provided listener's TLS Config before calling Serve. If
// srv.TLSConfig is non-nil and doesn't include the string "h2" in
// Config.NextProtos, HTTP/2 support is not enabled.
//
// Serve always returns a non-nil error.
func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    if fn := testHookServerServe; fn != nil {
        fn(srv, l)
    }
    var tempDelay time.Duration // how long to sleep on accept failure

    if err := srv.setupHTTP2_Serve(); err != nil {
        return err
    }

    // TODO: allow changing base context? can't imagine concrete
    // use cases yet.
    baseCtx := context.Background()
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
    for {
        rw, e := l.Accept()
        if e != nil {
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

分析一下:

a) 首先是context这个类型

这个类型比较奇葩,其作用就是一个map,以key,value的形式设置一些背景变量,使用方法是context.WithValue(parentCtx,key,value)

b) 然后进入一个for无限循环,

l.Accept()阻塞直到获取到一个net.Conn,之后通过srv.newConn(rw)建立一个server.conn(属于私有变量,不对外暴露),并设置状态为StateNew

c) 启动一个goroutine来处理这个连接

调用go c.serve(ctx)。从这里可以看出,go语言的并发模型不同于nodejs的单线程回调模型,也不同于Java的多线程方案,采用原生的goroutine来处理既有隔离性,又兼顾了性能。因为这样不会发生nodejs中因为异常处理问题经常让服务器挂掉的现象。同时,goroutine的创建代价远远低于创建线程,当然能在同一台机器比Java服务器达到更大的并发量了。

4. 从server到conn:一次请求所有的精华都在conn

前面提到了server.conn,来看一下源码:

// A conn represents the server side of an HTTP connection.
type conn struct {
    // server is the server on which the connection arrived.
    // Immutable; never nil.
    server *Server

    // rwc is the underlying network connection.
    // This is never wrapped by other types and is the value given out
    // to CloseNotifier callers. It is usually of type *net.TCPConn or
    // *tls.Conn.
    rwc net.Conn

    // remoteAddr is rwc.RemoteAddr().String(). It is not populated synchronously
    // inside the Listener's Accept goroutine, as some implementations block.
    // It is populated immediately inside the (*conn).serve goroutine.
    // This is the value of a Handler's (*Request).RemoteAddr.
    remoteAddr string

    // tlsState is the TLS connection state when using TLS.
    // nil means not TLS.
    tlsState *tls.ConnectionState

    // werr is set to the first write error to rwc.
    // It is set via checkConnErrorWriter{w}, where bufw writes.
    werr error

    // r is bufr's read source. It's a wrapper around rwc that provides
    // io.LimitedReader-style limiting (while reading request headers)
    // and functionality to support CloseNotifier. See *connReader docs.
    r *connReader

    // bufr reads from r.
    // Users of bufr must hold mu.
    bufr *bufio.Reader

    // bufw writes to checkConnErrorWriter{c}, which populates werr on error.
    bufw *bufio.Writer

    // lastMethod is the method of the most recent request
    // on this connection, if any.
    lastMethod string

    // mu guards hijackedv, use of bufr, (*response).closeNotifyCh.
    mu sync.Mutex

    // hijackedv is whether this connection has been hijacked
    // by a Handler with the Hijacker interface.
    // It is guarded by mu.
    hijackedv bool
}

解释一下:
首先,持有server的引用;持有对原始net.Conn引用;持有一个reader,封装自底层读取接口,可以从连接中读取数据,以及一个bufr(还是前面的reader,加了缓冲)。以及一个对应的同步锁,锁定对本身的参数修改,防止同步更新出错。
然后,这里的mu类型是sync.Mutex这个类型的作用有点像Java中的synchronized块(有关于Java的Synchronized,可以参考本人另一篇拙作《Java多线程你只需要看着一篇就够了》),mu就是持有对象锁的那个实例。我们可以看到conn的hijackedv属性就是通过mu来进行维护的,目的是防止同步更新问题。参考conn.hijackLocked(),不再展开。

继续看serv.Serve()方法,接着前面的3点:

d) setState(state)

实际上state被维护在Server里,只不过通过conn来调用了。一共有StateNew, StateActive, StateIdle, StateHijacked, StateClosed五个状态。从new开始,当读取了一个字节之后进入active,读取完了并发送response之后,进入idle。终结有两种,主动终结closed以及被接管: Hijack让调用者接管连接,在调用Hijack()后,http server库将不再对该连接进行处理,对于该连接的管理和关闭责任将由调用者接管。参考interface Hijacker

e) c.serve(ctx)

让我们先来看conn.serve()源码:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    defer func() {
        if err := recover(); err != nil {
            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 !c.hijacked() {
            c.close()
            c.setState(c.rwc, StateClosed)
        }
    }()

    if tlsConn, ok := c.rwc.(*tls.Conn); ok {
        if d := c.server.ReadTimeout; d != 0 {
            c.rwc.SetReadDeadline(time.Now().Add(d))
        }
        if d := c.server.WriteTimeout; d != 0 {
            c.rwc.SetWriteDeadline(time.Now().Add(d))
        }
        if err := tlsConn.Handshake(); err != nil {
            c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
            return
        }
        c.tlsState = new(tls.ConnectionState)
        *c.tlsState = tlsConn.ConnectionState()
        if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) {
            if fn := c.server.TLSNextProto[proto]; fn != nil {
                h := initNPNRequest{tlsConn, serverHandler{c.server}}
                fn(c.server, tlsConn, h)
            }
            return
        }
    }

    // HTTP/1.x from here on.

    c.r = &connReader{r: c.rwc}
    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)
        if c.r.remain != c.server.initialReadLimitSize() {
            // If we read any bytes off the wire, we're active.
            c.setState(c.rwc, StateActive)
        }
        if err != nil {
            if err == errTooLarge {
                // Their HTTP client may or may not be
                // able to read this if we're
                // responding to them and hanging up
                // while they're still writing their
                // request. Undefined behavior.
                io.WriteString(c.rwc, "HTTP/1.1 431 Request Header Fields Too Large\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n431 Request Header Fields Too Large")
                c.closeWriteAndWait()
                return
            }
            if err == io.EOF {
                return // don't reply
            }
            if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
                return // don't reply
            }
            var publicErr string
            if v, ok := err.(badRequestError); ok {
                publicErr = ": " + string(v)
            }
            io.WriteString(c.rwc, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n400 Bad Request"+publicErr)
            return
        }

        // Expect 100 Continue support
        req := w.req
        if req.expectsContinue() {
            if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
                // Wrap the Body reader with one that replies on the connection
                req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
            }
        } else if req.Header.get("Expect") != "" {
            w.sendExpectationFailed()
            return
        }

        // HTTP cannot have multiple simultaneous active requests.[*]
        // Until the server replies to this request, it can't read another,
        // so we might as well run the handler in this goroutine.
        // [*] Not strictly true: HTTP pipelining. We could let them all process
        // in parallel even if their responses need to be serialized.
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        if c.hijacked() {
            return
        }
        w.finishRequest()
        if !w.shouldReuseConnection() {
            if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
                c.closeWriteAndWait()
            }
            return
        }
        c.setState(c.rwc, StateIdle)
    }
}

5.从conn到conn.Serve:http协议的处理实现之处,conn变成Request和Response

上文的conn.Serve(),我们只关注主要逻辑:

1.初始化bufr和bufw。
...
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
...

这两个是读写的切入点,从效率考虑,是加了一层缓冲的。值得注意的是bufw和bufr还加了一层sync.Pool的封装,这是来源于sync包的对象池,目的是为了重用,不需要每次都执行new分配内存。

2.接下来重要的是,从底层读取客户端发送的数据:
...
w, err := c.readRequest(ctx)
...

我们看到readRequest定义:

func readRequest(b *bufio.Reader, deleteHostHeader bool) (req *Request, err error)
返回的是 (w *response, err error),而response又是server.go中的一个重要对象,它是conn的更高一层封装,包括了req,conn,以及一个writer,当然这个write操作实际上还是由conn,进而由更底层的net.Conn来执行的。对于开发者而言,面对的基本上就是这个response,可以说是一个设计模式中的门面模式。

另外,注意到readRequest执行的时候也调用了mu.Lock()

3.最重要的,调用用户的handler
...
serverHandler{c.server}.ServeHTTP(w, w.req)

首先serverHandler只是一个包装,这句实际上调用的是c.server.Handler.ServeHTTP()。而在前面讲到的server的初始化中,Handler就是DefaultServeMux或者用户指定的ServeMux,我们称之为路由器。在路由器中,根据用户定义路由规则,来具体调用用户的业务逻辑方法。

路由器可以看做一个Map,以路由规则(string)作为key,以业务方法(func类型)作为value。

ServeHttp传入了最重要的两个高层封装response对象和Request对象(严格来讲这里response是私有类型,暴露在外的是ResponseWriter,但从http的本质来理解,还是称之为response)。

从层次来看,这两个封装对象中间封装的是底层的conn,客户端发送来的数据(req.body),以及读写的接口reader,writer。

然后,用户的业务逻辑就接受数据,进行处理,进而返回数据。返回数据一般直接写入到这个w,即ResponseWriter中。这样,一个http请求的完整流程就完成了。

4.最后做一些处理工作

主要包括:异常处理,资源回收,状态更新。我们了解即可,重点还是放在主要流程上。

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

推荐阅读更多精彩内容