Go 语言编程 — net/http — HTTP 服务端

目录

HTTP 服务端

同样的,我们可以设想作为 HTTP 服务端处理一次请求应该具备的行为:

  1. 实现处理函数
  2. 预设 URL、Request Method、处理函数,三者之间的路由映射
  3. 监听请求
  4. 分发请求并完成处理

net/http 将上述行为整合为了两大步骤:

  1. 注册处理程序和请求路由。
  2. 监听并处理请求。

通过一个最简单的示例来感受:

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)
}
  1. 调用了 http.NewServeMux 函数来创建一个空的 ServeMux。
  2. 调用了 http.RedirectHandler 函数创建了一个新的 Handler,这个 Handler 会对收到的所有请求都执行 307 重定向到指定的 URL,如 http://www.baidu.com。
  3. 接下来使用 ServeMux.Handle 函数将 Handler 注册到新建的 ServeMux,所以它在 URL path /foo 上收到所有的请求都交给这个 Handler。
  4. 最后创建了一个新的 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 连接发送回客户端。

支持 https

HTTP 客户端

HTTP 服务端

猜你喜欢

转载自blog.csdn.net/Jmilk/article/details/107526723