- 开发无止境 -
Data: 2020-01-27 02:08:34Form: JournalClick: 12
基于 HTTP
构建的网络应用包括两个端,即客户端 ( Client
) 和服务端 ( Server
)。
两个端的交互行为包括从客户端发出 request
、服务端接受 request
进行处理并返回 response
以及客户端处理 response
。所以 http
服务器的工作就在于如何接受来自客户端的 request
,并向客户端返回 response
, 如下图所示:
上一章节内容,主要对net/http
包的Client
部分进行了介绍,这章节就对 Server
内容进行介绍。
对于 golang
来说,利用 net/http
包实现一个Http Server
非常简单,只需要简简单单几句代码就可以实现,先看看 Golang
的其中一种 http server
简单的实现:
例1:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello World!")
}
func main() {
//注册路由
http.HandleFunc("/", handler)
//创建服务且监听
http.ListenAndServe(":8080", nil)
}
再来看看另外一种http server
实现,代码如下:
例2:
package main
import (
"fmt"
"net/http"
)
type routeIndex struct {
content string
}
func (route *routeIndex) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, route.content)
}
func main() {
//注册路由
http.Handle("/", &routeIndex{content: "Hello, World"})
//创建服务且监听
http.ListenAndServe(":8080", nil)
}
上述两种方法都实现了简单的 http server
实现,写法虽然不同,但底层用到的原理其实都是一样的,我们通过源码进行解析。
在上述两种实现种,分别调用了 http.Handle
和 http.HandleFunc
来实现路由的处理,展开源码看看:
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
....
}
总结 http.Handle
和 http.HandleFunc
函数代码,可以得出以下几个重点内容: DefaultServeMux
,ServeMux
, Handler
以及 ServeMux.Handle
函数,下面逐一展开说明。
ServerMux
是 net/http
包中的一个路由器(router
),是一个 HTTP
请求多路复用器,用于将收到的 HTTP
请求路由到相应的处理程序(handlers
)。
在 ServerMux
中,我们可以通过调用 HandleFunc
或者 Handle
方法来注册一个路由和对应的处理函数。当一个请求到达 ServerMux
时,路由器会根据请求的 URL
路径找到对应的处理函数,并将请求转发给该函数进行处理。
ServerMux
结构体定义如下:
type ServeMux struct {
mu sync.RWMutex //用于保证并发安全性的互斥锁
m map[string]muxEntry //一个映射表,将URL模式映射到对应的处理程序。在处理HTTP请求时,ServeMux将使用此映射表来查找与请求URL路径匹配的处理程序
es []muxEntry //一个按长度排序的URL模式条目的切片。这个切片是用来加速ServeMux的URL匹配操作的。在处理HTTP请求时,ServeMux会按照长度递减的顺序迭代这个切片,以便找到最长的匹配URL模式
hosts bool //标志位,表示ServeMux是否具有任何带有主机名的URL模式。如果是,则在处理HTTP请求时,ServeMux还需要匹配主机名。如果不是,则可以忽略主机名匹配
}
ServeMux
是一个非常重要的组件,用于将HTTP
请求路由到正确的处理程序,并且在Go
标准库中被广泛使用。在ServeMux
中,还有一个 muxEntry
结构,muxEntry
是 ServeMux
内部维护的数据结构,用于将 URL
路径模式与处理程序相关联。定义代码如下:
type muxEntry struct {
h Handler //一个处理程序,它是用于处理与该URL模式匹配的HTTP请求的函数
pattern string //与该处理程序相关联的URL模式。在ServeMux中,pattern是映射到处理程序的关键字之一。在匹配请求路径时,ServeMux将使用pattern来判断请求是否与该条目匹配。
}
muxEntry
结构体用于将URL
模式与处理程序相关联,以便在处理HTTP
请求时能够正确地路由请求。
再来看看 DefaultServeMux
,可以看到 http.Handle
和 http.HandleFunc
这两个函数最终都由 DefaultServeMux
调用 Handle
方法来完成路由的注册的,该变量定义如下:
var defaultServeMux ServeMux
var DefaultServeMux = &defaultServeMux
这里的 DefaultServeMux
表示一个默认的 ServeMux
,当我们没有创建自定义的 ServeMux
,则会自动使用一个默认的 ServeMux
。
我们可以创建自定义的 ServeMux
取代默认的 DefaultServeMux
,示例代码如下:
package main
import (
"fmt"
"net/http"
)
type routeIndex struct {
content string
}
func (route *routeIndex) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, route.content)
}
func htmlHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
html := `<!doctype html>
<META http-equiv="Content-Type" content="text/html" charset="utf-8">
<html lang="zhCN">
<head>
<title>Golang</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;" />
</head>
<body>
<div id="app">Hello, HandleFunc World!</div>
</body>
</html>`
fmt.Fprintf(w, html)
}
func main() {
//自定义serveMux
mux := http.NewServeMux()
mux.Handle("/Handle", &routeIndex{content: "Hello, Handle World"})
mux.HandleFunc("/HandleFunc", htmlHandler)
//创建服务且监听
http.ListenAndServe(":8080", mux)
}
http.NewServeMux
方法用于创建一个新的 ServeMux
实例,如果调用的是自定义 ServeMux
实例 mux
,那么 Server
实例接收到的路由对象将不再是 DefaultServeMux
而是 mux
。
了解完 ServeMux
,再来看看 Handler
对象:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Handler
对象是个接口,Handler
接口中声明了名为 ServeHTTP
的函数,也就是说任何结构只要实现了这个 ServeHTTP
方法,那么这个结构体就是一个 Handler
对象。http.Handler.ServeHTTP
方法是用来是用以处理 request
并构建 response
的核心逻辑所在。
总结起来一句话,要完成完整的http server
服务,必须完成对Handler
接口的实现,即对 http.Handler.ServeHTTP
方法的实现。
对 Handler
对象有了大概了解后,回到 http.Handle
和 http.HandleFunc
函数,看看他们是怎么实现 Handler
接口的。
先看func Handle(pattern string, handler Handler)
函数, 其第二个参数必须为Handler
接口的实现,所以调用该函数则必须先自行完成对Handler
接口的实现,方式具体参考示例2。
再看看func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
函数,发现第二个参数只需传入满足它参数的一个函数即可,并不需要用户自行去实现 Handler
接口的,究其原因,我们一看源码就明白了:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
注意一下这行代码:mux.Handle(pattern, HandlerFunc(handler))
,这里 HandlerFunc
实际上是将 handler
函数做了一个类型转换,看一下 HandlerFunc
的定义:
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
看到这里,应该最终明白了把,原来这里 HandlerFunc
实际上已经默认实现了Handler
接口,实现了它的ServeHTTP
函数,用户只需要传入 handler
函数即可被HandlerFunc
强行转为一个Handler
对象,这是一种巧妙的转换技巧,不需要定义一个结构体,再让这个结构实现 ServeHTTP
方法。
ServeMux
内部维护一个map[string]muxEntry
,该map
作用将URL
模式映射到对应的muxEntry
结构体,而muxEntry
结构体则将处理程序与URL
模式相关联。
那ServeMux
是如何将将URL
模式映射到对应的muxEntry
结构体的呢?
通过调用 http.Handle
和 http.HandleFunc
函数完成映射的。而这两个函数,最终调用的方法的是: ServeMux.Handle
, 其代码如下:
func (mux *ServeMux) Handle(pattern string, handler Handler) {
//互斥锁,解决 多个goroutine 并发访问时的线程安全
mux.mu.Lock()
defer mux.mu.Unlock()
//检查参数的合法性,如果有不合法的参数,则会抛出 panic 异常
if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
//初始化ServeMux内部保存URL路径模式和处理程序之间映射关系的map(ServeMux.m),如果该 map 还未被初始化,则会在此处进行初始化
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
//将URL路径模式和处理程序建立映射关系
//首先,创建一个 muxEntry 结构体,保存处理程序和 URL 路径模式
//然后,将该 muxEntry 对象加入到 ServeMux 内部维护的 map 中
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
//如果URL路径模式的最后一个字符是斜杠(即该 URL 路径模式对应的处理程序是一个目录),则将该 muxEntry 对象插入到 ServeMux.es中
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
//如果URL路径模式的第一个字符不是斜杠(即该 URL 路径模式对应的处理程序是一个主机名),则将 ServeMux 的 hosts 字段设置为 true
if pattern[0] != '/' {
mux.hosts = true
}
}
上述函数主要作用是:
URL
路径模式和处理程序建立映射关系,并将映射关系保存到 ServeMux
的内部数据结构中URL
路径模式和处理程序不能为空,URL
路径模式不能重复等URL
路径模式按照长度从长到短排序,并标记 ServeMux
是否支持主机名路由。最后用一张图来总结整个注册路由流程:
处理完路由相关信息注册,就要进行TCP
监听服务启动以及TCP
连接并处理请求了。标准库提供的 net/http.ListenAndServe
可以用来监听 TCP
连接并处理请求,该函数会使用传入的监听地址和处理器初始化一个 HTTP
服务器 net/http.Server
,调用该服务器的 net/http.Server.ListenAndServe
方法:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
上述代码先创建了一个 Server
对象,传入了地址和 handler
参数后调用 Server
对象 ListenAndServe()
方法。
特别需要注意的一点是:该函数的第二个参数是 Handler
类型,不管是一个新的 ServeMux
对象mux
,还是默认的 DefaultServeMux
, ServeMux
其本身自己也实现了 Handler
接口,也实现了ServeHTTP
方法,也是一个 Handler
对象,但 ServeMux
的 ServeHTTP()
方法主要作用是匹配当前路由对应的 handler
方法,与自定义的http.Handler.ServeHTTP
方法用以处理 request
并构建 response
功能区别不同。
下面逐一分析。
net/http.Server
是 HTTP
服务器的主要结构体,用于控制 HTTP
服务器的行为。
其结构体定义为:
type Server struct {
//服务器监听的地址和端口号,格式为 "host:port",例如 "127.0.0.1:8080"
Addr string
//HTTP 请求的处理器。对于每个收到的请求,服务器会将其路由到对应的处理器进行处理。通常使用 http.NewServeMux() 方法创建一个默认的多路复用器,并将其作为处理器。如果没有设置该字段,则使用 DefaultServeMux
Handler Handler
//一个布尔值,用于指示是否禁用 OPTIONS 方法的默认实现。如果该值为 true,则在收到 OPTIONS 请求时,服务器不会自动返回 Allow 头部,而是交给用户自行处理。默认为 false,即启用 OPTIONS 方法的默认实现
DisableGeneralOptionsHandler bool
//HTTPS 服务器的 TLS 配置,用于控制 HTTPS 服务器的加密方式、证书、密钥等安全相关的参数
TLSConfig *tls.Config
//HTTP 请求的读取超时时间。如果服务器在该时间内没有读取到完整的请求,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
ReadTimeout time.Duration
//HTTP 请求头部读取超时时间。如果服务器在该时间内没有完成头部读取,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
ReadHeaderTimeout time.Duration
//HTTP 响应的写入超时时间。如果服务器在该时间内没有完成对响应的写入操作,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
WriteTimeout time.Duration
//HTTP 连接的空闲超时时间。如果服务器在该时间内没有收到客户端的请求,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
IdleTimeout time.Duration
//HTTP 请求头部的最大大小。如果请求头部的大小超过该值,服务器就会关闭连接。该字段为 int 类型,默认为 1 << 20(1MB)
MaxHeaderBytes int
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
//连接状态变化的回调函数,用于处理连接的打开、关闭等事件
ConnState func(net.Conn, ConnState)
//错误日志的输出目标。如果该字段为 nil,则使用 log.New(os.Stderr, "", log.LstdFlags) 创建一个默认的日志输出目标
ErrorLog *log.Logger
//所有 HTTP 请求的基础上下文。当处理器函数被调用时,会将请求的上下文从基础上下文派生出来。默认为 context.Background()。
BaseContext func(net.Listener) context.Context
//连接上下文的回调函数,用于创建连接上下文。每个连接上下文都与一个客户端连接相关联。如果未设置该字段,则每个连接的上下文都是 BaseContext 的副本
ConnContext func(ctx context.Context, c net.Conn) context.Context
//标志变量,用于表示服务器是否正在关闭。该变量在执行 Shutdown 方法时被设置为 true,用于避免新的连接被接受
inShutdown atomic.Bool
//标志变量,用于控制服务器是否支持 HTTP keep-alive。如果该变量为 true,则服务器在每次响应完成后都会关闭连接,即不支持 keep-alive。如果该变量为 false,则服务器会根据请求头部中的 Connection 字段来决定是否支持 keep-alive。该变量在执行 Shutdown 方法时被设置为 true,用于关闭正在进行的
disableKeepAlives atomic.Bool
// 一个 sync.Once 类型的值,用于确保在多线程环境下,NextProtoOnce 方法只被调用一次。NextProtoOnce 方法用于设置 Server.NextProto 字段
nextProtoOnce sync.Once
// error 类型的值,用于记录 NextProto 方法的调用结果。该值在多个 goroutine 之间共享,用于检测 NextProto 方法是否成功
nextProtoErr error
//互斥锁,用于保护 Server 结构体的字段。因为 Server 结构体可能被多个 goroutine 并发访问,所以需要使用互斥锁来确保它们的安全性
mu sync.Mutex
//存储 HTTP 或 HTTPS 监听器的列表。每个监听器都是一个 net.Listener 接口类型的实例,用于接收客户端请求。当调用 Server.ListenAndServe() 或 Server.ListenAndServeTLS() 方法时,会为每个监听地址创建一个对应的监听器,并将其添加到该列表中
listeners map[*net.Listener]struct{}
//表示当前处于活动状态的客户端连接的数量。该字段只是一个计数器,并不保证一定准确。该字段用于判断服务器是否处于繁忙状态,以及是否需要动态调整服务器的工作负载等
activeConn map[*conn]struct{}
//在服务器关闭时执行的回调函数列表。当服务器调用 Server.Shutdown() 方法时,会依次执行该列表中的每个回调函数,并等待它们全部执行完毕。该字段可以用于在服务器关闭时释放资源、保存数据等操作
onShutdown []func()
//表示所有监听器的组。该字段包含一个读写互斥锁 sync.RWMutex 和一个映射表 map[interface{}]struct{}。在监听器启动时,会将监听器地址作为键添加到映射表中。该字段主要用于实现优雅地关闭服务器。在服务器关闭时,会遍历所有监听器,逐个关闭它们,并等待所有连接关闭。如果在等待连接关闭时,有新的连接进来,服务器会先将新连接添加到 activeConn 字段中,并等待所有连接关闭后再退出。这样可以保证服务器在关闭过程中,不会丢失任何连接
listenerGroup sync.WaitGroup
}
当创建完 一个 Server
对象后,调用 Server
对象 ListenAndServe()
方法会使用网络库提供的 net.Listen
监听对应地址上的 TCP 连接并通过 net/http.Server.Serve
处理客户端的请求:
func (srv *Server) ListenAndServe() error {
//判断服务器是否正在关闭,如果是,则返回
if srv.shuttingDown() {
return ErrServerClosed
}
//获取服务器监听的地址
addr := srv.Addr
//如果服务器监听的地址为空,将其设置为 ":http"
if addr == "" {
addr = ":http"
}
//创建一个 TCP 监听器,监听指定的地址,如果创建监听器时出现错误,返回错误
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
//使用 Serve() 方法开始监听并处理连接,返回处理连接时可能出现的错误
return srv.Serve(ln)