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
有机会我会增加云端生成缩略图功能。