引言
本文我们来看看golang原生http库的实现 , 首先来看一下golang http库的demo案例:
package main
import (
"fmt"
"net/http"
)
func HelloHandle(w http.ResponseWriter, req *http.Request) {
fmt.Println(w, "hello world")
}
func main() {
http.HandleFunc("/hello", HelloHandle)
err := http.ListenAndServe(":8080", nil)
if err != nil {
return
}
}
首先,我们调用http.HandleFunc(“/”, index)注册路径处理函数,这里将路径/的处理函数设置为index。处理函数的类型必须是:
func (http.ResponseWriter, *http.Request)
其中*http.Request
表示 HTTP 请求对象,该对象包含请求的所有信息,如 URL、首部、表单内容、请求的其他内容等。
http.ResponseWriter
是一个接口类型:
// net/http/server.go
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
用于向客户端发送响应,实现了ResponseWriter接口的类型显然也实现了io.Writer接口。所以在处理函数HelloHandle中,可以调用fmt.Fprintln()向ResponseWriter写入响应信息。
源码解析
整体流程
Golang 默认提供的http server实现,核心流程还是比较简单的,如下图所示:
本文只抓核心逻辑,细节大家后续可以慢慢研究,很多框架核心流程其实都不难理解,难的还是各种细节处理。
路由注册
下面我们深入源码,一起来看看具体实现。
首先是用来注册路由映射的HandleFunc函数:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
我们发现它直接调用了一个名为DefaultServeMux对象的HandleFunc()方法。DefaultServeMux是ServeMux类型的实例:
type ServeMux struct {
mu sync.RWMutex // 互斥锁
m map[string]muxEntry // 请求映射集合
es []muxEntry // 有序数组,用于实现最长前缀匹配
hosts bool // whether any patterns contain hostnames
}
// 请求映射条目
type muxEntry struct {
h Handler // 处理器
pattern string // 请求路径
}
// http请求处理器
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// 默认情况下提供的多路复用处理器
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
像这种提供默认类型实例的用法在 Go 语言的各个库中非常常见,在默认参数就已经足够的场景中使用默认实现很方便
。
ServeMux保存了注册的所有路径和处理函数的对应关系。ServeMux.HandleFunc()方法如下:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
这里将处理函数handler转为HandlerFunc类型,然后调用ServeMux.Handle()方法注册。注意这里的HandlerFunc(handler)是类型转换,而非函数调用,类型HandlerFunc的定义如下
:
// 此处的作用是适配器,用于将用户传入的函数适配为实现了Handler接口的实现类
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
HandlerFunc实际上是以函数类型func(ResponseWriter, *Request)
为底层类型,为HandlerFunc类型定义了方法ServeHTTP。是的,Go 语言允许为(基于)函数的类型定义方法。
下面会调用请求多路复用器的Handle方法,完成路由注册,也就是建立请求路径及其处理器的映射关系:
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
...
// 判断是否存在重复映射问题
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
// 初始化请求映射集合
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
// 建立映射条目
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
}
}
服务启动
通过调用ListenAndServe函数,完成端口绑定与监听,以及开启循环接收客户端连接,并查询映射集合,获取对应的处理器来处理请求:
func ListenAndServe(addr string, handler Handler) error {
// handler不传则为nil,后续会采用默认的请求多路复用器
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
下面调用server对象的ListenAndServe完成真正的工作:
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
// 完成端口监听与绑定工作
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
// 开使接收并处理客户端连接
return srv.Serve(ln)
}
server的serve方法中会不断处理客户端连接及后续读写事件:
func (srv *Server) Serve(l net.Listener) error {
...
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
// 同步阻塞接收客户端链接
rw, err := l.Accept()
...
// 此处返回的是net/http包下的conn结构体实现
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
// 每个连接交给单独的协程进行处理
go c.serve(connCtx)
}
}
请求处理
上文讲到,go http库会为每个客户端连接单独分配一个协程,用于处理该连接上后续的数据读写请求,具体实现在net/http包下conn结构体的serve函数中:
func (c *conn) serve(ctx context.Context) {
...
// HTTP/1.x from here on.
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 {
// 按照http协议格式解析tcp报文为http request对象,这里的conn是net/http包下提供的
w, err := c.readRequest(ctx)
...
// 调用serverHandler的ServeHttp方法处理请求
serverHandler{c.server}.ServeHTTP(w, w.req)
inFlightResponse = nil
w.cancelCtx()
// 如果http请求被劫持了,此处直接返回
if c.hijacked() {
return
}
...
}
}
serverHandler对server对象进行了一层包装,主要是处理handler为nil时,为handler设置默认的请求多路复用器:
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
...
// 将请求处理转发给handler来处理
handler.ServeHTTP(rw, req)
}
此处我们看一下默认的请求多路复用器的ServeHttp函数实现逻辑:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// ServeMux实现了ServeHttp接口,因此也实现了Handler接口
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
...
// 查询请求映射集合,寻找当前请求对应的处理器
h, _ := mux.Handler(r)
// 调用HandlerFunc的ServerHttp方法处理请求
h.ServeHTTP(w, r)
}
路由匹配
ServeMux的Handler方法中,包含了路由匹配到核心逻辑,如下所示:
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
// CONNECT requests are not canonicalized.
// 单独处理CONNECT请求
if r.Method == "CONNECT" {
// redirectToPathSlash函数处理逻辑就是:
// 0. 假设当前请求路径URI为/tree
// 1. 如果ServeMux的请求映射集合m中包含/tree -- 精确匹配 , 那么不需要重定向
// 2. 如果不包含/tree,但是包含/tree/ , 则创建一个新的URI , 路径为/tree/
if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
// 如果需要重定向,则将/tree请求永久重定向到/tree/
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
// 如果不需要重定向,则调用请求复用器的handler方法匹配具体的Handler实现
return mux.handler(r.Host, r.URL.Path)
}
// All other requests have any port stripped and path cleaned
// before passing to mux.handler.
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
// If the given path is /tree and its handler is not registered,
// redirect for /tree/.
// 此处逻辑和上面一样
if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
if path != r.URL.Path {
// 返回匹配得到的pattern路径
_, pattern = mux.handler(host, path)
// 将当前请求r.URL.RawQuery重定向到到此路径path
u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}
return RedirectHandler(u.String(), StatusMovedPermanently), pattern
}
// 如果不需要重定向,则调用请求复用器的handler方法匹配具体的Handler实现
return mux.handler(host, r.URL.Path)
}
ServeMux的handler方法中先执行请求路径的精确匹配,再执行请求路径的最长前缀匹配:
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// host+path去匹配
if mux.hosts {
h, pattern = mux.match(host + path)
}
// 只带path去匹配
if h == nil {
h, pattern = mux.match(path)
}
// 没有找到handler
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
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
}
}
// 如果都没匹配上,则返回nil
return nil, ""
}
服务隔离
调用http.HandleFunc()/http.Handle()都是将处理器/函数注册到ServeMux的默认对象DefaultServeMux上。使用默认对象有一个问题:不可控
。
一来Server参数都使用了默认值,二来第三方库也可能使用这个默认对象注册一些处理,容易冲突。更严重的是,我们在不知情中调用http.ListenAndServe()
开启 Web 服务,那么第三方库注册的处理逻辑就可以通过网络访问到,有极大的安全隐患。所以,除非在示例程序中,否则建议不要使用默认对象。
我们可以使用http.NewServeMux()
创建一个新的ServeMux
对象,然后创建http.Server
对象定制参数,用ServeMux
对象初始化Server
的Handler
字段,最后调用Server.ListenAndServe()
方法开启 Web
服务:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", index)
mux.Handle("/greeting", greeting("Welcome to go web frameworks"))
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 20 * time.Second,
WriteTimeout: 20 * time.Second,
}
server.ListenAndServe()
}
这个程序与上面的Hello World功能基本相同,我们还额外设置了读写超时。
为了便于理解,我画了两幅图,其实整理下来整个流程也不复杂:
中间件
有时候需要在请求处理代码中增加一些通用的逻辑,如统计处理耗时、记录日志、捕获宕机等等。如果在每个请求处理函数中添加这些逻辑,代码很快就会变得不可维护,添加新的处理函数也会变得非常繁琐。所以就有了中间件的需求。
中间件有点像面向切面的编程思想,但是与 Java 语言不同。在 Java 中,通用的处理逻辑(也可以称为切面)可以通过反射插入到正常逻辑的处理流程中,在 Go 语言中基本不这样做。
在 Go 中,中间件是通过函数闭包来实现的。Go 语言中的函数是第一类值,既可以作为参数传给其他函数,也可以作为返回值从其他函数返回。我们前面介绍了处理器/函数的使用和实现。那么可以利用闭包封装已有的处理函数。
首先,基于函数类型func(http.Handler) http.Handler
定义一个中间件类型:
type Middleware func(http.Handler) http.Handler
接下来我们来编写中间件,最简单的中间件就是在请求前后各输出一条日志:
// golang里面典型的装饰器模式实现
func WithLogger(handler http.Handler) http.Handler {
// 此处是将匿名函数转换为了HandlerFunc类型,而不是函数嵌套
// HandlerFunc类型本身也实现了Handler接口,不要忘记
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger.Printf("path:%s process start...\n", r.URL.Path)
defer func() {
logger.Printf("path:%s process end...\n", r.URL.Path)
}()
// 调用目标handler的ServeHttp方法处理
handler.ServeHTTP(w, r)
})
}
实现很简单,通过中间件封装原来的处理器对象,然后返回一个新的处理函数。在新的处理函数中,先输出开始处理的日志,然后用defer语句在函数结束后输出处理结束的日志。接着调用原处理器对象的ServeHTTP()方法执行原处理逻辑。
类似地,我们再来实现一个统计处理耗时的中间件:
func Metric(handler http.Handler) http.HandlerFunc {
return func (w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
logger.Printf("path:%s elapsed:%fs\n", r.URL.Path, time.Since(start).Seconds())
}()
time.Sleep(1 * time.Second)
handler.ServeHTTP(w, r)
}
}
Metric中间件封装原处理器对象,开始执行前记录时间,执行完成后输出耗时。为了能方便看到结果,我在上面代码中添加了一个time.Sleep()调用。
最后,由于请求的处理逻辑都是由功能开发人员(而非库作者)自己编写的,所以为了 Web 服务器的稳定,我们需要捕获可能出现的 panic。PanicRecover中间件如下:
func PanicRecover(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
logger.Println(string(debug.Stack()))
}
}()
handler.ServeHTTP(w, r)
})
}
调用recover()函数捕获 panic,输出堆栈信息,为了防止程序异常退出。实际上,在conn.serve()方法中也有recover(),程序一般不会异常退出。但是自定义的中间件可以添加我们自己的定制逻辑。
现在我们可以这样来注册处理函数:
mux.Handle("/", PanicRecover(WithLogger(Metric(http.HandlerFunc(index)))))
mux.Handle("/greeting", PanicRecover(WithLogger(Metric(greeting("welcome, dj")))))
这种方式略显繁琐,我们可以编写一个帮助函数,它接受原始的处理器对象,和可变的多个中间件。对处理器对象应用这些中间件,返回新的处理器对象:
func applyMiddlewares(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares)-1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
注意应用顺序是从右到左的,即右结合,越靠近原处理器的越晚执行。
利用帮助函数,注册可以简化为:
middlewares := []Middleware{
PanicRecover,
WithLogger,
Metric,
}
mux.Handle("/", applyMiddlewares(http.HandlerFunc(index), middlewares...))
mux.Handle("/greeting", applyMiddlewares(greeting("welcome, dj"), middlewares...))
上面每次注册处理逻辑都需要调用一次applyMiddlewares()函数,还是略显繁琐。我们可以这样来优化,封装一个自己的ServeMux结构,然后定义一个方法Use()将中间件保存下来,重写Handle/HandleFunc将传入的http.HandlerFunc/http.Handler处理器包装中间件之后再传给底层的ServeMux.Handle()方法:
type MyMux struct {
*http.ServeMux
middlewares []Middleware
}
func NewMyMux() *MyMux {
return &MyMux{
ServeMux: http.NewServeMux(),
}
}
func (m *MyMux) Use(middlewares ...Middleware) {
m.middlewares = append(m.middlewares, middlewares...)
}
func (m *MyMux) Handle(pattern string, handler http.Handler) {
handler = applyMiddlewares(handler, m.middlewares...)
m.ServeMux.Handle(pattern, handler)
}
func (m *MyMux) HandleFunc(pattern string, handler http.HandlerFunc) {
newHandler := applyMiddlewares(handler, m.middlewares...)
m.ServeMux.Handle(pattern, newHandler)
}
注册时只需要创建MyMux对象,调用其Use()方法传入要应用的中间件即可:
middlewares := []Middleware{
PanicRecover,
WithLogger,
Metric,
}
mux := NewMyMux()
mux.Use(middlewares...)
mux.HandleFunc("/", index)
mux.Handle("/greeting", greeting("welcome, dj"))
这种方式简单易用,但是也有它的问题,最大的问题是必须先设置好中间件,然后才能调用Handle/HandleFunc注册,后添加的中间件不会对之前注册的处理器/函数生效。
为了解决这个问题,我们可以改写ServeHTTP方法,在确定了处理器之后再应用中间件。这样后续添加的中间件也能生效
。很多第三方库都是采用这种方式。http.ServeMux默认的ServeHTTP()方法如下:
func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(http.StatusBadRequest)
return
}
h, _ := m.Handler(r)
h.ServeHTTP(w, r)
}
改造这个方法定义MyMux类型的ServeHTTP()方法也很简单,只需要在m.Handler®获取处理器之后,应用当前的中间件即可:
func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// ...
h, _ := m.Handler(r)
// 只需要加这一行即可
h = applyMiddlewares(h, m.middlewares...)
h.ServeHTTP(w, r)
}
后面我们分析其他 Web 框架的源码时会发现,很多都是类似的做法。为了测试宕机恢复,编写一个会触发 panic 的处理函数:
func panics(w http.ResponseWriter, r *http.Request) {
panic("not implemented")
}
mux.HandleFunc("/panic", panics)
运行,在浏览器中请求localhost:8080/和localhost:8080/greeting
,最后请求localhost:8080/panic
触发 panic:
参考
本文转载至: Go 每日一库之 net/http(基础和中间件) , 并在其基础上进行了优化,方便读者阅读和理解