golang分布式存儲 讀書筆記(2)——流操作之PutStream封裝

接著上一篇的golang分布式存儲 讀書筆記(1)——流操作之GetStream封裝,這次要講的是上傳文件并保存,使用restfulPUT方法,書中封裝了PutStream結構。

接口設計

客戶端上傳數據時向接口服務器發送PUT請求,請求的url/objects/<object_name>

同樣接口服務器向數據服務器轉發PUT請求,請求的url/objects/<object_name>。數據服務器在本地指定文件夾(D:/objects/)下創建<object_name>文件,將PUT的內容寫入文件中。

目錄結構

GOPATH/src目錄下,目錄結構為:

go-storage
    apiServer
        objects
            get.go
            put.go
        apiServer.go
    dataServer
        dataServer.go

數據服務器實現

dataServer代碼

數據服務器的put接口和get接口很類似,只不過將讀文件改為了寫文件。

package main

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

const (
    objectDir = "D:/objects/"
)

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

func put(w http.ResponseWriter, r *http.Request) {
    // 提取文件名
    fname := strings.Split(r.URL.EscapedPath(), "/")[2]
    log.Println(fname)
    // 創建文件
    f, e := os.Create(objectDir + fname)
    if e != nil {
        log.Println(e)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    defer f.Close()
    // 往文件寫入數據
    io.Copy(f, r.Body)
}

func main() {
    http.HandleFunc("/objects/", Handler)
    http.ListenAndServe(":8889", nil)
}

io.Copy(f, r.Body)r.body的數據寫入f中,其中r.Body實現了io.ReadCloser接口,f實現了io.Writer接口。最后要記得關閉文件。

測試

使用Restlet Client發送PUT請求進行測試。

測試數據服務器.png

http://localhost:8889/objects/1.txt發送PUT請求,可以看到本地確實生成了1.txt文件。

接口服務器實現

版本一

實現PUT請求

可以使用http.NewRequest構造一個PUT請求,使用http.Client構造一個客戶端進行發送。

例如:

request, _ := http.NewRequest("PUT","http://127.0.0.1:8889/objects/"+object, reader)
client := http.Client{}
r, e := client.Do(request) // 發送并接受請求

完整代碼如下:

package main

import (
    "net/http"
    "io"
    "strings"
    "go-storage/apiServer/objects"
    "log"
)
const dataServerAddr = "http://localhost:8889/objects/"

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

func put(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()
    object := strings.Split(r.URL.EscapedPath(), "/")[2]
    request, _ := http.NewRequest("PUT",dataServerAddr + object, r.Body)
    client := http.Client{}
    resp, e := client.Do(request) // 發送并接受請求
    if e == nil && resp.StatusCode != http.StatusOK {
        w.WriteHeader(http.StatusNotFound)
        log.Printf("dataServer return http code %d", resp.StatusCode)
        return
    }
    defer resp.Body.Close()
    io.Copy(w, resp.Body)
}
func main() {

    http.HandleFunc("/objects/", Handler)
    http.ListenAndServe(":8888", nil)
}

測試

接口服務器測試.png

http://localhost:8888/objects/2.txt發送PUT請求,可以看到本地確實生成了2.txt文件。

版本二——封裝版本

書上的代碼實現的相對復雜一點,將整個操作封裝成了一個PutStream的結構體,先看結構體的具體成員:

type PutStream struct {
    writer *io.PipeWriter
    c      chan error
}

可以看到其中一個成員是io.PipeWriter類型,這類似于linux中的管道,一端寫入,一讀取讀取,這里是為了在兩個協程之間建立連接,這里使用channel并不好使。

另一個成員是channel類型,因為子協程不能有返回值,所以這里用通道傳遞錯誤。

PutStream的構造函數如下:

func NewPutStream(server, object string) *PutStream {
    reader, writer := io.Pipe() // 通過管道將 兩個協程 聯系起來 (用channel應該也可以把?)
    c := make(chan error)
    go func() {
        request, _ := http.NewRequest("PUT", "http://"+server+"/objects/"+object, reader)
        client := http.Client{}
        r, e := client.Do(request) // 如果 reader一直沒有數據,是不是 Do就會阻塞?
        if e == nil && r.StatusCode != http.StatusOK {
            e = fmt.Errorf("dataServer return http code %d", r.StatusCode)
        }
        c <- e
    }()
    return &PutStream{writer, c}
}

先使用io.Pipe()構造一個writerreader,再初始化一個通道c,最后使用writerc構造一個PutStream指針對象返回。

中間開啟了一個子協程,該協程構造一個http.NewRequest,并使用http.Client構造客戶端發送請求。其中request構造的時候使用了管道的讀端reader,此時該管道的讀端并沒有數據,但是http.NewRequest構造request的時候并不會阻塞,而是會阻塞在client.Do(request)這句代碼,直到管道的寫端寫入數據。

如果得到的響應出錯了,將錯誤寫入管道c中。

同時這個PutStream需要實現io.Writerio.Writer接口:

// 實現了 io.Writer接口
func (w *PutStream) Write(p []byte) (n int, err error) {
    return w.writer.Write(p)
}
// 關閉流并得到錯誤
func (w *PutStream) Close() error {
    w.writer.Close() // io.PipeWriter 關閉, reader也會關閉? client.Do(request)才能結束?
    return <-w.c
}

由于功能是要上傳并保存一個對象,所以實現一個StoreObject方法來調用PutStream

func StoreObject(r io.Reader, object string) (int, error) {
    stream := NewPutStream(data_server, object)

    // 會阻塞,直到r中收到 EOF,stream實現了io.Writer接口
    io.Copy(stream, r) // 將r的內容拷貝的  stream 中,stream有數據的時候,他對應的reader也就有了數據
    // 會阻塞到 stream中的c channel收到消息
    e := stream.Close()
    if e != nil {
        return http.StatusInternalServerError, e
    }
    return http.StatusOK, nil
}

新建一個PutStream指針類型的stream,由于它實現了io.Writer接口,所以可以調用io.Copyr中的內容復制到stream中,其實就是寫入到管道的寫端。最后關閉流,管道寫端寫入也就結束,讀端也讀取結束,子協程的發送也就結束了。

其實該版本的實現和版本一是一樣的,只不過多了一個子協程,多使用了io.Pipe管道。

看起來其實版本一更加簡單、直接。暫時也看不出封裝的優勢,也許在后面功能越來越復雜的時候,就可以體現這個優勢。個人感覺這個封裝還是比較優雅。

完整代碼

package objects, put.go

package objects

import (
    "net/http"
    "fmt"
    "io"
)

type PutStream struct {
    writer *io.PipeWriter
    c      chan error
}

const (
    data_server = "127.0.0.1:8889"
)

func StoreObject(r io.Reader, object string) (int, error) {
    stream := NewPutStream(data_server, object)

    // 會阻塞到 r中收到 EOF  stream實現了io.Writer接口
    io.Copy(stream, r) // 將r的內容拷貝的  stream 中,stream有數據的時候,他對應的reader也就有了數據
    // 會阻塞到 stream中的c channel收到消息
    e := stream.Close()
    if e != nil {
        return http.StatusInternalServerError, e
    }
    return http.StatusOK, nil
}

func NewPutStream(server, object string) *PutStream {
    reader, writer := io.Pipe() // 通過管道將 兩個協程 聯系起來 (用channel應該也可以把?)
    c := make(chan error)
    go func() {
        request, _ := http.NewRequest("PUT", "http://"+server+"/objects/"+object, reader)
        client := http.Client{}
        r, e := client.Do(request) // 如果 reader一直沒有數據,是不是 Do就會阻塞?
        if e == nil && r.StatusCode != http.StatusOK {
            e = fmt.Errorf("dataServer return http code %d", r.StatusCode)
        }
        c <- e
    }()
    return &PutStream{writer, c}
}

// 實現了 io.Writer接口
func (w *PutStream) Write(p []byte) (n int, err error) {
    return w.writer.Write(p)
}

func (w *PutStream) Close() error {
    w.writer.Close() // io.PipeWriter 關閉, reader也會關閉? client.Do(request)才能結束?
    return <-w.c
}

package main, apiServer.go

package main

import (
    "net/http"
    "io"
    "strings"
    "go-storage/apiServer/objects"
    "log"
)
const dataServerAddr = "http://localhost:8889/objects/"

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

    w.WriteHeader(http.StatusMethodNotAllowed)
}

func put(w http.ResponseWriter, r *http.Request) {
    // object 要保存的對象名
    object := strings.Split(r.URL.EscapedPath(), "/")[2]
    c, e := objects.StoreObject(r.Body, object)
    if e != nil {
        log.Println(e)
    }
    w.WriteHeader(c)
}

func main() {

    http.HandleFunc("/objects/", Handler)
    http.ListenAndServe(":8888", nil)
}

測試過程同版本一。

疑問

  • 如果傳輸一個4g的文件,到底需不需要占用4g內容?在上一篇文章中我認為這種流操作可以減小內存占用,但是現在不太確定。

參考

《分布式對象存儲--原理架構及Go語言實現》

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容