上一节我们实现了单机版的缓存服务,但是我们的目标是分布式缓存。那么,我们就需要把缓存服务部署到多态机器节点上,对外提供访问接口。客户端就可以通过这些接口去实现缓存的增删改查。
分布式缓存需要实现节点间通信,而通信方法常见的有HTTP和RPC。建立基于 HTTP 的通信机制是比较常见和简单的做法。
所以,我们基于 Go 语言标准库 http 搭建 HTTP Server。
net/http标准库
简单实现一个http服务端例子。
type server int
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
}
func main() {
var s server
http.ListenAndServe("localhost:9999", &s)
}
//下面的是http源码
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
在该代码中,创建任意类型 server,并实现 ServeHTTP
方法。
http.ListenAndServe
接收 2 个参数,第一个参数是服务启动的地址,第二个参数是 Handler,实现了 ServeHTTP
方法的对象都可以作为 HTTP 的 Handler。
Cache HTTP 服务端
我们需要创建一个结构体去实现Handler接口,而该结构体应该是有些属性变量来支撑我们做一些事情的。
HTTPPool
有 2 个参数,一个是 addr,用来记录自己的地址,包括主机名/IP 和端口。- 另一个是 basePath,作为节点间通讯地址的前缀,默认是
/geecache/。比如
http://example.com/geecache/开头的请求,就用于节点间的访问。因为一个主机上还可能承载其他的服务,加一段 Path 是一个好习惯。比如,大部分网站的 API 接口,一般以/api
作为前缀。
HTTPPool实现了ServeHTTP方法,即是Handler接口。
const defaultBasePath = "/geecache/"
type HTTPPool struct {
addr string //本地IP端口, 比如:"localhost:10000"
basePath string
}
func (pool *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//处理请求和响应,后面会实现的
}
//创建HTTPPool方法
func NewHTTPPool(addr string, basePath string) *HTTPPool {
return &HTTPPool{
addr: addr,
basePath: basePath,
}
}
接下来实现最为核心的ServeHTTP方法。
func (pool *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, pool.basePath) {
panic("HTTPPool serving unexpected path: " + r.URL.Path)
}
parts := strings.SplitN(r.URL.Path[len(pool.basePath):], "/", 2)
if len(parts) != 2 {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
groupName := parts[0]
group := GetGroup(groupName)
if group == nil {
http.Error(w, "no such group: "+groupName, http.StatusNotFound)
return
}
view, err := group.Get(parts[1])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(view.ByteSlice())
}
该方法是先判断前缀是否相等,若不相等,返回错误,之后再判断分组名字,再判断key。
url.Path的格式是/<basepath>/<groupname>/<key>。举个例子,r.URL.Path是/geecache/scores/tom,那r.URL.Path[len(pool.basePath):]即是scores/tom,接着就是使用strings.SplitN函数进行分割来获取分组名字和key。
测试
// 缓存中没有的话,就从该db中查找
var db = map[string]string{
"tom": "100",
"jack": "200",
"sam": "444",
}
func main() {
//传函数入参
cache.NewGroup("scores", 2<<10, cache.GetterFunc(funcCbGet))
//传结构体入参,也可以
// cbGet := &search{}
// cache.NewGroup("scores", 2<<10, cbGet)
addr := "localhost:10000"
peers := cache.NewHTTPPool(addr, cache.DefaultBasePath)
log.Fatal(http.ListenAndServe(addr, peers))
}
// 函数的
func funcCbGet(key string) ([]byte, error) {
fmt.Println("callback search key: ", key)
if v, ok := db[key]; ok {
return []byte(v), nil
}
return nil, fmt.Errorf("%s not exit", key)
}
// 结构体,实现了Getter接口的Get方法,
type search struct {
}
func (s *search) Get(key string) ([]byte, error) {
fmt.Println("struct callback search key: ", key)
if v, ok := db[key]; ok {
return []byte(v), nil
}
return nil, fmt.Errorf("%s not exit", key)
}
执行 go run main.go,查看效果
完整代码:https://github.com/liwook/Go-projects/tree/main/go-cache/3-httpServer