接著上一篇的golang分布式存儲 讀書筆記(1)——流操作之GetStream封裝,這次要講的是上傳文件并保存,使用restful
的PUT
方法,書中封裝了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
請求進行測試。
往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)
}
測試
往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()
構造一個writer
和reader
,再初始化一個通道c
,最后使用writer
和c
構造一個PutStream
指針對象返回。
中間開啟了一個子協程,該協程構造一個http.NewRequest
,并使用http.Client
構造客戶端發送請求。其中request
構造的時候使用了管道的讀端reader
,此時該管道的讀端并沒有數據,但是http.NewRequest
構造request
的時候并不會阻塞,而是會阻塞在client.Do(request)
這句代碼,直到管道的寫端寫入數據。
如果得到的響應出錯了,將錯誤寫入管道c
中。
同時這個PutStream
需要實現io.Writer
和io.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.Copy
將r
中的內容復制到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
內容?在上一篇文章中我認為這種流操作可以減小內存占用,但是現在不太確定。