go 进阶 http标准库相关: 五. HttpServer 接收请求路由发现原理

一. 接收请求后的路由匹配

  1. 在执行ListenAndServe()启动http服务在指定端口监听接收请求时,内部会会调用
  1. " net.Listen(“tcp”, addr)": 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
  2. “srv.Serve(ln)”: 等待接收客户端连接Accept(),与接收到连接后"go c.serve(ctx)" 启动新的goroutine来处理本次请求. 同时主goroutine继续等待客户端连接
for {
	//1.等待接收客户端请求
    rw, e := l.Accept()
    ...
    //2.接收到请求后封装conn连接
    c, err := srv.newConn(rw)
    //3.设置连接状态
    c.setState(c.rwc, StateNew)
    //4.通过协程执行(并发的体现)
    go c.serve()
}
  1. 查看"go c.serve()“内部,核心分为三个步骤,当前我们只关注"serverHandler{c.server}.ServeHTTP(w, w.req)”
for{
  //1.封装Request和response的逻辑
  w, err := c.readRequest(ctx)
  ...
  //2.匹配在http.HandleFunc()中注册的路由,找到对应的处理函数,执行我们写的业务逻辑
  serverHandler{c.server}.ServeHTTP(w, w.req)
  ...
  //3.进行最后处理工作,异常处理,资源回收,状态更新等
  w.finishRequest()
}
  1. 根据request选择handler,并且进入到这个handler的ServeHTTP,也就是路由匹配,匹配流程:
  1. 判断是否有路由能满足这个request(循环遍历ServeMux的muxEntry)
  2. 如果有路由满足,调用这个路由handler的ServeHTTP
  3. 如果没有路由满足,调用NotFoundHandler的ServeHTTP
  1. 也就是, 如果注册了带 host 的路由,则按 host + path 去寻找,如果没注册带 host 的路由,则按 path 寻找
    路由规则匹配以完全匹配优先,如果注册的路由规则最后一个字符是/,则除了完全匹配外,还会以前缀查找
  2. 其中ServeHTTP是一个接口,绝大多数Web框架都是通过实现该接口,从而替换掉Golang默认的路由
  3. 找到路由之后就直接调用我们开头注册的方法,如果我们往 Response 中写入数据,就能返回给客户端,这样一个请求就处理完成了

1. serverHandler.ServeHTTP()

  1. ServeHTTP(): 拿到DefaultServeMux
  2. 在serverHandler实现的ServeHTTP()方法里的sh.srv.Handler就是我们最初在http.ListenAndServe()中传入的Handler参数,如果该Handler对象为nil,则会使用默认的DefaultServeMux。也就是我们自定义的ServeMux对象。最后调用ServeMux的ServeHTTP()方法匹配当前路由对应的handler方法
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    // 此handler即为http.ListenAndServe 中的第二个参数
    // 获取Server对应的Handler 封装结构体的时候传入的是nil所以使用默认的DefaultServeMux
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    // 如果是 OPTIONS Method 请求且 URI 是 *,就使用 globalOptionsHandler 
    // Method == "OPTIONS" Preflighted Request(带预检的跨域请求) 
    // Preflighted Request在发送真正的请求前,会先发送一个方法为OPTIONS的预请求(Preflighted Request)
    // 用于试探服务端是否能接受真正的请求。如果options获得的回应时拒绝性质的,如404、403、500等状态,就会停止post、get请求的发出。
    // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_request_headers)
    // 测试方法 使用 net/http/serve_test.go 中的 TestOptions 函数
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    // 传的是nil 执行 DefaultServeMux.ServeHTTP() 方法
    handler.ServeHTTP(rw, req)
}

type serverHandler struct {
    srv *Server
}

2. ServeMux.ServeHTTP()

  1. ServeMux.ServeHTTP()方法很简单,就是先查找路由,然后执行对应的处理函数Handler的ServeHTTP()执行业务逻辑
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    // 如果RequestURI为 "*" 判断是不是HTTP/1.1 然后关闭长连接 响应 BadRequest
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    // 如果是一个正常的 GET POST 请求 执行ServeMux.Handler() 方法 寻找匹配的路由
    h, _ := mux.Handler(r)
    // 执行匹配到的路由的ServeHTTP方法
    h.ServeHTTP(w, r)
}

3. mux.Handler(r *Request) 获取对应的Handler

  1. mux.Handler(r *Request)该方法无论是普通GET、POST方法请求,还是代理CONNECT请求方法,都需要先对路径进行处理,例如请求路径为"/tree",处理后的路径为"/tree/"

