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(mapstringmuxEntry类型),获取路由和对应处理器函数(我们传入的),然后就可以调用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
}
相关推荐
金銀銅鐵1 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路2 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x2 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn4 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈5 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780516 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123456 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒6 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记6 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598786 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端