目录
一. 接收请求后的路由匹配
- 在执行ListenAndServe()启动http服务在指定端口监听接收请求时,内部会会调用
- " net.Listen(“tcp”, addr)": 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
- “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()
}
- 查看"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()
}
- 根据request选择handler,并且进入到这个handler的ServeHTTP,也就是路由匹配,匹配流程:
- 判断是否有路由能满足这个request(循环遍历ServeMux的muxEntry)
- 如果有路由满足,调用这个路由handler的ServeHTTP
- 如果没有路由满足,调用NotFoundHandler的ServeHTTP
- 也就是, 如果注册了带 host 的路由,则按 host + path 去寻找,如果没注册带 host 的路由,则按 path 寻找
路由规则匹配以完全匹配优先,如果注册的路由规则最后一个字符是/,则除了完全匹配外,还会以前缀查找 - 其中ServeHTTP是一个接口,绝大多数Web框架都是通过实现该接口,从而替换掉Golang默认的路由
- 找到路由之后就直接调用我们开头注册的方法,如果我们往 Response 中写入数据,就能返回给客户端,这样一个请求就处理完成了
1. serverHandler.ServeHTTP()
- ServeHTTP(): 拿到DefaultServeMux
- 在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()
- 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
- 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)
- 处理路径之后,调用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
}
- 需要注意,带主机名的路由优先于普通路径匹配,带主机名的路由注册如下面的方式,一般不推荐使用:
hostName, err := os.Hostname()
if err != nil {
panic(err)
}
http.HandleFunc(hostName+"/hello", HelloServer)
- 然后编辑/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) 匹配
- 匹配源码
- 先在映射表mux.m(map[string]muxEntry)中进行查询,如果有路径对应的处理函数则直接返回对应的handler和pattern
- 如果映射表中不存在对应的处理函数,则再遍历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后执行
- 在回到最开始,我们根据HandlerFunc格式编写业务处理器,注册路由,业务处理器对应HandlerFunc,HandlerFunc又实现了ServeHTTP,Handler是一个接口内部有ServeHTTP方法,最终业务处理器作为Handler,维护路由关系封装到了ServerMux的map容器与数组切片中, 当请求url与pattern匹配成功后获取到对应的Handler执行即可
二. 总结
- 在通过net/http编写服务端时, 首先调用NewServeMux()创建多路复用器,编写对外接收请求的接口函数也就是处理器,然后调用多路复用器上的HandleFunc()方法,将接口与接口路径进行绑定,注册路由, 最后调用ListenAndServe()函数在指定端口开启监听,启动服务
- "net/http"下的ListenAndServe()函数,在该函数中首先会封装一个Server结构体变量,调用Server的ListenAndServe()方法,该方法中
- " net.Listen(“tcp”, addr)": 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
- “srv.Serve(ln)”: 等待接收客户端连接Accept(),与接收到连接后的处理流程
- 查看srv.Serve(ln)内部逻辑,该方法内可以重点简化为:
- 方法内通过for开启了一个死循环
- 在循环内部,调用Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),阻塞监听客户端连接,是阻塞的(该方法内部有多路复用的相关逻辑,此处先不关注)
- 当有接收到连接请求后Accept()方法返回,拿到一个新的net.Conn连接实例,继续向下执行,封装net.Conn连接,设置连接状态为StateNew
- 通过协程执行连接的serve()方法,每一个连接开启一个goroutine来处理
- 然后for循环继续执行等待下一个连接
- 最后当接收到连接请求Accept()方法返回会拿到一个net.Conn连接,可以理解为拿到了一个基于TCP的HTTP连接,并将conn连接的状态标志为StateNew,然后开启一个goroutine执行它的serve()方法,这里对每一个连接开启一个goroutine来处理,这就是并发的体现,查看conn上的serve(),该方法内重点执行了:
- 首先调用newBufioReader() 封装了一个bufio.Reader
- 开启了一个无限for循环,循环内
- 调用conn的readRequest(ctx)方法读取请求的内容,比如解析HTTP请求协议,读取请求头,请求参数,封装Request和response,在解析时会读取请求头的 Content-Length,不为 0会通过TCPConn.Read() 方法读取指定长度的数据并存入请求体中,如果 Content-Length 为 0 或者没有设置,则请求体为空
- 封装serverHandler调用serverHandler上的ServeHTTP(w, w.req)方法进行路由匹配,找到对应的处理函数,执行我们写的业务逻辑
- 调用response的finishRequest()方法进行最后处理工作,当底层 bufio.Writer 缓冲区的大小达到阈值或者Flush() 被显式调用时,就会将缓冲区内的数据写入到底层连接中,并触发 Conn 的 Write() 方法将数据发送到客户端,另外finishRequest()方法还会进行一些比如异常处理,资源回收,状态更新等操作
- 最后调用conn的setState()设置连接状态为StateIdle,方便后续重用连接
- 接下来可以重点讲一下serverHandler上的ServeHTTP(w, w.req)方法,内部是如何进行路由匹配的,查看该方法内部:
- 在前面我们也了解到DefaultServeMux中包含一个map属性m,在调用HandleFunc()方法,将接口与接口路径进行绑定,注册路由注册时,会将接口路径为key,接口路径与接口处理器封装为muxEntry作为value保存到这个map中
- 在ServeHTTP()这个方法中首先会拿到DefaultServeMux多路复用器,执行多路复用器上的ServeHTTP()方法
- 在ServeHTTP()中会调用ServeMux下的 handler()方法—>调用ServeMux 下的 match(path string),获取用户请求的URL进行合法处理,比如拼接后缀"/“,去除特殊字符比如”.“,处理后,获取到接口路径在DefaultServeMux内部的map属性中查找到对应的接口处理器,注意会优先拼接"主机名+路由路径"进行匹配,如果匹配不到再用路径去匹配,还有一个注意点, 如果匹配不到会在mux.es([]muxEntry)进行查找,因为mux.es是存放所有以”/" 结尾的路由路径的切片
- 最后获取到Handler接口处理器后,我们根据HandlerFunc格式编写业务处理器,HandlerFunc又实现了ServeHTTP,Handler是一个接口内部有ServeHTTP方法,最终业务处理器作为Handler,维护路由关系封装到了ServerMux的map容器与数组切片中, 当请求url与pattern匹配成功后获取到对应的Handler执行即可