golang 浅析net/http 路由注册与请求处理

注册路由

初学web时,我们常用http.HandleFunc()来注册路由,然后用http.ListenAndServe()来启动服务,接下来让我们分析一下http包内部是如何注册路由的。

除了常用的http.HandleFunc()可以注册路由,还有http.Handle可以注册,先看一下源码。

golang 复制代码
func Handle(pattern string, handler Handler) { 
    DefaultServeMux.Handle(pattern, handler) 
}

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

对比一下两个函数的不同:1、分别调用了DefaultServeMux的Handle和HandleFunc方法。2、handler参数类型分别为http.Handler接口,和func(ResponseWriter, *Request)类型。说明一下,DefaultServerMux是http包的全局变量,如果不使用默认的复用器

接下来看一下Handle和HandleFunc的主要源码,和Handler接口

golang 复制代码
// 注册路由
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()
    ...
    
    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    e := muxEntry{h: handler, pattern: pattern}
    mux.m[pattern] = e
	
    ...
}

// 调用Handle注册路由
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

// 实现了Handler的函数类型
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

可以看到ServeMux.Handle最终实现了路由的注册,mux.m记录了路由与处理器的映射;而ServeMux.HandleFunc将handler参数转换成了HandlerFunc类型,然后调用ServeMux.Handle。

那么问题来了,ServeMux.Handle的handler参数是Handler接口类型,我们调用http.HandleFunc()传入的处理器函数签名是func(ResponseWriter, *Request),我们传入的函数咋就实现了Handler接口?

答案就在于HandlerFunc类型,它实现了Handler接口。我们传入的处理器函数与HandlerFunc类型函数签名是一致的,如果没有HandlerFunc,要注册函数的话,我们就要自己定义结构体,写ServeHTTP方法,实现Handler接口,而有了HandlerFunc我们就可以把这一步省去了,在设计模式中,这叫装饰器模式。

处理请求

ServerMux

使用http.HandleFunc和http.Handle注册的路由都注册到了DefaultServerMux,它也实现了handler接口,那让我们来看一下ServerMux的ServeHTTP方法。

golang 复制代码
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

mux.Handler()中会调用mux.match(),从ServeMux.m(map[string]muxEntry类型),获取路由和对应处理器函数(我们传入的),然后就可以调用h.ServeHTTP(w,r)来处理对应的请求。

现在已得知我们传入的处理函数是被ServeMux.ServeHTTP()调用,那ServerMus.ServeHTTP()又是怎么被调用的呢?接下来跟踪一下http.ListenAndServe()的源码。

Server

golang 复制代码
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

用http.ListenAndServe()启动服务的话,handler参数一般传nil,使用DefaultServerMux做处理器,下面的分析都以此为前提。

在函数内部帮我们创建了一个Server结构体,如果想更灵活地使用,可以自己创建Server结构体,调用server.ListenAndServe()。

golang 复制代码
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.ListenAndServe()中完成了监听地址的绑定,然后再调用Server.Serve()

golang 复制代码
func (srv *Server) Serve(l net.Listener) error {
    
	...
    
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, err := l.Accept()

        ...
            
        connCtx := ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew, runHooks)
        go c.serve(connCtx)
    }
}

Server.Serve中开启了一个for循环来接收连接,并为每一个连接创建contexxt,开一个协程继续处理。

golang 复制代码
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    var inFlightResponse *response
    defer func() {
        if err := recover(); err != nil && err != ErrAbortHandler {
            const size = 64 << 10
            buf := make([]byte, size)
            buf = buf[:runtime.Stack(buf, false)]
            c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
        }
        if inFlightResponse != nil {
            inFlightResponse.cancelCtx()
        }
        if !c.hijacked() {
            if inFlightResponse != nil {
                inFlightResponse.conn.r.abortPendingRead()
                inFlightResponse.reqBody.Close()
            }
            c.close()
            c.setState(c.rwc, StateClosed, runHooks)
        }
    }()

	...
    
    for {
        w, err := c.readRequest(ctx)

        ...

        inFlightResponse = w
        serverHandler{c.server}.ServeHTTP(w, w.req)  // 这里并不是Handler接口的ServeHTTP方法
        inFlightResponse = nil
        w.cancelCtx()
        if c.hijacked() {
            return
        }
        
      	...
    }

    ...
}

在http包server.go 1991行,嵌套了Server结构体的serverHandler结构体调用ServeHTTP方法,需要注意的是这并不是Handler接口的ServeHTTP方法,我们传入的处理器函数的调用还在其内部。

golang 复制代码
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux	// 如果在创建Server时,Handler参数为nil,就使用DefaultServeMux
                                                                    //  通过http.ListenAndServe开启服务一般都用DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }

    if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
        var allowQuerySemicolonsInUse int32
        req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
            atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
        }))
        defer func() {
            if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
                sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")
            }
        }()
    }

    handler.ServeHTTP(rw, req)	// 调用DefaultServeMux的ServeHTTP
}

最后一行调用DefaultServeMux的ServeHTTP,上文已介绍过,它的作用就是获取到请求对应的处理器函数并执行。

延伸

gin框架是基于http包封装的,gin匹配路由的方式不同于原生包,使用前缀路由树来匹配路由,通过engin.Run启动服务,其内部也是调用的http.ListenAndServe(),那gin是如何应用的自定义匹配方式呢?其实很简单,上文提到,调用http.ListenAndServe()时第二个参数handler是nil的话,会使用DefaultServeMux当做复用器,那engin.Run()中调用时传入gin定义的复用器就好了。

golang 复制代码
func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    if engine.isUnsafeTrustedProxies() {
        debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
            "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
    }

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine.Handler())	// engin.Handler()返回gin的自定义处理器
    return
}
相关推荐
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk5 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*5 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue5 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man5 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer087 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml48 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠9 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#