這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Golang編寫簡單圖片伺服器
圖片伺服器
最近的開發過程中,遇到一個問題,就是大量零碎圖片的儲存,最後我決定研究一個簡單的映像伺服器,以解決影像檔儲存的效能問題。在此,寫一篇博文記錄我經曆的思想過程和遇到的坑。
我們知道Linux隱藏檔不建議將大量檔案儲存體到一個檔案夾,這樣做不僅容易大量消耗系統的iNode塊,也很容易發生檔案讀寫速度快速下降。
解決方案
通過分析需求,可得出一個方案,就是儘可能的讓檔案隨機分布在不同的檔案夾中,考慮到檔案夾子檔案數1000是個效能坎,可以給檔案分配編號fileid,通過給fileid分區,就可以避免效能問題。
為了增加整個伺服器的持續度,我決定使用uint64作為檔案id,這樣可以提供更大的計數區間,減少建立fileid的碰撞機率。
基本結構
結構很簡單:
隨機fileid發生器,fileid轉檔案路徑(儲存路徑),JSON設定檔讀取,上傳、下載控制器
Golang使用JSON做設定檔
這裡值得寫的還是比較多的,
隨機數產生
隨機數產生部分我選擇了seehuhn編寫的mt19937庫,項目地址:github.com/seehuhn/mt19937。
值得一提的是這個隨機數庫給的文檔並不能用,簡單地看了一下代碼,發現正確用法應該是這樣的:
mt:=mt19937.New() mt.Seed(time.Now().UnixNano()) var buf = make([]byte, 8) randuint64:=mt.Uint64()
隨機fileid產生器代碼
func MakeImageID()string{ mt:=mt19937.New() mt.Seed(time.Now().UnixNano()) var buf = make([]byte, 8) binary.BigEndian.PutUint64(buf, mt.Uint64()) return strings.ToUpper(hex.EncodeToString(buf))}
我產生的fileid最後是這樣形式的:6A778903AD673478,16位十六進位字串,很適合儲存在資料庫中。
fileid轉檔案路徑
使用了很Ugly的Sprintf方法,不知道這個能不能有更優雅的寫法。要注意的是,Golang裡沒有Substring這種截取子字串的函數,而是使用切片方式進行子字串截取。具體方式是這樣的:substring=string[start:end]
func ImageID2Path(imageid string)string{ return fmt.Sprintf("%s/%s/%s/%s/%s/%s/%s/%s/%s.jpg",conf.Storage,imageid[0:2],imageid[2:4],imageid[4:6],imageid[6:8],imageid[8:10],imageid[10:12],imageid[12:14],imageid[14:16])}
JSON設定檔讀取
Golang中最好用的莫過於JSON設定檔,人好寫,機器也好解析,官方庫完全支援JSON的讀寫,非常好用。
我使用的JSON結構如下:
{ "ListenAddr":"0.0.0.0:10086", "Storage":"/var/www/html/image/storage"}
解析代碼:
//對應的JSON結構type Config struct { ListenAddr string Storage string}//產生一個全域的conf變數儲存讀取的配置var conf Config//讀取配置函數func LoadConf(){//開啟檔案 r, err := os.Open("config.json") if err != nil { log.Fatalln(err) }//解碼JSON decoder := json.NewDecoder(r) err = decoder.Decode(&conf) if err != nil { log.Fatalln(err) }}
調用LoadConf()之後,你就可以在任何地方使用conf中的配置。
Web介面(gorilla/mux)
基礎路由
Golang上最簡單易用的url路由器,我認為是”github.com/gorilla/mux”。
使用這個需要先go get,忘記說了,golang引用第三方庫需要使用go get先下載庫的代碼。
//建立一個web路由 r := mux.NewRouter() //這個是設定普通GET路徑,HandleFunc第一個參數是URL,第二個參數是要響應的函數,最後可以在Methods的字串裡填入你想要區分的請求方式,目前我測試能用的包括GET、POST、PUT和DELETE r.HandleFunc("/", HomeHandler).Methods("GET") r.HandleFunc("/",UploadHandler).Methods("POST") //在url裡可以用花括弧括起來一個變數,這個變數支援放在url的任何位置。RESTAPI中常見的把文章id,使用者id寫入url都可以用這樣的方式實現。 r.HandleFunc("/{imgid}",DownloadHandler).Methods("GET") err := http.ListenAndServe(conf.ListenAddr, r)
如何提取URL中的參數呢?在請求處理函數裡使用:
vars := mux.Vars(r) imageid := vars["imgid"]
r是http.Request
下載處理
代碼很簡單容易看
func DownloadHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) imageid := vars["imgid"] if len([]rune(imageid)) != 16 { w.Write([]byte("Error:ImageID incorrect.")) return } imgpath := ImageID2Path(imageid) if !FileExist(imgpath) { w.Write([]byte("Error:Image Not Found.")) return } http.ServeFile(w, r, imgpath)}
Golang的http包,對於伺服器提供檔案下載實現了很好的封裝,一句http.ServeFile(w, r, filepath)完事,其中w和r是處理函數中傳入的參數,我們只需要原樣傳入即可,filepath是需要提供給用戶端的檔案,其餘的東西http庫會自動處理,包括content-type之類的。
上傳處理
func UploadHandler(w http.ResponseWriter, r *http.Request) { //隨機產生一個不存在的fileid var imgid string for{ imgid=MakeImageID() if !FileExist(ImageID2Path(imgid)){ break } } //上傳參數為uploadfile r.ParseMultipartForm(32 << 20) file, _, err := r.FormFile("uploadfile") if err != nil { log.Println(err) w.Write([]byte("Error:Upload Error.")) return } defer file.Close() //檢測檔案類型 buff := make([]byte, 512) _, err = file.Read(buff) if err != nil { log.Println(err) w.Write([]byte("Error:Upload Error.")) return } filetype := http.DetectContentType(buff) if filetype!="image/jpeg"{ w.Write([]byte("Error:Not JPEG.")) return } //迴繞檔案指標 log.Println(filetype) if _, err = file.Seek(0, 0); err!=nil{ log.Println(err) } //提前建立整棵儲存樹(如果不進行儲存樹結構建立,下面的檔案建立不會成功) if err=BuildTree(imgid); err!=nil{ log.Println(err) } //將檔案寫入ImageID指定的位置 f, err := os.OpenFile(ImageID2Path(imgid), os.O_WRONLY|os.O_CREATE, 0666) if err != nil { log.Println(err) w.Write([]byte("Error:Save Error.")) return } defer f.Close() io.Copy(f, file) w.Write([]byte(imgid))}
總結一下
基本開發就是這樣,很簡單的我們就用Golang實現了一個高效能的Web圖片伺服器,Golang使用它特有的協程機制實現了不需要過多特殊處理就能完成高並發程式的開發。如果正確的使用Golang,你可以很容易的做到普通語言非常麻煩處理的高效能並行網路編程。
PS:Golang的http內建了高並發支援,如果自己寫程式想利用go進行高並發,可以使用golang的go關鍵字,在你的函數前面加一個go標誌,他就會自動的去協程裡運行了,非常方便,完全不用考慮如何建立線程之類的,Golang的核心自動幫你做了。
代碼
代碼已上傳GitHub: https://github.com/zjyl1994/QuickImageServer
有機會我會增加雲端產生縮圖功能。