目录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语言实现 ----- 胡世杰