如果在浏览器上访问 http://localhost:8080/tree 浏览器会自动给你加上"/" 向后台真正请求的路径为http://localhost:8080/tree/所以无法debug该逻辑处理

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

    // CONNECT requests are not canonicalized.
    // 对CONNECT请求的处理,CONNECT 处理代理场景
    // Method == "CONNECT" 类似于我们常使用的 POST GET ,http 1.1定义了8种方法,connect为其中之一
    if r.Method == "CONNECT" {
        // redirectToPathSlash函数主要用于自动检测是否重定向URL并修改重定向URL路径,
        //当注册的URL路径为/tree/,而请求URL路径为/tree,
        // redirectToPathSlash函数无法在mux.m中查找注册的handler,则将设请求URL设置为/tree/
        if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
            return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
        }
        return mux.handler(r.Host, r.URL.Path)
    }

    // 去掉主机名上的端口号
    host := stripHostPort(r.Host)
    // 处理URL,去掉 ".", ".."
    path := cleanPath(r.URL.Path)

    // If the given path is /tree and its handler is not registered,
    // redirect for /tree/.
    // 非代理场景重定向的处理,与"CONNECT"逻辑相同
    if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
        return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
    }

    // 如果处理后的path和请求的URL.Path不一致,如请求路径为"/tree",处理后的路径为"/tree/",执行重定向并返回URL路径
    // 重定向通过http.redirectHandler.ServeHTTP函数进行处理,如下:
    /*
       HTTP/1.1 301 Moved Permanently
       Content-Type: text/html; charset=utf-8
       Location: /tree/
       Date: Sun, 29 Nov 2020 09:15:24 GMT
       Content-Length: 41
       Connection: keep-alive
       <a href="/tree/">Moved Permanently</a>.
    */ 
    if path != r.URL.Path {
        _, pattern = mux.handler(host, path)
        url := *r.URL
        url.Path = path
        return RedirectHandler(url.String(), StatusMovedPermanently), pattern
    }

    // 在mux.m和mux.es中根据host/url.path寻找对应的handler
    return mux.handler(host, r.URL.Path)
}

mux.handler(host, path string)

  1. 处理路径之后,调用mux.handler(host, path string)从mux.m(map[string]muxEntry)和mux.es([]muxEntry)中查找对应的处理函数Handler,CONNECT方法除外
// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()

    // Host-specific pattern takes precedence over generic ones
    // 若当前 mux 中注册有带主机名的路由,就用"主机名+路由路径"去匹配
    // 也就是说带主机名的路由优先于不带的
    if mux.hosts {
        h, pattern = mux.match(host + path)
    }

    // 若没有匹配到,就直接把路由路径拿去匹配
    if h == nil {
        h, pattern = mux.match(path)
    }
    // 如果还没有匹配到,就默认返回 NotFoundHandler,该 Handler 会往 响应里写上 "404 page not found"
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    // 返回获得的 Handler 和路由路径
    return
}
  1. 需要注意,带主机名的路由优先于普通路径匹配,带主机名的路由注册如下面的方式,一般不推荐使用:
hostName, err := os.Hostname()
if err != nil {
    panic(err)
}
http.HandleFunc(hostName+"/hello", HelloServer)
  1. 然后编辑/etc/hosts文件,将主机名和ip的对应关系添加到该文件中。如主机名为wangsaichaodeMacBook-Pro,局域网的ip地址为127.0.0.1,则添加后的文件如下(其实跟我们之前自定义域名是一样的)
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1   localhost
127.0.0.1       server.cas.com
127.0.0.1       app1.cas.com
127.0.0.1       app2.cas.com
127.0.0.1       rest.cas.com
127.0.0.1       wangsaichaodeMacBook-Pro
255.255.255.255 broadcasthost
::1             localhost

mux.match(path string) 匹配

  1. 匹配源码
  1. 先在映射表mux.m(map[string]muxEntry)中进行查询,如果有路径对应的处理函数则直接返回对应的handler和pattern
  2. 如果映射表中不存在对应的处理函数,则再遍历mux.es([]muxEntry)进行查找,因为mux.es是存放所有以"/" 结尾的路由路径的切片,并且路由长的位于切片的前面(排序过的)。strings.HasPrefix(path, e.pattern)判断字符串 path 是否以 e.pattern 开头,是的话返回对应的处理器函数handler和pattern, 所以注册路由时,只会在以"/" 结尾的路由路径中才会出现需要选择最长匹配方案
// 从代码中可以看出,匹配规则过于简单,导致基本所有的go框架
// 最重要的一点就是:重写路由匹配这部分,比如gin
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // Check for exact match first.
    // 若 mux.m 中已存在该路由映射,直接返回该路由的 Handler,和路径
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // Check for longest valid match.  mux.es contains all patterns
    // that end in / sorted from longest to shortest.
    // 找到路径能最长匹配的路由。
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

