轉載請注明出處
今天我們用go來搭建一個文件服務器FileServer,並且我們簡單分析一下,它究竟是如何工作的。知其然,並知其所以然!
首先搭建一個最簡單的,資源就掛載在服務器的根目錄下,並且路由路徑為根路徑:127.0.0.1:8080/
http.Handle("/", http.FileServer(http.Dir("sourse")))
err := http.ListenAndServe(":8080", nil) if err != nil { fmt.Println(err) }
服務器程序和資源結構如下:
打開源碼,我們定位到net/http/fs.go文件中,看看http.FileServer是如何定義的
func FileServer(root FileSystem) Handler { return &fileHandler{root} }
原來FileServer函數是返回一個Handler,接下來我們再看看fileHandler是怎么定義的
type fileHandler struct { root FileSystem }
原來是個結構體,既然是個Handler,那么它一定實現了ServeHttp函數,找找看
func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) { upath := r.URL.Path if !strings.HasPrefix(upath, "/") { upath = "/" + upath r.URL.Path = upath } serveFile(w, r, f.root, path.Clean(upath), true) //看來關鍵在這里 }
進入到關鍵函數serveFile看看,它的函數聲明如下:
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) //最后一個參數表示是否重新定向,在web服務中,它總是true
這里最后一個參數很重要,我們下面會揭示為什么,好啦,看看源碼,無關部分我都砍掉:
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) { const indexPage = "/index.html" // redirect .../index.html to .../ // can't use Redirect() because that would make the path absolute, // which would be a problem running under StripPrefix if strings.HasSuffix(r.URL.Path, indexPage) { localRedirect(w, r, "./") return } f, err := fs.Open(name) if err != nil { msg, code := toHTTPError(err) Error(w, msg, code) return } defer f.Close() d, err := f.Stat() if err != nil { msg, code := toHTTPError(err) Error(w, msg, code) return } if redirect { // redirect to canonical path: / at end of directory url // r.URL.Path always begins with / url := r.URL.Path if d.IsDir() { if url[len(url)-1] != '/' { localRedirect(w, r, path.Base(url)+"/") ---------------------------- 1 return } } else { if url[len(url)-1] == '/' { localRedirect(w, r, "../"+path.Base(url)) ---------------------------- 2 return } } } // serveContent will check modification time sizeFunc := func() (int64, error) { return d.Size(), nil } serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f) ---------------------------- 3 }
重點看到紅色標注部分,現在我們假設我們請求是http://127.0.0.1/abc/d.jpg。那么我們 r.URL.Path的值就是/abc/d.jpg,於是乎,程序進入到1部分(看我藍色字體標注),path.Base()函數是取函數最后/部分,也就是/d.jpg。現在請求變成了/d.jpg,然后進行重定向,這時瀏覽器根據重定向內容再次發送請求,這次請求的url.Path是我們上一次處理好的/d.jpg,最后,程序便順利的進入到了第3部分(見我藍色字體標注)。serveContent 這個函數是最終向瀏覽器發送資源文件的
大概的一個處理文件資源請求的流程就是這樣子,現在我們來解釋一下,為什么
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) 函數的第四個參數那么重要
因為在web服務中,我們發現它永遠都是true,這就導致了我們的url無論是什么,都將會被它cut成只剩最后一部分/xxx.jpg類似的樣子。換句話說,假設我們為文本服務器設置的路由格式是/xxx/xxx/xxx/x.jpg的話。
那么文本服務器根本沒法正常工作,因為它只認識/xx.jpg的路由格式。
這或許也正是你在網上找相關資料的時候,發現大家轉發的內容都是將文本服務器掛載在根節點上。
"/"路由我們通常會將它拿來做網站的入口,這樣豈不是很不爽了?那么有沒有解決的辦法呢? 當然是有的啦,在net/http/server.go文件中,有這么一個函數:
// StripPrefix returns a handler that serves HTTP requests // by removing the given prefix from the request URL's Path // and invoking the handler h. StripPrefix handles a // request for a path that doesn't begin with prefix by // replying with an HTTP 404 not found error. func StripPrefix(prefix string, h Handler) Handler { if prefix == "" { return h } return HandlerFunc(func(w ResponseWriter, r *Request) { if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) { r.URL.Path = p h.ServeHTTP(w, r) } else { NotFound(w, r) } }) }
根據注釋以及代碼來看,它的作用是返回一個Handler,但是這個Handler呢,有點點不一樣,不一樣在哪里呢,它會過濾掉一部分路由前綴。
比如我們有如下路由:/aaa/bbb/ccc.jpg,那么執行StripPrefix("/aaa/bbb", ..handler)之后,我們將會得到一個新的Handler,這個新Handler的執行函數和原來的handler是一樣的,但是這個新Handler在處理路由請求的時候,會自動將/aaa/bbb/ccc.jpg理解為/aaa.jpg
好啦,分析到這里,我們現在再來搭建一個路由路徑為/s/下的文件服務器,代碼如下:
func main() { http.Handle("/s/", http.StripPrefix("/s/", http.FileServer(http.Dir("sourse")))) err := http.ListenAndServe(":8080", nil) if err != nil { fmt.Println(err) } }