ServerMux

ServeMux详解 - 掘金 (juejin.cn)

03.4. Go 的 http 包详解 | 第三章. Web 基础 |《Go Web 编程》

以下实现了一个http服务。编译运行后访问http://127.0.0.1:8080/。

1
2
3
4
5
6
7
8
9
10
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8080", nil)) // 启动服务
// ListenAndServe always returns a non-nil error.
}

func handler(w http.ResponseWriter, r *http.Request) {
// 写入回应(实现了io.Writer)
fmt.Fprintf(w, "hello world")
}

http包中,Handler是一个接口,规定了作为处理器需要有ServeHTTP方法

HandlerFunc是一个类型,实现了ServeHTTP方法。通过把一个func(ResponseWriter, *Request)类型的函数值到HandlerFunc进行类型强转,实现了包装

1
2
3
4
5
6
7
8
9
type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 路由实现器
}

type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

Golang中,ServeMux类型实现了路由功能(将HTTP请求映射到对应的处理器)。

1
2
3
4
5
6
7
8
9
10
type ServeMux struct {
mu sync.RWMutex // 锁,由于请求涉及到并发处理,因此这里需要一个锁机制
m map[string]muxEntry // 路由规则,一个 string 对应一个 mux 实体,这里的 string 就是注册的路由表达式
hosts bool // 是否在任意的规则中带有 host 信息
}

type muxEntry struct {
h Handler // 这个路由表达式对应哪个 handler
pattern string // 匹配字符串
}

ServeMux的Handle方法通过将传入的路由和处理函数存入 ServeMux 的映射表 m 中来实现 “路由注册(register)”。

ServeMux还有一个方便的HandleFunc 方法,接收一个具体的处理函数将其包装后再传给Handle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
if pattern == "" { // 检查路由路径是否为空
panic("http: invalid pattern")
}
if handler == nil { // 检查处理函数是否为空
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist { // 检查该路由是否已经注册过
panic("http: multiple registrations for " + pattern)
}
if mux.m == nil { // 如果还没有任何路由注册,就为 mux.m 分配空间
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern} // 实例化一个 muxEntry
mux.m[pattern] = e // 将该路由与该 muxEntry 的实例存到 mux.m 中
// 如果该路由路径以 "/" 结尾,就把该路由按照大到小的路径长度插入到 mux.e 中
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
// 如果该路由路径不以 "/" 开始,标记该 mux 中有路由的路径带有主机名
if pattern[0] != '/' {
mux.hosts = true
}
}

在一个ServerMux中,可以注册多个处理程序(Handler),每个处理程序对应一个URL路径。当一个HTTP请求到达ServerMux时,ServerMux会根据请求的URL路径选择对应的处理程序,并将请求传递给该处理程序进行处理。

1
2
3
4
5
6
7
8
9
mux := http.NewServeMux()

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Welcome to my website!")
})

mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
})

路由器里面存储好了相应的路由规则之后,那么具体的请求又是怎么分发的?见下面的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
w.Header().Set("Connection", "close")
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}


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)
return RedirectHandler(p, StatusMovedPermanently), pattern
}
}
return mux.handler(r.Host, 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
}

本质上根据用户请求的 URL 和路由器里面存储的 map 去匹配的,当匹配到之后返回存储的 handler,调用这个 handler 的 ServeHTTP 接口就可以执行到相应的函数了。

在源码中,ServeMux类型实现了Handler 接口(ServeHTTP方法),我们可以将它传给http.ListenAndServe

1
2
3
4
5
6
7
8
func main() {
// 创建一个新的路由器
router := http.NewServeMux()
// 将处理函数与 URL 路径进行映射
router.HandleFunc("/", homeHandler)
// 启动 HTTP 服务器,监听 8080 端口
http.ListenAndServe(":8080", router)
}

可以嵌套使用ServerMux,即一个ServerMux可以作为另一个ServerMux的处理程序,这种方式可以让我们更灵活地组织和管理HTTP请求的处理程序——

1
2
3
4
5
6
7
8
9
10
11
rootMux := http.NewServeMux()
rootMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Welcome to my website!")
})

helloMux := http.NewServeMux()
helloMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
})

rootMux.Handle("/hello", helloMux)

回到最初。http.HandleFunc("/", handler)的实现如下:

1
2
3
4
// server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}

在一个应用程序的多个文件中定义HTTP handler也是非常典型的,如果它们必须全部都显式地注册到这个应用的ServeMux实例上会比较麻烦。所以为了方便,net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handle和http.HandleFunc函数。

http.ListenAndServe做了什么工作?它创建了Server对象,调用ListenAndServe方法启动端口监听和请求接收。

1
2
3
4
5
6
7
8
9
10
11
12
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

type Server struct {
// 服务端的 host 和 端口号
Addr string

Handler Handler // 默认为 ServeMux
// 省略其他参数
}

Server对象使用 net.Listen 来启动监听端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// server.go
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)
}

img

在 Serve 方法中,有一个永久for循环。接收到请之后,会为每一个请求创建一个 conn 实例,conn 表示服务端的一个 HTTP 连接,并启动一个新的 goroutine 来处理这个请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
var tempDelay time.Duration // how long to sleep on accept failure
for {
rw, e := l.Accept()
if e != nil {
...
return e
}
tempDelay = 0
c, err := srv.newConn(rw)
if err != nil {
continue
}
go c.serve()
}
}

在 conn 的 serve 方法中,其实就只做了两件事,一件事读取请求中的数据,然后是调用 ServeHTTP 方法进行处理。