本文主要分享golang net/http server.go中代码运行的过程。
一、背景
分享这个源码浅析,肯定是在日常学习,工作中有遇到一些不明白的事,下面先上代码,一个很简单的http server:
package main
import (
"fmt"
"io"
"net/http"
)
func myhandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(w, "hello world")
io.WriteString(w, "hello world")
}
func myhandler1(w http.ResponseWriter, r *http.Request) {
fmt.Println(w, "hello golang")
io.WriteString(w, "hello golang")
}
func main() {
http.HandleFunc("/helloWorld", myhandler)
http.HandleFunc("/helloGolang", myhandler1)
fmt.Println("http server running in http://127.0.0.1 ... ... ")
err := http.ListenAndServe(":8080", nil)
defer func() {
err := recover()
if err != nil {
fmt.Println(err)
}
}()
if err != nil {
panic("listen failed")
}
}
把代码跑起来:
$ go run web.go
在浏览器上输入:http://127.0.0.1:8080/helloWorld 就可以看到"hello world"出现在浏览器页面上。
上面的代码是很简单,学过golang的同学都会,就是导一个net/http标准库,跟着流程走,但是大家有没有想过或者看过它的实现过程呢?
我在学习的过程中,思索到以下几个问题,本文也就是从这几个问题出发,把上面代码实现的过程,整理出来,分享给大家。
- 我在浏览器输入相应的地址,怎么就给我映射到了相应的函数中去呢?
- http.HandleFunc()函数到底做了什么?
- http.ListenAndServe()函数到底做了什么就能实现监听客户端?
下面来一一整理
二、http.HandleFunc()函数到底做了什么?
http.HandleFunc()函数到底做了什么?我们来具体看一下该函数的实现过程,这个函数大约在net/http的server.go的2422行左右
这里出现DefaultServeMux是什么?我们再深入看看。
发现 DefaultServeMux 是一个全局变量,是一个 指向ServeMux的指针,ServeMux是一个呢?源码是这么解释的
基本上理解第一段落就好:ServeMux是一个路由器,它根据已注册的路径(pattren)列表匹配每个传入请求的URL,并调用与URL最匹配的路径(pattern)的处理程序(handler)。例如下文中的"/helloworld"就是pattern,"myhandler"就是handler
http.HandleFunc("/helloWorld", myhandler)
知道了 DefaultServeMux 是一个 指向ServeMux的指针,那么DefaultServeMux.HandleFunc()就是调用ServeMux的成员方法HandleFunc()
这里有个比较重要的类型HandlerFunc,由下图可以看到它是一个func(ResponseWriter, *Request)类型,这个类型还有一个类型方法ServeHTTP()。
ServeHTTP()至关重要,如果同学用过beego框架,源码也有会实现这个方法。关于类型方法的理解可以看《go语言编程》的第三章第一节-为类型添加方法,也可以看我写的type关键字用法一文。
HandlerFunc(handler)就是将handler,也就是myhandler转化为func(ResponseWriter, *Request)类型
注:需要《go语言编程》资源的同学可以留言,给你网盘链接,资源仅供学习交流用。
好了,类型HandlerFunc了解完后,我们回到主干上,ServeMux的成员方法HandleFunc()里还调用了Handle()函数。
注意了,此函数有两个参数,第一个是pattern,也就是路径。第二个是接口类型Handler,如下图
ServeMux的Handle()函数是一个核心函数,下文的这句代码实现了将你想要登记的’("/helloWorld", myhandler)'加到DefaultServeMux这个全局变量中去,形成一个路由器,或者一张路由表(muxEntry),然后等待请求的到来,来一个就到表里面查找(根据pattern查找),找到相应的pattrn就执行相应的处理逻辑,也就是执行handler,关于怎么查找后文会提到。
mux.m[pattern] = muxEntry{h: handler, pattern: pattern}
下面来看一张图,大概就理解了。
不妨我们看一下,pattern与handler是怎么加上去的,每执行一次“http.HandleFunc("/helloWorld", myhandler)”就会执行下图中的函数,我们打印出来看一下(这里我只想注册两个pattern,所以也就打印出来两次)
上图我们已经可以看到全局变量DefaultServeMux已经有了两条路由规则。我们再回顾一下DefaultServeMux的结构,
总结:
http.HandleFunc()将每一条路由规则添加到DefaultServeMux这个全局变量中,形成一张可查询的路由表供请求使用。
三、http.ListenAndServe()函数到底做了什么
http.ListenAndServe()函数到底做了什么?我们来具体看一下该函数的实现过程,这个函数大约在net/http的server.go的3019行左右
从上图可以看到,利用传进来的“9090”端口(handler为nil)创建了一个Server实例,接着调用实例方法ListenAndServe(),那么来看看Server是怎么定义的(一部分,完整的自行查看)。可以看到Server结构体里的Handler是接口类型,跟上文提到的一样
再来理解实例方法ListenAndServe()做了什么?下图,net.Listen()返回一个listener类型以监听客户端的连接,也就是继续调用实例方法Serve()(这里实例方法不停调用,看得贼难受)。怎么监听请求的连接?熟悉socket通信的都知道,无非就是开个循环不停的accept(),有请求过来就继续代码逻辑,没有就干等,阻塞等待。那看看Serve()是不是真的这么干?
下文是Serve()函数的源码。Serve()函数也是一个核心函数,看到for循环没?这是个死循环,通过“rw, e := l.Accept()”这句代码进行阻塞,这样又回到socket通信的原理。
值得注意的是在这个函数的最后有一句“go c.serve(ctx)”,这样就专门为这一次的连接开一个goroutine去处理请求,映射到相应的处理逻辑中去。这句代码也充分体现了golang在语言层面上就具有高并发的特性
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
var tempDelay time.Duration // how long to sleep on accept failure
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, e := l.Accept() // 等待连接---------------------
if e != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
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) // 处理请求--------------------------
}
}
总结:
http.ListenAndServe()函数总的来说做了两件大事,一是不停监听请求;二是对每一个请求开一个goroutine去处理,去比对路由表,映射到相应的处理逻辑。
四、在浏览器输入相应的地址,怎么就给我映射到了相应的函数中去呢?
从第三节的最后我们了解到对每一次的请求就开一个goroutine去处理
go c.serve(ctx)
由于c.serve(ctx)函数太长,我就不一一post上来了,看主要的逻辑思路就好。创建一个serverHandler实例,由下面的图片可以看出这个serverHandler实例的srv成员根本就是我们一开始就创建的Server实例啊,见第三节中的第一张图片。紧接着就是调用了ServeHTTP(w, w.req)方法。
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
... ...
for {
... ...
// c.server就是 Server{Addr: addr, Handler: handler} handler为nil
// serverHandler{c.server}创建了一个实例
serverHandler{c.server}.ServeHTTP(w, w.req)
... ...
}
真正的重头戏来了,下图的“ handler = DefaultServeMux ”是关键的代码,记得吗? DefaultServeMux是全局变量,它指向ServeMux路由器,ServeMux里有muxEntry,muxEntry是注册了很多路由实例,只要把请求进来的pattern去比对muxEntry里的pattern就可以把请求映射到相应的处理函数中去,忘了这几者的定义可以回到第二节的第二张图或者倒数第三张图。接下来“ handler.ServeHTTP(rw, req) ”就是描述怎么通过请求的pattern查找已经在DefaultServeMux注册好的pattern,从而映射到相应的处理函数(handler)中去。
上图的“ hander.ServeHTTP(rw, req) ”实际上就是调用了ServeMux的成员方法ServeHTTP()如下图。在成员函数中又调用了另外一个成员函数Handler()得到" h “,” h “是什么?” h “就是handler啊!是全局变量DefaultServeMux里已经注册好的路由表里将要映射到处理逻辑的函数,是muxEntry的成员” h “, 它是一个接口类型,就是我们的” myhandler “。
Handler()这个函数很简单,只是把请求的host、pattern拿出来,再放到另外一个成员函数handler()中。本文的例子,直接了解最后一句代码就可以,其他的代码无非就是加了一些其他配置后走的逻辑。
handler()这个函数是一个中转函数,它拿着host、pattern按照条件去路由表里对比,找到映射的处理逻辑,本文是走第二个if语句的逻辑。
match()函数,毫无疑问就是核心函数了,它描述了如何去根据请求的pattern去匹配到相应的处理逻辑handler,本文例子,for语句块的代码逻辑可以忽略。
通过match()函数,我们知道根据请求的pattern去路由表里查找相应的handler,也就是该请求相应的处理函数。找到的处理函数怎么让它生效?我们回头看看
上图中拿到的"h"也就是处理请求的函数名,然后怎么实现调用这个函数?文章就在” h.ServeHTTP(w, r) "里,这句代码充分体现了接口的灵活性,它是怎么调用的呢?答案在下两张图
其实就相当于下图的代码,这里可能会有点绕,建议补一下 接口与type关键字定义的类型 这一部分内容
五、最后
这边文章其实是我看beego框架源码时的附属品,这对我理解beego源码时帮助很大。
看源码是一个极为费精神,并且考验耐心的过程,往往代码量上来之后常常看了下文忘了上文,有时觉得代码的编排极为混乱(毕竟不是自己写的,不了解那些大神的思路)。但是,看源码也是一个极为享受的过程,可以帮助自己查漏补缺,学到更多的不同写法,学到更好的编程思想。