获取到Handler后执行

  1. 在回到最开始,我们根据HandlerFunc格式编写业务处理器,注册路由,业务处理器对应HandlerFunc,HandlerFunc又实现了ServeHTTP,Handler是一个接口内部有ServeHTTP方法,最终业务处理器作为Handler,维护路由关系封装到了ServerMux的map容器与数组切片中, 当请求url与pattern匹配成功后获取到对应的Handler执行即可

二. 总结

  1. 在通过net/http编写服务端时, 首先调用NewServeMux()创建多路复用器,编写对外接收请求的接口函数也就是处理器,然后调用多路复用器上的HandleFunc()方法,将接口与接口路径进行绑定,注册路由, 最后调用ListenAndServe()函数在指定端口开启监听,启动服务
  2. "net/http"下的ListenAndServe()函数,在该函数中首先会封装一个Server结构体变量,调用Server的ListenAndServe()方法,该方法中
  1. " net.Listen(“tcp”, addr)": 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
  2. “srv.Serve(ln)”: 等待接收客户端连接Accept(),与接收到连接后的处理流程
  1. 查看srv.Serve(ln)内部逻辑,该方法内可以重点简化为:
  1. 方法内通过for开启了一个死循环
  2. 在循环内部,调用Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),阻塞监听客户端连接,是阻塞的(该方法内部有多路复用的相关逻辑,此处先不关注)
  3. 当有接收到连接请求后Accept()方法返回,拿到一个新的net.Conn连接实例,继续向下执行,封装net.Conn连接,设置连接状态为StateNew
  4. 通过协程执行连接的serve()方法,每一个连接开启一个goroutine来处理
  5. 然后for循环继续执行等待下一个连接
  1. 最后当接收到连接请求Accept()方法返回会拿到一个net.Conn连接,可以理解为拿到了一个基于TCP的HTTP连接,并将conn连接的状态标志为StateNew,然后开启一个goroutine执行它的serve()方法,这里对每一个连接开启一个goroutine来处理,这就是并发的体现,查看conn上的serve(),该方法内重点执行了:
  1. 首先调用newBufioReader() 封装了一个bufio.Reader
  2. 开启了一个无限for循环,循环内
  3. 调用conn的readRequest(ctx)方法读取请求的内容,比如解析HTTP请求协议,读取请求头,请求参数,封装Request和response,在解析时会读取请求头的 Content-Length,不为 0会通过TCPConn.Read() 方法读取指定长度的数据并存入请求体中,如果 Content-Length 为 0 或者没有设置,则请求体为空
  4. 封装serverHandler调用serverHandler上的ServeHTTP(w, w.req)方法进行路由匹配,找到对应的处理函数,执行我们写的业务逻辑
  5. 调用response的finishRequest()方法进行最后处理工作,当底层 bufio.Writer 缓冲区的大小达到阈值或者Flush() 被显式调用时,就会将缓冲区内的数据写入到底层连接中,并触发 Conn 的 Write() 方法将数据发送到客户端,另外finishRequest()方法还会进行一些比如异常处理,资源回收,状态更新等操作
  6. 最后调用conn的setState()设置连接状态为StateIdle,方便后续重用连接
  1. 接下来可以重点讲一下serverHandler上的ServeHTTP(w, w.req)方法,内部是如何进行路由匹配的,查看该方法内部:
  1. 在前面我们也了解到DefaultServeMux中包含一个map属性m,在调用HandleFunc()方法,将接口与接口路径进行绑定,注册路由注册时,会将接口路径为key,接口路径与接口处理器封装为muxEntry作为value保存到这个map中
  2. 在ServeHTTP()这个方法中首先会拿到DefaultServeMux多路复用器,执行多路复用器上的ServeHTTP()方法
  3. 在ServeHTTP()中会调用ServeMux下的 handler()方法—>调用ServeMux 下的 match(path string),获取用户请求的URL进行合法处理,比如拼接后缀"/“,去除特殊字符比如”.“,处理后,获取到接口路径在DefaultServeMux内部的map属性中查找到对应的接口处理器,注意会优先拼接"主机名+路由路径"进行匹配,如果匹配不到再用路径去匹配,还有一个注意点, 如果匹配不到会在mux.es([]muxEntry)进行查找,因为mux.es是存放所有以”/" 结尾的路由路径的切片
  4. 最后获取到Handler接口处理器后,我们根据HandlerFunc格式编写业务处理器,HandlerFunc又实现了ServeHTTP,Handler是一个接口内部有ServeHTTP方法,最终业务处理器作为Handler,维护路由关系封装到了ServerMux的map容器与数组切片中, 当请求url与pattern匹配成功后获取到对应的Handler执行即可
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值