目錄git
這個月我想學一下go語言,因而決定學習一個go實現的緩存服務。github
首先本文基於golang的http包實現一個簡單http的緩存服務,由於用golang自帶的http包實現一個處理請求的服務端十分便利,咱們只須要寫一個簡單的map保存數據,寫一個http的handler處理請求便可,你不須要考慮任何複雜的併發問題,由於golang的http服務框架會幫你處理好底層的一切。golang
本文實現的簡單緩存具有三種基本接口 : SET
GET
DEL
分別經過http協議的PUT
、GET
、DELETE
、操做進行。redis
put
數據庫
PUT /cache/<key> content <value>
GET
瀏覽器
GET /cache/<key> content <value>
DELETE
緩存
DELETE /cache/<key>
本緩存服務裏面經過一個cache包實現緩存功能。
cache包接口定義
:bash
package cache type Cache interface { Set(string, []byte) error Get(string) ([]byte, error) Del(string) error GetStat() Stat }
cache 接口實現
Cache 結構很簡單,一張map,另加一把鎖保護便可.服務器
package cache import "sync" type SimpleCache struct { c map[string][]byte mutex sync.RWMutex Stat } func (c *SimpleCache) Set(k string, v []byte) error { c.mutex.Lock() defer c.mutex.Unlock() tmp, exist := c.c[k] if exist { c.del(k, tmp) } c.c[k] = v c.add(k, v) return nil } func (c *SimpleCache) Get(k string) ([]byte, error) { c.mutex.RLock() defer c.mutex.RUnlock() return c.c[k], nil } func (c *SimpleCache) Del(k string) error { c.mutex.Lock() defer c.mutex.Unlock() v, exist := c.c[k] if exist { delete(c.c, k) c.del(k, v) } return nil } func (c *SimpleCache) GetStat() Stat { return c.Stat } func newInMemoryCache() *SimpleCache { return &SimpleCache{make(map[string][]byte), sync.RWMutex{}, Stat{}} }
cache包測試:數據結構
package main import ( "./cache" "fmt" ) func main() { c := cache.New("inmemory") k, v := "sola", []byte{'a','i','l','u','m','i','y','a'} c.Set(k, v) tmp, _ := c.Get(k) fmt.Println("key: ", k, " value: ", tmp) c.Del(k) tmp, _ = c.Get(k) fmt.Println("key: ", k, " value: ", tmp) }
sola@sola:~/Coder/GitHub/go-cache/http-cache/server$ go run main.go 2019/02/10 00:07:15 inmemory ready to serve key: sola value: [97 105 108 117 109 105 121 97] sola@sola:~/Coder/GitHub/go-cache/http-cache/server$ go run main.go 2019/02/10 00:07:28 inmemory ready to serve key: sola value: [97 105 108 117 109 105 121 97] key: sola value: []
Golang自帶的http包已經實現了htpp客戶端和服務端,咱們能夠利用它更爲快速的開發http服務。本章僅介紹一下http包服務端的使用。
Golang中處理 HTTP 請求主要跟兩個東西相關:ServeMux 和 Handler。
ServrMux 本質上是一個 HTTP 請求路由器(或者叫多路複用器,Multiplexor)。它把收到的請求與一組預先定義的 URL 路徑列表作對比,而後在匹配到路徑的時候調用關聯的處理器(Handler)。
處理器(Handler)負責輸出HTTP響應的頭和正文。任何知足了http.Handler接口的對象均可做爲一個處理器。通俗的說,對象只要有個以下簽名的ServeHTTP方法便可:
ServeHTTP(http.ResponseWriter, *http.Request)
Golang的 HTTP 包自帶了幾個函數用做經常使用處理器,好比NotFoundHandler 和 RedirectHandler。
NotFoundHandler
返回一個簡單的請求處理器,該處理器會對每一個請求都回復"404 page not found"。
RedirectHandler
返回一個請求處理器,該處理器會對每一個請求都使用狀態碼code重定向到網址url。
接着,咱們來看兩個簡單的樣例:
package main import ( "io" "log" "net/http" ) func HelloGoServer(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "Hello, this is a GoServer") } func main() { http.HandleFunc("/", HelloGoServer) err := http.ListenAndServe(":9090", nil) if err != nil { log.Fatal("ListenAndServer ", err) } }
瀏覽器看看咱們的hello程序:
一、 http.HandleFunc("/", HelloGoServer)
http提供的外部方法HandleFunc實際也是調用ServeMux的內部方法,只是它使用的是http包默認的ServeMux,註冊一個處理器函數handler(HelloGoServer
)和對應的模式pattern(/
)(註冊到DefaultServeMux
)。ServeMux的文檔解釋了模式的匹配機制。
二、http.ListenAndServe(":9090", nil)
ListenAndServe同字面意思監聽並服務。這裏是監聽9090端口,它其實也是一個外部方法,調用內部Server類型的ListenAndServe。
package main import ( "log" "net/http" ) func main() { mux := http.NewServeMux() rh := http.RedirectHandler("http://www.baidu.com", 307) mux.Handle("/foo", rh) log.Println("Listening...") http.ListenAndServe(":3000", mux) }
一、這個樣例中咱們沒用默認的ServeMux,而是經過 http.NewServeMux 函數來建立一個空的 ServeMux。
二、http.RedirectHandler 函數建立了一個重定向處理器,這個處理器會對收到的全部請求,都執行307重定向操做到 http://www.baidu.com。
三、ServeMux.Handle 函數將處理器註冊到新建立的 ServeMux,因此它在 URL 路徑/foo 上收到全部的請求都交給這個處理器。
四、最後經過 http.ListenAndServe 函數啓動服務處理請求,經過傳遞剛纔建立的 ServeMux來爲請求去匹配對應處理器。
鍵入後你會跳轉到百度。
最後來實現咱們的cache-server
cache已經有了,咱們只須要寫一個http的Handler來分別處理GET
,PUT
,DELETE
請求便可。
上面提過任何知足了http.Handler接口的對象即ServeHTTP(http.ResponseWriter, *http.Request)
均可做爲一個處理器,那麼咱們先來看看這個接口的參數.
ResponseWriter
接口被HTTP處理器用於構造HTTP回覆。
type ResponseWriter interface { // Header返回一個Header類型值,該值會被WriteHeader方法發送。 // 在調用WriteHeader或Write方法後再改變該對象是沒有意義的。 Header() Header // WriteHeader該方法發送HTTP回覆的頭域和狀態碼。 // 若是沒有被顯式調用,第一次調用Write時會觸發隱式調用WriteHeader(http.StatusOK) // WriterHeader的顯式調用主要用於發送錯誤碼。 WriteHeader(int) // Write向鏈接中寫入做爲HTTP的一部分回覆的數據。 // 若是被調用時還未調用WriteHeader,本方法會先調用WriteHeader(http.StatusOK) // 若是Header中沒有"Content-Type"鍵, // 本方法會使用包函數DetectContentType檢查數據的前512字節,將返回值做爲該鍵的值。 Write([]byte) (int, error) }
Request
類型表明一個服務端接受到的或者客戶端發送出去的HTTP請求。Request各字段的意義和用途在服務端和客戶端是不一樣的。
type Request struct { // Method指定HTTP方法(GET、POST、PUT等)。對客戶端,""表明GET。 Method string // URL在服務端表示被請求的URI,在客戶端表示要訪問的URL。 // // 在服務端,URL字段是解析請求行的URI(保存在RequestURI字段)獲得的, // 對大多數請求來講,除了Path和RawQuery以外的字段都是空字符串。 // (參見RFC 2616, Section 5.1.2) // // 在客戶端,URL的Host字段指定了要鏈接的服務器, // 而Request的Host字段(可選地)指定要發送的HTTP請求的Host頭的值。 URL *url.URL // 接收到的請求的協議版本。本包生產的Request老是使用HTTP/1.1 Proto string // "HTTP/1.0" ProtoMajor int // 1 ProtoMinor int // 0 // Header字段用來表示HTTP請求的頭域。若是頭域(多行鍵值對格式)爲: // accept-encoding: gzip, deflate // Accept-Language: en-us // Connection: keep-alive // 則: // Header = map[string][]string{ // "Accept-Encoding": {"gzip, deflate"}, // "Accept-Language": {"en-us"}, // "Connection": {"keep-alive"}, // } // HTTP規定頭域的鍵名(頭名)是大小寫敏感的,請求的解析器經過規範化頭域的鍵名來實現這點。 // 在客戶端的請求,可能會被自動添加或重寫Header中的特定的頭,參見Request.Write方法。 Header Header // Body是請求的主體。 // // 在客戶端,若是Body是nil表示該請求沒有主體買入GET請求。 // Client的Transport字段會負責調用Body的Close方法。 // // 在服務端,Body字段老是非nil的;但在沒有主體時,讀取Body會馬上返回EOF。 // Server會關閉請求的主體,ServeHTTP處理器不須要關閉Body字段。 Body io.ReadCloser // ContentLength記錄相關內容的長度。 // 若是爲-1,表示長度未知,若是>=0,表示能夠從Body字段讀取ContentLength字節數據。 // 在客戶端,若是Body非nil而該字段爲0,表示不知道Body的長度。 ContentLength int64 // TransferEncoding按從最外到最裏的順序列出傳輸編碼,空切片表示"identity"編碼。 // 本字段通常會被忽略。當發送或接受請求時,會自動添加或移除"chunked"傳輸編碼。 TransferEncoding []string // Close在服務端指定是否在回覆請求後關閉鏈接,在客戶端指定是否在發送請求後關閉鏈接。 Close bool // 在服務端,Host指定URL會在其上尋找資源的主機。 // 根據RFC 2616,該值能夠是Host頭的值,或者URL自身提供的主機名。 // Host的格式能夠是"host:port"。 // // 在客戶端,請求的Host字段(可選地)用來重寫請求的Host頭。 // 如過該字段爲"",Request.Write方法會使用URL字段的Host。 Host string // Form是解析好的表單數據,包括URL字段的query參數和POST或PUT的表單數據。 // 本字段只有在調用ParseForm後纔有效。在客戶端,會忽略請求中的本字段而使用Body替代。 Form url.Values // PostForm是解析好的POST或PUT的表單數據。 // 本字段只有在調用ParseForm後纔有效。在客戶端,會忽略請求中的本字段而使用Body替代。 PostForm url.Values // MultipartForm是解析好的多部件表單,包括上傳的文件。 // 本字段只有在調用ParseMultipartForm後纔有效。 // 在客戶端,會忽略請求中的本字段而使用Body替代。 MultipartForm *multipart.Form // Trailer指定了會在請求主體以後發送的額外的頭域。 // // 在服務端,Trailer字段必須初始化爲只有trailer鍵,全部鍵都對應nil值。 // (客戶端會聲明哪些trailer會發送) // 在處理器從Body讀取時,不能使用本字段。 // 在從Body的讀取返回EOF後,Trailer字段會被更新完畢幷包含非nil的值。 // (若是客戶端發送了這些鍵值對),此時才能夠訪問本字段。 // // 在客戶端,Trail必須初始化爲一個包含將要發送的鍵值對的映射。(值能夠是nil或其終值) // ContentLength字段必須是0或-1,以啓用"chunked"傳輸編碼發送請求。 // 在開始發送請求後,Trailer能夠在讀取請求主體期間被修改, // 一旦請求主體返回EOF,調用者就不可再修改Trailer。 // // 不多有HTTP客戶端、服務端或代理支持HTTP trailer。 Trailer Header // RemoteAddr容許HTTP服務器和其餘軟件記錄該請求的來源地址,通常用於日誌。 // 本字段不是ReadRequest函數填寫的,也沒有定義格式。 // 本包的HTTP服務器會在調用處理器以前設置RemoteAddr爲"IP:port"格式的地址。 // 客戶端會忽略請求中的RemoteAddr字段。 RemoteAddr string // RequestURI是被客戶端發送到服務端的請求的請求行中未修改的請求URI // (參見RFC 2616, Section 5.1) // 通常應使用URI字段,在客戶端設置請求的本字段會致使錯誤。 RequestURI string // TLS字段容許HTTP服務器和其餘軟件記錄接收到該請求的TLS鏈接的信息 // 本字段不是ReadRequest函數填寫的。 // 對啓用了TLS的鏈接,本包的HTTP服務器會在調用處理器以前設置TLS字段,不然將設TLS爲nil。 // 客戶端會忽略請求中的TLS字段。 TLS *tls.ConnectionState }
golang請求及應答中涉及到的常量.
golang中的HTTP狀態碼
const ( StatusContinue = 100 StatusSwitchingProtocols = 101 StatusOK = 200 StatusCreated = 201 StatusAccepted = 202 StatusNonAuthoritativeInfo = 203 StatusNoContent = 204 StatusResetContent = 205 StatusPartialContent = 206 StatusMultipleChoices = 300 StatusMovedPermanently = 301 StatusFound = 302 StatusSeeOther = 303 StatusNotModified = 304 StatusUseProxy = 305 StatusTemporaryRedirect = 307 StatusBadRequest = 400 StatusUnauthorized = 401 StatusPaymentRequired = 402 StatusForbidden = 403 StatusNotFound = 404 StatusMethodNotAllowed = 405 StatusNotAcceptable = 406 StatusProxyAuthRequired = 407 StatusRequestTimeout = 408 StatusConflict = 409 StatusGone = 410 StatusLengthRequired = 411 StatusPreconditionFailed = 412 StatusRequestEntityTooLarge = 413 StatusRequestURITooLong = 414 StatusUnsupportedMediaType = 415 StatusRequestedRangeNotSatisfiable = 416 StatusExpectationFailed = 417 StatusTeapot = 418 StatusInternalServerError = 500 StatusNotImplemented = 501 StatusBadGateway = 502 StatusServiceUnavailable = 503 StatusGatewayTimeout = 504 StatusHTTPVersionNotSupported = 505 )
golang 中的HTTP行爲常量定義
5 package http 6 7 // Common HTTP methods. 8 // 9 // Unless otherwise noted, these are defined in RFC 7231 section 4.3. 10 const ( 11 MethodGet = "GET" 12 MethodHead = "HEAD" 13 MethodPost = "POST" 14 MethodPut = "PUT" 15 MethodPatch = "PATCH" // RFC 5789 16 MethodDelete = "DELETE" 17 MethodConnect = "CONNECT" 18 MethodOptions = "OPTIONS" 19 MethodTrace = "TRACE" 20 )
到這裏全部用到的http包中結構都已經說明了,開始寫main包,
咱們定義一個cacheHandler類型,用咱們的inMemoryCache接口初始化它,並實現他的ServeHTTP方法。
最後將cacheHandler類型的CacheHandler方法註冊到http包默認的ServeMux路由,綁定端口26316,啓動服務。
package main import ( "./cache" "io/ioutil" "net/http" "log" "strings" ) type cacheHandler struct { cache.Cache } func (h *cacheHandler) CacheHandler(w http.ResponseWriter, r *http.Request) { log.Println("url ", r.URL, " Method ", r.Method) //Split Get Key key := strings.Split(r.URL.EscapedPath(), "/")[2] if len(key) == 0 { w.WriteHeader(http.StatusBadRequest) return } m := r.Method if m == http.MethodPut { h.HandlePut(key, w, r) return } else if m == http.MethodGet { h.HandleGet(key, w, r) return } else if m == http.MethodDelete { h.HandleDelete(key, w, r) return } w.WriteHeader(http.StatusMethodNotAllowed) } func (h *cacheHandler) HandlePut(k string, w http.ResponseWriter, r *http.Request){ b, _ := ioutil.ReadAll(r.Body) if len(b) != 0 { e := h.Set(k, b) if e != nil { log.Println(e) w.WriteHeader(http.StatusInternalServerError) } else { w.Write([]byte("successful")) } } } func (h *cacheHandler) HandleGet(k string, w http.ResponseWriter, r *http.Request){ b, e := h.Get(k) if e != nil { log.Println(e) w.WriteHeader(http.StatusInternalServerError) return } if len(b) == 0 { w.WriteHeader(http.StatusNotFound) return } w.Write(b) } func (h *cacheHandler) HandleDelete(k string, w http.ResponseWriter, r *http.Request){ e := h.Del(k) if e != nil { log.Println(e) w.WriteHeader(http.StatusInternalServerError) } else { w.Write([]byte("successful")) } } func main() { c := cache.New("inmemory") h := cacheHandler{c} http.HandleFunc("/cache/", h.CacheHandler) http.ListenAndServe(":26316", nil) }
使用postman測試put
瀏覽器直接測試Get
使用postman測試Delete
再次Get會返回404
緩存功能的服務已經實現了,那麼它的性能怎樣呢,鍵值對緩存服務中比較有名的是redis,咱們和它作下比較。
redis是一款in memory數據結構存儲,能夠被用做數據庫、緩存及消息中間件。支持包括字符串、散列、列表及集合在內的多種數據結構、支持範圍查詢、具有內建的複製功能、lua腳本、LRU緩存淘汰策略、事務處理及兩種不一樣的磁盤持久化方案(RDB和AOF)還能創建redis集羣提供高可用性能。
redis的RDB持久化方案會在指定時間點將內存數據集快照存入磁盤。RDB開始工做時,會本身fork出一個持久化進程,此時原服務進程的一切內存數據至關於保存了一份快照、而後持久化進程將它的內存壓縮並寫入磁盤。
redis的AOF方案則是將服務接受到的全部寫操做記入磁盤上的日誌文件、將日誌文件的格式和redis協議保持一致且只容許添加。
RDB方案對性能的影響比AOF小,由於它不佔用原服務進程的磁盤IO、RDB的缺點在於系統死機時丟失的數據比AOF要多,由於它只保留獲得數據到上一次持久化進程運行的那個時間點,而AOF能夠一直記錄到系統死機以前的最後一次寫操做的數據。
本篇實現的是一個簡單的內存緩存,不包含持久化方案,也不會保存進磁盤,一旦服務器重啓全部數據就會丟失。
性能方面只有redis的1/4,主要緣由在於REST協議的解析上,REST基於HTTP,HTTP基於TCP,而redis是直接創建在TCP上的。
下一篇文章會實現一個基於TCP的緩存協議規範。本系列筆記最終實現的緩存會是使用HTTP/REST協議和TCP混合的接口規範,其中HTTP/REST只用於各類管理功能。
本文源碼 :https://github.com/BethlyRoseDaisley/go-cache-server/tree/master/http-cache/server
參考資料:
分佈式緩存-原理、架構及Go語言實現 ----- 胡世傑