【Golang 源码包分析】01 net/http包

快速搭建 Web 服务器

在Golang仅需要几行代码,便可以建立一个简单的 Web 服务:

package main

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

func main() {
    helloHandler := func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintf(w, "Hello, world!\n")
    }

    http.HandleFunc("/hello", helloHandler)
    log.Fatal(http.ListenAndServe(":9999", nil))
}

编译运行,如果不出意外的话,访问 http://localhost:9999/hello,你将会在浏览器上看到Hello, world!

当时刚接触到Go的时候,我是很震惊的!!!以前写 PHP,要配 web 服务,还得搭个 Nginx或Apache 服务器呢,配置下监听的接口转发到fastcgi进行处理。Go这么流氓吗,直接几行代码就完事了?是的,Go 就真的很流氓,它直接监听了 TCP 端口。

Web 服务器运转机制

以使用TCP协议通讯的socket为例,socket通信交互流程大概是这样子的

file

我们来看一段代码 net 包开头的一段示例代码

ln, err := net.Listen("tcp", ":8080")
if err != nil {
    // handle error
}
for {
    conn, err := ln.Accept()
    if err != nil {
        // handle error
    }
    go handleConnection(conn)
}

创建 Listen Socket,监听指定端口8080,等待客户端的请求,创建 goroutine 处理请求。

实际上 Go 的 web server 也就是这么个处理逻辑,只是在这多了一些处理逻辑:请求的协议解析,读取客户端提交的数据,路由分配(根据请求分配对应的 handler 处理请求),封装响应数据。

http 包源码分析

还记得快速搭建 web 服务器的时候有行代码:http.ListenAndServe(":9999", nil),Go 就是通过这个方法来处理上面 端口监听、请求解析、路由分配、响应处理。

1. 端口监听

让我们追进去看看,实际上,初始化一个server对象,调用了 server 的 ListenAndServe 方法。

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

再追进去看看,瞧,这不是嘛 ln, err := net.Listen("tcp", addr) ,用TCP协议搭建了一个服务,监听着我们设置的端口呢。

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

2. 接受请求

是不是有点小兴奋了,让我们再继续追进去看看 srv.Serve(ln),核心代码片段:

    for {
        // 接受监听器listener的请求
        rw, e := l.Accept()
        if e != nil {
            // 监听是否关闭信号
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
        }
            ……
        // 创建新连接
        c := srv.newConn(rw)
        // 再返回之前,设置连接状态
        c.setState(c.rwc, StateNew) // before Serve can return
        // 创建goroutine,真正处理连接
        go c.serve(ctx)
    }

可以看到,这里用了一个死循环 for{} 等待。

  1. 首先通过 listener.Accept 接受请求,是不是跟上面提到的socket的运行机制很像了。
  2. 接着用接受到的请求创建一个新的 Conn,并设置为New状态。
  3. 最后创建 goroutine,真正的处理连接。

知识点:每个请求都会创建一个对应的goroutine去处理,所以各个请求之间是相互不影响的,同时提高并发性能。

3. 读取请求并解析

来,我们再进去 go c.serve(ctx) 看看,忽略一些细节,重点关注关键的流程

for {
        ……
    
        // 读request请求
        w, err := c.readRequest(ctx)
        
        ……
        // 调用业务层定义的路由
        serverHandler{c.server}.ServeHTTP(w, w.req)
        
        ……
        // flush刷io buffer的数据
        w.finishRequest()

readRequest 便是读取数据,解析请求的地方,包括解析请求的header、body,和一些基本的校验,比如header头信息,请求method等。

最后将请求的数据赋值到Request,并初始化Response对象,供业务层调用。

4. 路由分配handler

上面关键流程已经看到了serverHandler{c.server}.ServeHTTP(w, w.req),这个实际上就是调用最开始在main函数定义的handler,并将处理好的Request、Response对象作为参数传入。

type serverHandler struct {
    srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}

还记得吗,我们最上面是这样调用的 http.ListenAndServe(":9999", nil),第二个参数是nil。

所以,你看在ServeHTTP中,handler = DefaultServeMux,我们使用了默认的路由器,如果 ListenAndServe 不是传nil的话,那就会使用你自己定义的路由器。

我们来看看httprouter的例子,你就会明白了:

package main

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

    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)

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

看到了吧,上面router便是自定义的路由器,在走到上面ServerHTTP的时候,便是使用指定的路由器进行路由。

5. 路由分配handler

好了,我们知道了使用默认的路由器(DefaultServeMux),再看看它是怎么根据路径找对应handler的吧~~~

我想你看到这已经累了,请不要轻易离开,学海无涯苦作舟啊~~

路由的过程里面我们只要弄懂下面的三个问题,就知道 Go 自带的路由是怎么运行的了:

  1. 什么时候注册的路由?
  2. 如何根据注册的路由找对应的handler?
  3. 如果没注册路由访问会返回什么?

好,我们首先来看看默认路由器(DefaultServeMux)的结构定义:

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry 
    hosts bool
}

其中 m 是一个map,用来存储路由pattern与handler的关系;es 是一个slice,将路由按长度从大到小排序存储起来。

匹配规则:首先精确匹配 m 中的pattern;如果在 m 不能精确匹配路径时,会在 es 中找到最接近的路由规则:比如注册了两个路径 /a/b/ /a/ ,当请求URL是 /a/b/c时,会匹配到 /a/b/ 而不是 /a/

1. 什么时候注册的路由?

还记得吧,在 ListenAndServe 之前,有这么一行代码,http.HandleFunc("/hello", helloHandler),这个便是在注册路由。来我们把一些检查代码去掉,看看关键代码:

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

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

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    ……
    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
    }
}

看到了吧,这就是把路由表往 ServeMux.mServeMux.es 写的全过程

2. 如何根据注册的路由找对应的handler?

其实知道怎么写路由表,大概也能猜到是怎么进行找了,无非就是从上面的 mes 进行匹配。

我们上面从端口监听,接受请求,读取请求并解析,再到路由分配handler,一路追到 handler.ServeHTTP(rw, req),现在再看看这段代码的实现

// 根据预设的pattern,将request分配最匹配的handler处理
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    ……
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

……

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    if mux.hosts {
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // 优先查找m表
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // 未精确匹配成功,查询es(已排序),路径长的优先匹配
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

3. 如果没注册路由访问会返回什么?

这个问题其实上面代码已经看到了,Go 内置了一个 NotFoundHandler(),返回 "404 page not found"

好了,我想你看完这个,应该对 Go 如何用几行代码搭建 Web 服务器有了基本了解了吧。

结尾

个人建议,看完这个文章后,自己点进去 Go 的源码包里,我想你会有新收获的。


file

如果有任何疑问或错误可直接留言,我看到会回复的~~

公众号『知否专栏』,欢迎关注。


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

推荐阅读更多精彩内容