1. Begin
使用Go语言的标准库net/http可以快速构建一个简单的web服务。这篇文章从零开始构建一个简单的web服务器,并主要聚焦于处理器Handler和多路复用器ServeMux。
在开始之前,MindMap里一无所有:
2. The Simplest Server
使用net/http包中的ListenAndServe()函数可以快速构建一个最简单的服务器:
package main
import (
"net/http"
)
func main() {
http.ListenAndServe("",nil)
}
使用go run命令执行程序,然后在本地浏览器打开http://localhost ,得到404 page not found的错误。
这就是一个最简单的服务器,什么也不做,但确实是一个服务器。
现在MindMap里多了点东西:
3. 发生了什么
调用http.ListenAndServe()函数就可以启动一个服务器,那么发生了什么呢?
查看源码可知这个函数的实现:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
原来是通过构造一个Server并调用这个Server的ListenAndServe()函数实现的。
那也就是说,我们可以自己构造一个Server,然后启动服务器:
func main() {
s := http.Server{
Addr: "127.0.0.1:8080",
Handler: nil,
}
s.ListenAndServe()
}
启动之后结果一样(如果端口被占用可以换一个端口)。
使用http.ListenAndServe()函数可以使用两个参数(Addr服务器地址和Handler处理器)就可以启动一个简单的服务。如果需要更加复杂的设置,需要使用Server结构。Server定义如下:
// 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
TLSConfig *tls.Config
ReadTimeout time.Duration
ReadHeaderTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
BaseContext func(net.Listener) context.Context
ConnContext func(ctx context.Context, c net.Conn) context.Context
}
这样,MindMap中又多了点东西,也知道了一些关联:
3. 处理器Handler
由于Handler为空,所以这个服务器什么也不做只是返回404。为了让服务器做事情,需要添加处理器Handler:
type MyHandler struct{}
func (mh *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
func main() {
s := http.Server{
Addr: "127.0.0.1:8080",
Handler: &MyHandler{},
}
s.ListenAndServe()
}
执行结果就是得到了一个Hello World。
这里定义了一个简单的处理器MyHandler,然后添加到定义的Server中。
所以为了让服务器有事情做,需要定义Handler告诉它做什么。一个Handler其实是一个接口:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
任何结构只要有一个签名是这样的函数,都可以作为Server的处理器;同样,任何接受这两个参数的函数,也可以当做一个处理器。
不过这么做有个明显的缺陷就是,服务器不会通过URL来将请求路由到不同的处理器。为了能根据URL处理不同的请求,我们需要多个处理器。
4. 多个Handler
直接对Server指定Handler只能添加一个处理器,为了支持多个处理器,需要别的做法:
type HelloHandler struct{}
func (hh *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello")
}
type WorldHandler struct{}
func (wh *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "World")
}
func main() {
s := http.Server{
Addr: "127.0.0.1:8080",
}
http.Handle("/hello", &HelloHandler{})
http.Handle("/world", &WorldHandler{})
s.ListenAndServe()
}
在这里定义了两个Handler,然后通过http.Handle()函数注册这两个Handler而不是直接对Server指定,这样就可以使用两个处理器了。在浏览器中分别访问http://localhost:8080/hello 和 http://localhost:8080/world 可以得到不同的结果。
来看看http.Handle()函数做了什么:
// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
原来是把具体的Handler注册到了一个叫做DefaultServeMux的变量里,在详细看看具体做了什么之前,先看看处理器函数HandlerFunc。
5. 处理器函数HandlerFunc
除了使用Handler结构外,还可以使用具有相同参数的函数,只不过需要使用http.HandleFunc()函数:
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello")
}
func world(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "World")
}
func main() {
s := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/hello", hello)
http.HandleFunc("/world", world)
s.ListenAndServe()
}
效果和使用http.Handle()一样。处理器函数能够完成和处理器一样的功能,代码也更简洁一些。可以在实际使用中根据不同场景使用不同的方式。比如,如果代码中已经有处理的接口,只需要添加一个ServeHTTP函数就可以把这个接口转换成处理器了。
来看看http.HandleFunc()做了什么:
// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
又是DefaultServeMux,接下来详细看看这个是什么,以及它是怎么和Server关联起来的。
6. 多路复用器ServeMux和DefaultServeMux
不管使用http.Handle()注册处理器Handler还是使用http.HandleFunc()注册处理器函数HandlerFunc,内部都涉及到了一个变量DefaultServeMux,这个是默认的多路复用器,即ServeMux的一个实例:
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
多路复用器的作用就是根据URL将不同的请求路由到不同的处理器上进行处理:
ServeMux结构里有一个map,用来保存路由和处理器的映射:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
而ServeMux的Handle()和HandleFunc()函数就是将处理器注册到这个map里的。
不过HandleFunc()怎么把一个函数变成一个处理器呢?
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
原来使用了一个名叫HandlerFunc的结构(注意和http.HandleFunc的区别)完成了转换。HandlerFunc定义如下:
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
这样我们就理解了不管是http.Handle()还是http.HandleFunc()最终都归结于ServeMux.Handle()。如果不指定ServeMux的话,就使用默认的DefaultServeMux。
还需要注意一点就是,ServeMux本身也实现了ServeHTTP()函数,也是可以作为处理器的,这就是DefaultServeMux可以作为处理器的原因。
7. ServeMux是如何路由的
我们已经大致了解了ServeMux是根据内部维护的一个map来找到具体的处理器的。这里来看看具体的实现细节,对于理解整个流程很有帮助。
回到最开始,不管是使用http.ListenAndServe()还是Server.ListenAndServe(),都是需要先Listen然后Serve,主要的过程就是创建一个TCP链接然后监听请求,最终就是到serve()函数里,主要代码如下:
func (c *conn) serve(ctx context.Context) {
...
serverHandler{c.server}.ServeHTTP(w, w.req)
...
}
这里使用了一个叫做serverHandler的代理:
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)
}
从这里可以看到DefaultServeMux就是默认的多路复用器。所以如果给Server指定了一个Handler,那么就会调用这个Handler的ServeHTTP()函数;如果没有指定的话,就调用DefaultServeMux的ServeHTTP()函数,毕竟我们在前面已经知道DefaultServeMux就是ServeMux的一个实例,而ServeMux实现了ServeHTTP()函数:
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request 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
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
这个函数所做的就是根据请求找到对应的Handler,然后让这个处理器完成具体的工作。下面就是ServeMux.Handler()函数的主要部分ServeMux.handler():
// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
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
}
里面调用了ServeMux.match()函数,其实就是根据URL到map去找具体的Handler:
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
// Check for exact match first.
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
// Check for longest valid match. mux.es contains all patterns
// that end in / sorted from longest to shortest.
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
首先,精确匹配,找到了就返回。如果没有找到,就根据一定的规则来找,最后如果还没有找到,那么返回404。
在后面的那个循环中,来找最长匹配的URL。mux.es是一个切片,这里存储的都是在注册Handler时模式以/结尾的:
func (mux *ServeMux) Handle(pattern string, handler Handler) {
...
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
...
}
这样就清楚了,路由过程如下:
- 精确匹配,找到就返回;
- 如果没有精确匹配到,就到所有注册
Handler时模式以/结尾的模式中寻找,原则是最长匹配; - 还没有找到的话,就404。
举个例子,比如注册时将hello这个处理器注册到了/hello/下,那么对于/hello/wrong这个请求如果精确匹配没找到的话就可以匹配/hello/,因为这个模式以/结尾;如果注册hello处理器的时候模式是/hello的话,那就不会把/hello/wrong路由到/hello对应的处理器上来了。
ServeMux有一个缺点就是无法使用变量来实现URL模式匹配,比如/post/123这种,可以使用第三方库来避开这个限制。
这篇文章梳理了一下net/http包中下面的几个概念:
http.ListenAndServe()Server.ListenAndServe()http.Handle()Handlerhttp.HandleFunc()HandlerFuncDefaultServeMux
而在我们的MindMap中,形成了下面的结构:
To Be Continued…