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()
Handler
http.HandleFunc()
HandlerFunc
DefaultServeMux
而在我们的MindMap中,形成了下面的结构:
To Be Continued…