目录
HTTP 服务端
同样的,我们可以设想作为 HTTP 服务端处理一次请求应该具备的行为:
- 实现处理函数。
- 预设 URL、Request Method、处理函数,三者之间的路由映射。
- 监听请求。
- 分发请求并完成处理。
net/http 将上述行为整合为了两大步骤:
- 注册处理程序和请求路由。
- 监听并处理请求。
通过一个最简单的示例来感受:
package main
import (
"fmt"
"net/http"
)
func indexHandler(w http.ResponseWriter, request *http.Request) {
fmt.Fprintln(w, "Hello World.")
}
func main() {
http.HandleFunc("/", indexHandler)
err := http.ListenAndServe("127.0.0.1:80", nil)
if err != nil {
fmt.Println("ListenAndServe: ", err)
}
}
- http.HandleFunc 函数:用于注册处理程序和请求路由。
- http.ListenAndServe 函数:用于监听和处理外部请求。
实现原理
注册处理程序和请求路由
对于 net/http 而言,HTTP 服务器的本质就是一组实现了 http.Handler 接口的 Handlers(处理器集合),处理 HTTP 请求时,根据请求的 URL 进行路由选择到合适的 Handler 进行处理:
主要体现为两个关键的结构体 http.ServeMux 和 http.Handler:
-
http.Handler:处理 Request 并返回 Response。任何满足了 http.Handler 接口的对象都可作为一个 Handler。http.Handler 是 net/http 服务端具有可扩展性的核心体现。
-
http.ServrMux:本质上是一个 HTTP 请求路由器,或者叫多路复用器(Multiplexor)。它把收到的请求的 URL 与一组预先定义的 HASH 表进行匹配,HASH 的 keys 就是 URL paths + Request Methods 的组合、values 就是关联的 Handlers。
当我们直接调用 http.HandleFunc 注册一个 Handler(处理器)和请求路由时,默认使用的是 http.DefaultServeMux(路由器),会直接调用 http.ServeMux.HandleFunc 方法:
上述方法会将 Handler 转换成 http.Handler 接口类型调用 net/http.ServeMux.Handle 注册一个 Handler:
func (mux *ServeMux) Handle(pattern string, handler Handler) {
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
路由和对应的 Handler 被组成 http.DefaultServeMux 会持有一个 http.muxEntry 哈希,其中存储了从 URL 到 Handler 映射关系,HTTP 服务器在处理请求时就会使用该哈希查找 Handler。
net/http 自建了几个常用的 Handler:FileServer、RedirectHandler 和 NotFoundHandler。
例子:
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
rh := http.RedirectHandler("http://www.baidu.com", 307)
mux.Handle("/foo", rh)
log.Println("Listening...")
http.ListenAndServe(":3000", mux)
}
- 调用了 http.NewServeMux 函数来创建一个空的 ServeMux。
- 调用了 http.RedirectHandler 函数创建了一个新的 Handler,这个 Handler 会对收到的所有请求都执行 307 重定向到指定的 URL,如 http://www.baidu.com。
- 接下来使用 ServeMux.Handle 函数将 Handler 注册到新建的 ServeMux,所以它在 URL path /foo 上收到所有的请求都交给这个 Handler。
- 最后创建了一个新的 ServeHTTP,并通过 http.ListenAndServe 函数监听所有进入的请求,通过传递刚才创建的 ServeMux 来为请求去匹配对应 Handler。
监听并处理请求
http.ListenAndServe 函数用于监听 TCP 连接并处理请求,该函数会使用传入的监听地址(addr)和 Handler 初始化一个 HTTP 服务器 http.Server,调用该服务器的 http.Server.ListenAndServe 方法:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
http.Server.ListenAndServe 方法使用了 net 库提供的 net.Listen 函数监听对应地址上的 TCP 连接并通过 http.Server.Serve 处理客户端的请求:
func (srv *Server) ListenAndServe() error {
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
http.Server.Serve 会在循环中监听外部的 TCP 连接并为每个连接调用 http.Server.newConn 创建新的结构体 http.conn,它是 HTTP 连接的服务端表示:
func (srv *Server) Serve(l net.Listener) error {
l = &onceCloseListener{Listener: l}
defer l.Close()
baseCtx := context.Background()
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
...
return err
}
connCtx := ctx
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx)
}
}
创建了服务端的连接之后,net/http 中的实现会为每个 HTTP 请求创建单独的 Goroutine 并在其中调用 http.Conn.serve 方法,如果当前 HTTP 服务接收到了海量的请求,会在内部创建大量的 Goroutine,这可能会使整个服务质量明显降低无法处理请求。
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
for {
w, _ := c.readRequest(ctx)
serverHandler{c.server}.ServeHTTP(w, w.req)
w.finishRequest()
...
}
}
上述代码片段是简化后的连接处理过程,其中包含读取 HTTP 请求、调用 Handler 处理 HTTP 请求以及调用完成该请求。读取 HTTP 请求会调用 http.Conn.readRequest 方法,该方法会从连接中获取 HTTP 请求并构建一个实现了 http.ResponseWriter 接口的变量 http.response,向该结构体写入的数据都会被转发到它持有的缓冲区中:
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
...
w.written += int64(lenData)
if w.contentLength != -1 && w.written > w.contentLength {
return 0, ErrContentLength
}
if dataB != nil {
return w.w.Write(dataB)
} else {
return w.w.WriteString(dataS)
}
}
解析了 HTTP 请求并初始化 http.ResponseWriter 之后,我们就可以调用 http.serverHandler.ServeHTTP 方法查找处理器来处理 HTTP 请求了:
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)
}
如果当前的 HTTP 服务器中不包含任何 Handler,那么就会使用默认的 http.DefaultServeMux 处理外部的 HTTP 请求。
http.ServeMux 是一个 HTTP 请求的多路复用器,它可以接收外部的 HTTP 请求、根据请求的 URL 匹配并调用最合适的处理器:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
经过一系列的函数调用,上述过程最终会调用 HTTP 服务器的 http.ServerMux.match 方法,该方法会遍历前面注册过的路由表并根据特定规则进行匹配:
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
如果请求的路径和路由中的表项匹配成功,就会调用表项中对应的 Handler,Handler 中包含的业务逻辑会通过 http.ResponseWriter 构建 HTTP 请求对应的响应并通过 TCP 连接发送回客户端。