1.3 Go语言实现

首先让我们来看一下main函数的实现。和大多数语言一样,Go语言也有一个main函数作为系统的入口点。在main函数中我们需要做的只是注册一个HTP处理函数并开始监听端口,见例1-1。 例1-1main函数

package main

import (
    "log"
    "os"
    "net/http"
)

func main()  {
    http.HandleFunc("/objects/",objects.Handler)
    log.Fatal(http.ListenAndServe(os.Getenv("LISTEN_ADDRESS"),nil))
}

httpHandlefunc的作用是注册HTP处理函数objects.Handler,如果有客户端访问本机的HTTP服务且URI以“/objects/”开头,那么该请求将由objects.Handler负责处理。除此以外的HTTP请求会默认返回HTTP错误代码404 Not Found。

处理函数注册成功后,我们调用htt. Listen AndServe正式开始监听端口,该端口由系统环境变量 LISTEN ADDRESS定义。正常情况下该函数永远不会返回,程序运行后会始终监听端口上的请求,除非我们发送信号中断进程。非正常情况下,该函数会将错误返回,此时 log.Fatal会打印错误的信息并退出程序。

objects.Handler函数属于objects包,该包一共有3个函数,除了 Handler以外还有put和get函数。在Go语言中,某个变量或函数名的首字母大写,则意味着该变量或函数可在包外部被引用;如果是首字母小写,则意味着该变量或函数名仅可在包内部被引用。 Handler函数的首字母H大写就表明该函数可以在 objects包外部被调用(被main包的main函数调用),而put和get函数则仅在 objects包内部可见。 objects.Handler函数:

package objects
import "net/http"

func Handler(w http.ResponseWriter,r* http.Request){
    m:=r.Method
    if m==http.MethodPut{
        put(w, r)
        return
    }
    if m==http.MethodGet{
        get(w, r)
        return
    }
    w.WriteHeader(http.StatusMethodNotAllowed)
}

Handler有两个参数。w的类型是httpResponseWriter,用于写入HTTP的响应。它的WriteHeader方法用于写HTP响应的错误代码,它的Write方法则用于写HTTP响应的正文。r的类型是+httpRequest,是一个指向httpRequest结构体的指针,代表当前处理的HTTP的请求。它的Method成员变量记录了该HTTP请求的方法。Handler函数会首先检查HTTP请求的方法:如果是PUT,则调用put函数;如果是GET,则调用get函数;两者皆否,则返回HTTP错误代码 405 MethodNotAllowed。

put函数负责处理HTTP的PT请求,将PUT上来的对象存储在本地硬盘上,见例1-3。

func put(w http.ResponseWriter,r* http.Request)  {
    path := os.Getenv("STORAGE_ROOT") +  "/objects/" +  strings.Split(r.URL.EscapedPath(),"/")[2]
    f,e:=os.Create(path)
    if e!=nil{
        log.Println(e)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    defer f.Close()
    io.Copy(f,r.Body)

}

put函数首先获取URL中<objectname>的部分,URL成员变量记录了HTTP请求的URL,它的 EscapedPath方法用于获取经过转义以后的路径部分,该路径的形式是strings. Split函数的功能是用分隔符将一个字符串分割成多个字符串,它有两个输入变量,第一个输入变量是需要分割的字符串,第二个输入变量则是分隔符,分割后的结果以字符串的数组形式返回。在这里,strings.Split会将我们的URL路径拆分成3 个字符串,分别是“”,“ objects”和<object name>。所以数组的第三个元素就是<object name>

然后我们会调用 os.Create在本地文件系统的根存储目录的 objects子目录下创建 个同名文件f(根存储目录由系统环境变量 STORAGE ROOT定义。除了 objects子目 录以外,还有一些其他的子目录,我们还会在后续章节看到它们的用处)。如果创建失 败则返回HTTP错误代码500;如果创建成功则将r.Body用io.Copy写入文件f。 io.Copy 接收两个参数,第一个参数是用于写入的 io.writer,在这里我们的文件f就是一个 io.Writer,任何写入f的数据都会被写入f所代表的文件; io.Copy接收的第二个参数则 是一个用于读取的io.Reader,在这里就是r.body。 http.Request的Body成员变量是一个io.Reader,用来读取HTTP请求的正文内容。

这里需要提醒一点:我们的实现代码默认用户提供的URL的<object name>部分不 能包含“/”,如果包含,则“/”后的部分将被丢弃。真实生产环境中的代码需要对用户的所有输入都进行严格的校验,如果用户输入的URL不符合我们的预期,则服务器必须返回一个错误的信息,而不是忽略。

细心的读者看到这里可能要问:那如果我的对象名字里就需要有一个“/”怎么办呢?别担心,任何对象名字在被放入URL之前都需要在客户端进行URL编码。在编码时,一些不适合放入URL的字符会被转义。比如说用户有一个对象名字是“C:/中文目录/ab&ctxt”,编码后的名字是“C%3A%2F%E4%B8%AD%E6%96%87%E7%9B%AE%E5%BD%95%2Fa%5Cb%26c.txt”,其中的“/“被转义成“%2F”。然后客户端会以“/objects/C%3A%2F%E4%B8%AD%E6%96%87%E7%9B%AE%E5%BD%95%2Fa%5Cb%26cxt”作为HTTP请求的URL访问我们的服务。我们在获取URL时使用的 r.URL.EscapedPath方法得到的就是这个字符串。

get函数负责处理HTTP的GET请求,从本地硬盘上读取内容并将其作为HTTP的响应输出,见例1-4

func get(w http.ResponseWriter,r* http.Request)  {
    path := os.Getenv("STORAGE_ROOT") +  "/objects/" +  strings.Split(r.URL.EscapedPath(),"/")[2]
    f,e:=os.Open(path)
    if e!=nil{
        log.Println(e)
        w.WriteHeader(http.StatusNotFound)
        return
    }
    defer f.Close()
    io.Copy(w,f)

}

和put函数类似,get函数首先获取<object name>并调用 os.Open尝试打开本地文件系统根存储目录objects子目录中的同名文件f,如果打开失败,则返回HTTP错误代码404;如果打开成功,则用 1o.Copy将f的内容写入w,此时f作为读取内容的io.Reader,而w则作为写入的io.Writer。

你可能会觉得诧异,刚才我们介绍put的时候还说f是一个io.Writer,现在怎么又 变成了io.Reader ?事实上f本身的类型是* os File,一个指向os File结构体的指针。os File这个结构体同时实现了io. Writer和io. Reader这两个接口(Go语言中称为interface)。在Go语言中,实现接口只需要实现该接口所要求的全部方法,io.Write接口只要求实现一个 Write方法,而 io Read接口则只要求实现一个Read方法。 os File同时实现了 Write和Read方法,于是它既是一个io.Writer也是一个 io.Reader 10。

http.ResponseWriter也是一个接口,这个接口除了实现了WriteHeader方法以外也实现 了 Write方法,所以它也是一个io.Write接口。

目录

分布式对象存储原理架构Go实现
第一章 对象存储简介
第二章 可扩展的分布式系统
第三章 元数据服务