http 是典型的 C/S 架构,客户端向服务端发送请求(request),服务端做出应答(response)。
golang 的标准库 net/http
提供了 http 编程有关的接口,封装了内部TCP连接和报文解析的复杂琐碎的细节,使用者只需要和 http.request
和 http.ResponseWriter
两个对象交互就行。也就是说,我们只要写一个 handler,请求会通过参数传递进来,而它要做的就是根据请求的数据做处理,把结果写到 Response 中。废话不多说,来看看 hello world 程序有多简单吧!
package main
import (
"io"
"net/http"
)
type helloHandler struct{}
func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
}
func main() {
http.Handle("/", &helloHandler{})
http.ListenAndServe(":12345", nil)
}
运行 go run hello_server.go
,我们的服务器就会监听在本地的 12345
端口,对所有的请求都会返回 hello, world!
:
正如上面程序展示的那样,我们只要实现的一个 Handler,它的接口原型是(也就是说只要实现了 ServeHTTP
方法的对象都可以作为 Handler):
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
然后,注册到对应的路由路径上就 OK 了。
http.HandleFunc
接受两个参数:第一个参数是字符串表示的 url 路径,第二个参数是该 url 实际的处理对象。
http.ListenAndServe
监听在某个端口,启动服务,准备接受客户端的请求(第二个参数这里设置为 nil
,这里也不要纠结什么意思,后面会有讲解)。每次客户端有请求的时候,把请求封装成 http.Request
,调用对应的 handler 的 ServeHTTP
方法,然后把操作后的http.ResponseWriter
解析,返回到客户端。
上面的代码没有什么问题,但是有一个不便:每次写 Handler 的时候,都要定义一个类型,然后编写对应的 ServeHTTP
方法,这个步骤对于所有 Handler 都是一样的。重复的工作总是可以抽象出来,net/http
也正这么做了,它提供了 http.HandleFunc
方法,允许直接把特定类型的函数作为 handler。上面的代码可以改成:
package main
import (
"io"
"net/http"
)
func helloHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "hello, world!\n")
}
func main() {
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":12345", nil)
}
其实,HandleFunc
只是一个适配器,
// 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 object that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
自动给 f
函数添加了 HandlerFunc
这个壳,最终调用的还是 ServerHTTP
,只不过会直接使用 f(w, r)
。这样封装的好处是:使用者可以专注于业务逻辑的编写,省去了很多重复的代码处理逻辑。如果只是简单的 Handler,会直接使用函数;如果是需要传递更多信息或者有复杂的操作,会使用上部分的方法。
如果需要我们自己写的话,是这样的:
package main
import (
"io"
"net/http"
)
func helloHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "hello, world!\n")
}
func main() {
// 通过 HandlerFunc 把函数转换成 Handler 接口的实现对象
hh := http.HandlerFunc(helloHandler)
http.Handle("/", hh)
http.ListenAndServe(":12345", nil)
}
大部分的服务器逻辑都需要使用者编写对应的 Handler,不过有些 Handler 使用频繁,因此 net/http
提供了它们的实现。比如负责文件 hosting 的 FileServer
、负责 404 的NotFoundHandler
和 负责重定向的RedirectHandler
。下面这个简单的例子,把当前目录所有文件 host 到服务端:
package main
import (
"net/http"
)
func main() {
http.ListenAndServe(":12345", http.FileServer(http.Dir(".")))
}
强大吧!只要一行逻辑代码就能实现一个简单的静态文件服务器。从这里可以看出一件事:http.ListenAndServe
第二个参数就是一个 Handler 函数(请记住这一点,后面有些内容依赖于这个)。
运行这个程序,在浏览器中打开 http://127.0.0.1:12345
,可以看到所有的文件,点击对应的文件还能看到它的内容。
其他两个 Handler,这里就不再举例子了,读者可以自行参考文档。
虽然上面的代码已经工作,并且能实现很多功能,但是实际开发中,HTTP 接口会有许多的 URL 和对应的 Handler。这里就要讲 net/http
的另外一个重要的概念:ServeMux
。Mux
是 multiplexor
的缩写,就是多路传输的意思(请求传过来,根据某种判断,分流到后端多个不同的地方)。ServeMux
可以注册多了 URL 和 handler 的对应关系,并自动把请求转发到对应的 handler 进行处理。我们还是来看例子吧:
package main
import (
"io"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello, world!\n")
}
func echoHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, r.URL.Path)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/", echoHandler)
http.ListenAndServe(":12345", mux)
}
这个服务器的功能也很简单:如果在请求的 URL 是 /hello
,就返回 hello, world!
;否则就返回 URL 的路径,路径是从请求对象 http.Requests
中提取的。
这段代码和之前的代码有两点区别:
NewServeMux
生成了 ServerMux
结构,URL 和 handler 是通过它注册的http.ListenAndServe
方法第二个参数变成了上面的 mux
变量还记得我们之前说过,http.ListenAndServe
第二个参数应该是 Handler 类型的变量吗?这里为什么能传过来 ServeMux
?嗯,估计你也猜到啦:ServeMux
也是是 Handler
接口的实现,也就是说它实现了 ServeHTTP
方法,我们来看一下:
type ServeMux struct {
// contains filtered or unexported fields
}
func NewServeMux() *ServeMux
func (mux *ServeMux) Handle(pattern string, handler Handler)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
哈!果然,这里的方法我们大都很熟悉,除了 Handler()
返回某个请求的 Handler。Handle
和 HandleFunc
这两个方法 net/http
也提供了,后面我们会说明它们之间的关系。而 ServeHTTP
就是 ServeMux
的核心处理逻辑:根据传递过来的 Request,匹配之前注册的 URL 和处理函数,找到最匹配的项,进行处理。可以说ServeMux
是个特殊的 Handler,它负责路由和调用其他后端 Handler 的处理方法。
关于ServeMux
,有几点要说明:
/
:表示一个子树,后面可以跟其他子路径; 末尾不是 /
,表示一个叶子,固定的路径/
结尾的 URL 可以匹配它的任何子路径,比如 /images
会匹配 /images/cute-cat.jpg
ServeMux
也会识别和处理 .
和 ..
,正确转换成对应的 URL 地址你可能会有疑问?我们之间为什么没有使用 ServeMux
就能实现路径功能?那是因为net/http
在后台默认创建使用了 DefaultServeMux
。
嗯,上面基本覆盖了编写 HTTP 服务端需要的所有内容。这部分就分析一下,它们的源码实现,加深理解,以后遇到疑惑也能通过源码来定位和解决。
首先来看 http.ListenAndServe()
:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
这个函数其实也是一层封装,创建了 Server
结构,并调用它的 ListenAndServe
方法,那我们就跟进去看看:
// 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
......
}
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections. If
// srv.Addr is blank, ":http" is used.
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
Server
保存了运行 HTTP 服务需要的参数,调用 net.Listen
监听在对应的 tcp 端口,tcpKeepAliveListener
设置了 TCP 的 KeepAlive
功能,最后调用 srv.Serve()
方法开始真正的循环逻辑。我们再跟进去看看 Serve
方法:
// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
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 {
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
// 创建 Conn 连接
c, err := srv.newConn(rw)
if err != nil {
continue
}
c.setState(c.rwc, StateNew) // before Serve can return
// 启动新的 goroutine 进行处理
go c.serve()
}
}
最上面的注释也说明了这个方法的主要功能:
Listener l
传递过来的请求srv.Handler
func (c *conn) serve() {
origConn := c.rwc // copy it before it's set nil on Close or Hijack
...
for {
w, err := c.readRequest()
if c.lr.N != c.server.initialLimitedReaderSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
...
// HTTP cannot have multiple simultaneous active requests.[*]
// Until the server replies to this request, it can't read another,
// so we might as well run the handler in this goroutine.
// [*] Not strictly true: HTTP pipelining. We could let them all process
// in parallel even if their responses need to be serialized.
serverHandler{c.server}.ServeHTTP(w, w.req)
w.finishRequest()
if w.closeAfterReply {
if w.requestBodyLimitHit {
c.closeWriteAndWait()
}
break
}
c.setState(c.rwc, StateIdle)
}
}
看到上面这段代码 serverHandler{c.server}.ServeHTTP(w, w.req)
这一句了吗?它会调用最早传递给 Server
的 Handler 函数:
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
了吗?如果没有 handler 为空,就会使用它。handler.ServeHTTP(rw, req)
,Handler 接口都要实现 ServeHTTP
这个方法,因为这里就要被调用啦。
也就是说,无论如何,最终都会用到 ServeMux
,也就是负责 URL 路由的家伙。前面也已经说过,它的 ServeHTTP
方法就是根据请求的路径,把它转交给注册的 handler 进行处理。这次,我们就在源码层面一探究竟。
我们已经知道,ServeMux
会以某种方式保存 URL 和 Handlers 的对应关系,下面我们就从代码层面来解开这个秘密:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry // 存放路由信息的字典!\(^o^)/
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
explicit bool
h Handler
pattern string
}
没错,数据结构也比较直观,和我们想象的差不多,路由信息保存在字典中,接下来就看看几个重要的操作:路由信息是怎么注册的?ServeHTTP
方法到底是怎么做的?路由查找过程是怎样的?
// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
// 边界情况处理
if pattern == "" {
panic("http: invalid pattern " + pattern)
}
if handler == nil {
panic("http: nil handler")
}
if mux.m[pattern].explicit {
panic("http: multiple registrations for " + pattern)
}
// 创建 `muxEntry` 并添加到路由字典中
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
if pattern[0] != '/' {
mux.hosts = true
}
// 这是一个很有用的小技巧,如果注册了 `/tree/`, `serveMux` 会自动添加一个 `/tree` 的路径并重定向到 `/tree/`。当然这个 `/tree` 路径会被用户显示的路由信息覆盖。
// Helpful behavior:
// If pattern is /tree/, insert an implicit permanent redirect for /tree.
// It can be overridden by an explicit registration.
n := len(pattern)
if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
// If pattern contains a host name, strip it and use remaining
// path for redirect.
path := pattern
if pattern[0] != '/' {
// In pattern, at least the last character is a '/', so
// strings.Index can't be -1.
path = pattern[strings.Index(pattern, "/"):]
}
mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(path, StatusMovedPermanently), pattern: pattern}
}
}
路由注册没有什么特殊的地方,很简单,也符合我们的预期,注意最后一段代码对类似/tree
URL 重定向的处理。
// 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)
}
好吧,ServeHTTP
也只是通过 mux.Handler(r)
找到请求对应的 handler,调用它的ServeHTTP
方法,代码比较简单我们就显示了,它最终会调用 mux.match()
方法,我们来看一下它的实现:
// Does path match pattern?
func pathMatch(pattern, path string) bool {
if len(pattern) == 0 {
// should not happen
return false
}
n := len(pattern)
if pattern[n-1] != '/' {
return pattern == path
}
// 匹配的逻辑很简单,path 前面的字符和 pattern 一样就是匹配
return len(path) >= n && path[0:n] == pattern
}
// 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) {
var n = 0
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
// 最长匹配的逻辑在这里
if h == nil || len(k) > n {
n = len(k)
h = v.h
pattern = v.pattern
}
}
return
}