一、启动 http 服务
import (
"net/http"
)
func main() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ping...ping..."))
})
http.ListenAndServe(":8999", nil)
}
在 Golang只需要几行代码便能启动一个 http 服务,在上述代码中,完成了两件事:
- 调用 http.HandleFunc 方法,注册了对应于请求路径 /ping 的 handler 函数
- 调用 http.ListenAndServe,启动了一个端口为 8999 的 http 服务
二、server核心数据结构
2.1 server 结构
type Server struct {
Addr string
Handler Handler
mu sync.Mutex
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
TLSConfig *tls.Config
ConnState func(net.Conn, ConnState)
activeConn map[*conn]struct{}
doneChan chan struct{}
listeners map[*net.Listener]struct{}
// ...还有其他变量
}
这些变量在 Server 结构定义了服务器的配置和状态跟踪。它们用于控制服务器监听地址、处理请求、超时设置、连接状态跟踪等方面,并提供了并发安全性的保护。
2.2 Handler 接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Handler 是一个接口,定义了方法: ServeHTTP。任何结构只要实现了这个ServeHTTP方法,那么这个结构体就是一个Handler对象。
ServeHTTP方法的作用是,根据 http 请求 Request 中的请求路径 path 映射到对应的 handler 处理函数,对请求进行处理和响应。ServeHTTP方法有两个参数:ResponseWriter和*Request。
- ResponseWriter是一个接口,用于向客户端发送HTTP响应;
- *Request是一个指向Request结构体的指针,包含来自客户端的HTTP请求的各种信息,如请求方法、URL和请求头等。
通过实现 Handler 接口并定义自己的 ServeHTTP 方法,可以创建HTTP处理程序。当收到一个HTTP请求时,服务器会调用这个处理程序的 ServeHTTP 方法,并将适当的ResponseWriter和*Request给它,以便处理请求并生成响应。
2.3 muxEntry 具体路由
type muxEntry struct {
h Handler
pattern string
}
muxEntry是一个结构体类型,代表具体的路由条目。每个路由条目包含一个路径模式pattern和关联的处理函数h,用于匹配和处理特定的URL路径。
- h是一个 Handler 接口类型的变量,表示与该路由条目关联的处理程序。通过该处理程序可以执行特定的逻辑来处理HTTP请求并生成响应。
- pattern是一个字符串类型的变量,表示该路由条目所匹配的URL模式或路径模式。这个模式用于将特定的HTTP请求与路由条目关联起来,以便选择正确的处理程序进行处理。它用于确定请求的URL是否与该路由条目匹配,并决定选择哪个处理程序来处理该请求。以下是一些pattern的匹配规则
- 完全匹配:pattern可以是一个普通的字符串,比如 "/about"。这意味着只有当请求的URL路径与该字符串完全相同时,才会匹配该路由条目。
- 模糊匹配:pattern以斜线 / 结尾,如 "/products/"。这表示当请求的URL路径只要以该前缀开头时,该路由条目将会匹配。例如,对于请求的URL路径为 "/products/123"、"/products/456",这些路由条目都会与之匹配。
2.4 ServeMux多路路由器
type ServeMux struct {
mu sync.RWMutex //允许多个 goroutine 并发地读取路由表,但在修改路由表时需要独占锁。
m map[string]muxEntry //用于将 所有的URL 模式(或路径模式)映射到 muxEntry 结构体
es []muxEntry //只存储 "/" 结尾的路由,即模糊匹配patter的muxEntry,且切片内按从最长到最短的顺序排列
hosts bool //表示是否有路由模式包含主机名
}
ServeMux是一个用于路由HTTP请求的多路复用器(multiplexer)。它可以将收到的请求根据路径匹配与注册的处理函数进行对应关联,并分发给相应的处理函数进行处理,实际上是一个实现了http.Handler
接口的结构体。内部通过一个 map 维护了从 path 到 handler 的映射关系, 有效地管理和选择正确的处理程序来处理传入的HTTP请求。当收到一个请求时,ServeMux 会根据请求的URL路径进行匹配,并检查注册的路由模式pattern,然后选择适合的处理程序handler进行处理。
2.5 defaultServeMux的全局多路路由器
//全局变量
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
在http包的初始化程中,会创建一个名为defaultServeMux的全局ServeMux实例,然后通过&defaultServeMux将其地址赋值给DefaultServeMux。这样就得到了一个默认的全局变量,我们可以通过直接使用DefaultServeMux来进行请求路由和处理,而管理自己的多路复用器。当我们使用http.HandleFunc等函数来注册处理函数时,默认情况下它们会将路由信息添加到DefaultServeMux上。
三、http.HandleFunc()注册路由底层原理
3.1 http.HandleFunc()
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
如上所示,当调用http.HandleFunc()时,会直接调用全局变量DefaultServeMux.HandleFunc()。HandleFunc的第一个参数是路由表达式,也就是请求路径,第二个参数是一个函数类型,也就是真正处理请求的函数。
3.2 DefaultServeMux.HandleFunc()
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
// ...
mux.Handle(pattern, HandlerFunc(handler))
}
如上所示,在 ServeMux.HandleFunc 定义了接收一个 ResponseWriter 对象和一个 Request 对象的 HandlerFunc 类型。
- HandlerFunc类型是一个适配器,并且这种类型实现了ServeHTTP方法,即是Handler接口的具体实现类型,并在ServeHTTP方法中又调用了被转换的函数自身,也就是说这个类型的函数其实就是一个Handler类型的对象,通过类型转换可以将一个具有func(ResponseWriter, *Request)签名的普通函数转换为一个Handler对象,而不需要再定义一个结构体,再让这个结构实现ServeHTTP方法,非常方便的将普通函数用作HTTP处理程序。
通过将处理函数转换为 HandlerFunc 类型,并将其作为参数传递给 ServeMux 的 Handle 方法,我们可以通过mux.Handle将处理函数注册到 ServeMux 的路由 map 中,以便在收到请求时能够正确匹配处理函数并处理该请求。这种设计模式使得我们可以将不同的处理函数注册到 ServeMux,并根据请求的路径来选择相应的处理函数进行处理。这样可以实现灵活的路由功能,使我们能够根据具体的业务逻辑来处理不同的请求。
3.3 mux.Handle()
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
// 检查路由表达式是否为空
if pattern == "" {
panic("http: invalid pattern")
}
// 检查处理函数是否为空
if handler == nil {
panic("http: nil handler")
}
// 如果这个路由表达式已经注册过处理器函数,直接panic
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
// 如果 mux.m 为nil 通过 make 函数初始化 map
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
// 用路由表达式和处理函数handler创建 muxEntry 对象
e := muxEntry{h: handler, pattern: pattern}
// 所有注册的路由都会存放到mux.m里面
mux.m[pattern] = e
// 如果路由以/做结尾,则在mux.es中存放一份规则,同时做好从长到短的排序
if pattern[len(pattern)-1] == '/' {
// 如果路由patterm以'/'结尾,则将对应的muxEntry对象加入到[]muxEntry中,路由长的位于切片的前面
mux.es = appendSorted(mux.es, e)
}
// 如果该路由路径不以 "/" 开始,标记该 mux 中有路由的路径带有主机名
if pattern[0] != '/' {
mux.hosts = true
}
}
通过以上代码,Handle 方法实现了将传进来的pattern和handler保存在muxEntry结构中,并且pattern作为key,把muxEntry装入到DefaultServeMux的Map里面。简单来说就是保存当前路由和自己定义的那个处理函数。在收到请求时,ServeMux 可以根据请求的 URL 查找到对应的处理函数,并进行处理。同时,将以“/”结尾的模糊匹配路由添加到 es 切片中,同时在es切片中通过appendSorted对路由按从长到短进行排序,代码如下所示。
func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
n := len(es)
i := sort.Search(n, func(i int) bool {
return len(es[i].pattern) < len(e.pattern)
})
if i == n {
return append(es, e)
}
es = append(es, muxEntry{}) // try to grow the slice in place, any entry works.
copy(es[i+1:], es[i:]) // Move shorter entries down
es[i] = e
return es
}
四、http.ListenAndServe()监听与服务底层原理
4.1 Http.ListenAndServe()启动 server
在上述http启动过程中,我们通过调用 net/http 包下的公开方法 http.ListenAndServe,实现了对服务端的一键启动。
而在http.ListenAndServe 内部会声明一个新的 Server 对象,嵌套执行 Server.ListenAndServe 方法。我们使用Http包启动web服务时Handler一般设置为nil,所以使用的就是默认的DefaultServeMux。
//The handler is typically nil, in which case the DefaultServeMux is used.
//Handler通常是nil,此时,会默认使用DefaultServeMux。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
4.2 server.ListenAndServe()
func (srv *Server) ListenAndServe() error {
// ...
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
// ...
return srv.Serve(ln)
}
Server.ListenAndServe 方法中,根据用户传入的地址,申请到一个监听器TCPListener 监听地址addr,继而调用 Server.Serve 方法为这个连接提供服务
4.3 Server.Serve()
var ServerContextKey = &contextKey{"http-server"}
type contextKey struct {
name string
}
func (srv *Server) Serve(l net.Listener) error {
// ...
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
//rw可视为tcp连接
rw, err := l.Accept()
// ...
connCtx := ctx
// ...
//c为http连接
c := srv.newConn(rw)
// ...
go c.serve(connCtx)
}
}
Server.Serve 方法体现了典型的服务端的运行架构,使用For + Listener.Accept
的模式来接受并处理客户端请求。这种模式是一种常见的服务器设计模式,通常用于监听网络连接请求的处理。
在这个方法中,通过循环调用listener.Accept()方法来接受客户端连接。每当有新的连接建立时,将 server 封装成一组 valueContext,并开启一个新的goroutine调用 conn.serve 来处理该连接并传入server 封装后的valueContext,这确保了可以同时处理多个同时到达的连接请求。
4.4 *conn.serve()
*conn.serve()代码很长,核心是下面几行代码,简化之后如下:
func (c *conn) serve(ctx context.Context) {
// ...
c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4