Go HTTP 源码分析
原文首发: youngxhui.top
前言
在 go 中,对于 http 服务可以做到开箱即用,无需第三方框架,而且使用起来也很简单。但是为什么还会有很多 http 框架的诞生,例如 gin,echo 等,说明自带的 http 服务还有不完美的地方,导致了用户选择第三方开发的框架。
这是一个采用标准库开发的 http 服务。
package main import "net/http" func main() { http.HandleFunc("/ping", PingHandler) http.ListenAndServe(":8000", nil) } func PingHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) }
这是一个很简单的 http 服务,当启动服务的时候,通过访问 http://localhost:8000/ping 就可以访问到相关的 Handler,并返回响应信息:pong。
这个代码很简单,其中关于 http 的一共出现了两个函数。分别为:http.HandleFunc
、http.ListenAndServe
。下面就对这两个函数进行解析,让你知道每一步都做了什么。
HandleFunc
首先是 HandleFunc
,它的作用基本就是将 handle
注册到 http 框架中,让对应的 url 和 handler 一一对应,这样 url 和 Handler 就会映射起来。
通过 HandlerFunc()
的源码来看一下相关的处理方式。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }
HandlerFunc
比较简单,可以看到将路由注册到 DefaultServeMux
这个对象上。通过不断的往下看源码,可以找到 ServeMux.Handler
这个方法上。这个方法主要是将服务中的路由进行注册。将服务注册到 ServerMux 对象上,也就是上文所提到的 DefaultServeMux
。
这个对象的构造也比较简单。
type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry hosts bool } type muxEntry struct { h Handler pattern string }
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") } 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 } }
如果 ServerMux.m 为空,会进行初始化,之后将 handler
和 pattern
存放到 muxEntry
中,最后将 muxEntry
存放到 m
中,m
的 key 是 pattern
。这里的 pattern 就是 url 路径。
在 ServeMux 中,mux.es 切片是用来保存以斜杠结尾的路由模式对应的 muxEntry 对象的。它的作用是在请求的 URL 中去掉末尾的斜杠后进行匹配,从而避免重复处理类似 /path 和 /path/ 这样的 URL。
例如,如果有两个路由模式分别为 /path 和 /path/,请求的 URL 为 /path/,如果没有 mux.es 切片,将会尝试匹配 /path 和 /path/ 两个路由模式,最终会选择匹配 /path/ 的路由模式进行处理。这会导致处理器被重复调用。而使用 mux.es 切片,请求的 URL 会被处理为 /path,只会匹配到 /path 这一个路由模式,避免了处理器被重复调用的问题。
因此,mux.es 切片的作用是为了提高 ServeMux 的匹配效率,避免重复处理请求。
所有的 url 和 Handler 的映射关系都是通过 map[string]muxEntry
进行保存。这样就会出现问题,稍微复杂一些的 url 就无法很好的匹配。这也就是为什么会有大量的 go web 框架,而这些框架都是改写路由的匹配算法。
上面就是一个主要的注册过程。
ListenAndServe
这个方法主要对端口进行监听。
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }
从源码可以看到,这里需要一个 handler 参数,并且新生成一个 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) }
在 ListenAndServe 中,首先对服务的状态进行了判断,如果是 shuttingDown 就提示 http: Server closed
。这里的 shuttingDown
主要是通过一个叫 atomicBool
进行判断的。咋一看以为是原子操作,仔细看其实是定义了一个 int32 类型,通过 int32 的原子操作保证了并发安全。
type atomicBool int32 func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 } func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) } func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) }
之后通过 net.Listen() 方法进行监听,这里对这个方法不做过多的赘述,之后通过 Serve 方法。 下面是主要的核心方法
ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, err := l.Accept() if err != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } return err } connCtx := ctx if cc := srv.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew, runHooks) // before Serve can return go c.serve(connCtx) }
l
为 net.Listener 对象,当每次接收到信息的时候,首先会进行一个错误判断。 如果是 down 的信号,就会直接返回相关错误,否则先对错误进行断言,检查是否为 net.Error
,这个是一个接口,其中 Temporary
方法官方已经标记为启用,这个方法更多的表示为超时。如果有超时,你们就会对延时 tempDelay
,进行增加,起初是 5 毫秒,之后每次增加 2 倍,最大为 1 秒钟,之后会进行重试。
通过 srv.ConnContext 会生成一个新的 ctx,否则就使用之前的 ctx,也就是 context.Backgroud()
。然后开启一个协程进行服务。
在协程中,通过 readRequest 方法进行获取,返回 reponse。通过 response 对象获取 request。通过 request 判断请求是否要继续。
req := w.req if req.expectsContinue() { if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 { req.Body = &expectContinueReader{readCloser: req.Body, resp: w} w.canWriteContinue.Store(true) } } else if req.Header.get("Expect") != "" { w.sendExpectationFailed() return }
这里判断首先通过 expectsContinue
方法,这个方法中获取请求头中的 Expect
字段是否等于 100-continue
。当等于的时候要继续进行判断,其中请求的协议为 HTTP 1.1 和 ContentLength 不为 0。这样就可以获取到请求体。
当请求头中的 Expect
和上述条件不相同的时候,直接返回 417 错误。
之后创建了一个 serverHandler
并且调用了 ServeHTTP
并且传入了 response 和 request。
ServeHTTP 再一次出现,其中第一步就是获取 Handler。
handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux }
那么,这里获取的 handler 应该是什么呢?经过多个方法或函数,可以已经对 sh.srv.Handler
一步一步的向上推到。这里我将这个过程画了一张图,图上箭头表示关系之间的依赖,红色表示持有 handler 数据。
通过这个依赖图可以看到,handler 是由最开始的 ListenAndServe
方法进行传入的,而我们的示例代码中这部分传入的是 nil
,也就是从开始到现在 handler 一直为 nil。这也就是为什么会一个判断,当 handler 为空的时候使用 DefaultServeMux
。其实关于默认的 handler 为 DefaultServeMux
这个事情,在 ListenAndServe
这个代码的注释中就已经说明。
之后就是调用 handler.ServeHTTP
方法。那么这里的 ServeHTTP
方法就和上文中提到的 ServeHTTP
方法是一个了。
总结
通过两个方法基本可以做到路由的注册方式和路由的查询方式,并且对请求来临的时候相关的处理过程。这些方法对之后研究其他框架源码或者工作方式更加清晰。
本文来自博客园,作者:youngxhui,转载请注明原文链接:https://www.cnblogs.com/youngxhui/p/17730468.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通