原文地址,转载请注明出处: https://blog.csdn.net/qq_34021712/article/details/110351336 ©王赛超
目录
前面两章学习了 注册路由的http.HandleFunc()函数
和 监听端口以及请求的处理 http.ListenAndServe()
。
第一章:路由注册 http.HandleFunc()
第二章:监听端口 处理请求 http.ListenAndServe()
经过第二章之后,已经了解了一次http请求的处理流程,但是第二章中还有一小部分没讲,就是 serverHandler{c.server}.ServeHTTP(w, w.req)
以及内部代码还没看,这块主要就是请求过来如何根据路由找到我们写的处理函数,执行处理函数。
还把那个最简单的代码拿过来,如下:
一. 搭建简单的web服务器
对于Go
,实现一个最简单的http server
用不着几行代码,如下:
func main() {
http.HandleFunc("/", HelloServer)
_ = http.ListenAndServe(":8080", nil)
}
func HelloServer(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(`hello world`))
if err != nil {
fmt.Println(err)
}
}
http.HandleFunc("/", HelloServer)
这个函数就是注册路由,细节我们已经在第一章讲过了。
http.ListenAndServe(":8080", nil)
这个函数监听8080
端口,接收请求,根据路由找到处理函数处理请求。
不多废话了,不清楚的可以先看一下前2章的内容。
二.分析一下serverHandler{c.server}.ServeHTTP(w, w.req)
serverHandler.ServeHTTP()
// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
type serverHandler struct {
srv *Server
}
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)
}
在serverHandler
实现的ServeHTTP()
方法里的sh.srv.Handler
就是我们最初在http.ListenAndServe()
中传入的Handler
参数,如果该Handler
对象为nil
,则会使用默认的DefaultServeMux
。也就是我们自定义的ServeMux
对象。最后调用ServeMux
的ServeHTTP()
方法匹配当前路由对应的handler
方法。
ServeMux.ServeHTTP()
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
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)
}
ServeMux.ServeHTTP()
方法很简单,就是先查找路由,然后执行对应的处理函数Handler
的ServeHTTP()
执行业务逻辑。
mux.Handler(r *Request)
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" {
// If r.URL.Path is /tree and its handler is not registered,
// the /tree -> /tree/ redirect applies to CONNECT requests
// but the path canonicalization does not.
// 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)
}
// All other requests have any port stripped and path cleaned
// before passing to mux.handler.
// 去掉主机名上的端口号
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(r *Request)
该方法无论是普通GET
、POST
方法请求,还是代理CONNECT
请求方法,都需要先对路径进行处理,例如请求路径为"/tree"
,处理后的路径为"/tree/"
。
注意:如果在浏览器上访问 http://localhost:8080/tree
浏览器会自动给你加上"/"
向后台真正请求的路径为http://localhost:8080/tree/
所以无法debug
该逻辑处理。
建议使用 postman
测试 ,测试日志如下: 如果访问 /tree
服务端会返回 301
需要重定向到 /tree/
服务器返回重定向的报文如下:
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>.
postman
控制台记录:
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)
我们再看看 match
方法是怎么进行匹配的:
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
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, ""
}
注意
:
这里是先在映射表mux.m(map[string]muxEntry)
中进行查询,如果正好有路径对应的处理函数则直接返回对应的handler
和pattern
。
如果映射表中不存在对应的处理函数,则再遍历mux.es([]muxEntry)
进行查找,在第一章(Go使用net/http标准库(一)源码学习之- http.HandleFunc())中我们知道mux.es
是存放所有以"/"
结尾的路由路径的切片,并且路由长的位于切片的前面(排序过的)。strings.HasPrefix(path, e.pattern)
判断字符串 path
是否以 e.pattern
开头,是的话返回对应的处理器函数handler
和pattern
, 所以注册路由时,只会在以"/"
结尾的路由路径中才会出现需要选择最长匹配方案。
下面用注册路由举例说明:
比如注册的路由有:
mux.HandleFunc("/a/b/", ab)
mux.HandleFunc("/a/", a)
那么当一个请求的URL
为/a/b/c
的时候,就是由 ab
来处理这个请求。因为 mux.es
中元素是按照它们的长度由大到小顺序存放的。
handler.ServeHTTP(w ResponseWriter, r *Request)
再回到 mux.ServeHTTP(w ResponseWriter, r *Request)
方法中,获取到handler
之后,又调用了handler.ServeHTTP(w ResponseWriter, r *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
在第一章(Go使用net/http标准库(一)源码学习之- http.HandleFunc())中,我们知道HandlerFunc
类型是一个适配器,并且这种类型实现了ServeHTTP
方法,并在ServeHTTP
方法中又调用了被转换的函数自身,也就是说这个类型的函数其实就是一个Handler
类型的对象,通过类型转换可以将一个具有func(ResponseWriter, *Request)
签名的普通函数转换为一个Handler
对象,而不需要再定义一个结构体,再让这个结构实现ServeHTTP
方法,非常方便的将普通函数
用作HTTP处理程序
。
最终调用到了我们写的 handler
处理函数。逻辑处理完成之后,后续的一些写响应,复用连接等相关操作在上一章(Go使用net/http标准库(二)源码学习之- http.ListenAndServe()) 中也说过了。
到此,go http
源码学习到此结束。