分享
  1. 首页
  2. 文章

golang http server 源码解析

咔叽咔叽_7647 · · 1011 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:咔叽咔叽_7647

查看原文:golang http server 源码解析

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1011 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