• Go分布式缓存 HTTP 服务端(day3)


    Go分布式缓存 HTTP 服务端(day3)

    1 http 标准库

    Go 语言提供了 http 标准库,可以非常方便地搭建 HTTP 服务端和客户端。比如我们可以实现一个服务端,无论接收到什么请求,都返回字符串 “Hello World!”

    package main
    
    import (
    	"log"
    	"net/http"
    )
    
    type server int
    
    func (h *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    	log.Println(r.URL.Path)
    	w.Write([]byte("Hello World!"))
    }
    
    func main() {
    	var s server
    	http.ListenAndServe("localhost:9999", &s)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 创建任意类型 server,并实现 ServeHTTP 方法。
    • 调用 http.ListenAndServe 在 9999 端口启动 http 服务,处理请求的对象为 s server

    接下来我们执行 go run . 启动服务,借助 curl 来测试效果:

    $ curl http://localhost:9999  
    Hello World!
    $ curl http://localhost:9999/abc
    Hello World!
    
    • 1
    • 2
    • 3
    • 4

    Go 程序日志输出

    2020/02/11 22:56:32 /
    2020/02/11 22:56:34 /abc
    
    • 1
    • 2

    http.ListenAndServe 接收 2 个参数,第一个参数是服务启动的地址,第二个参数是 Handler,任何实现了 ServeHTTP 方法的对象都可以作为 HTTP 的 Handler。

    在标准库中,http.Handler 接口的定义如下:

    package http
    
    type Handler interface {
        ServeHTTP(w ResponseWriter, r *Request)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2 GeeCache HTTP 服务端

    分布式缓存需要实现节点间通信,建立基于 HTTP 的通信机制是比较常见和简单的做法。如果一个节点启动了 HTTP 服务,那么这个节点就可以被其他节点访问。今天我们就为单机节点搭建 HTTP Server。

    不与其他部分耦合,我们将这部分代码放在新的 http.go 文件中,当前的代码结构如下:

    geecache/
        |--lru/
            |--lru.go  // lru 缓存淘汰策略
        |--byteview.go // 缓存值的抽象与封装
        |--cache.go    // 并发控制
        |--geecache.go // 负责与外部交互,控制缓存存储和获取的主流程
    	|--http.go     // 提供被其他节点访问的能力(基于http)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    首先我们创建一个结构体 HTTPPool,作为承载节点间 HTTP 通信的核心数据结构(包括服务端和客户端,今天只实现服务端)。

    day3-http-server/geecache/http.go - github

    package geecache
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    	"strings"
    )
    
    const defaultBasePath = "/_geecache/"
    
    // HTTPPool implements PeerPicker for a pool of HTTP peers.
    type HTTPPool struct {
    	// this peer's base URL, e.g. "https://example.net:8000"
    	self     string
    	basePath string
    }
    
    // NewHTTPPool initializes an HTTP pool of peers.
    func NewHTTPPool(self string) *HTTPPool {
    	return &HTTPPool{
    		self:     self,
    		basePath: defaultBasePath,
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • HTTPPool 只有 2 个参数,一个是 self,用来记录自己的地址,包括主机名/IP 和端口。
    • 另一个是 basePath,作为节点间通讯地址的前缀,默认是 /_geecache/,那么 http://example.com/_geecache/ 开头的请求,就用于节点间的访问。因为一个主机上还可能承载其他的服务,加一段 Path 是一个好习惯。比如,大部分网站的 API 接口,一般以 /api 作为前缀。

    接下来,实现最为核心的 ServeHTTP 方法。

    // Log info with server name
    func (p *HTTPPool) Log(format string, v ...interface{}) {
    	log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...))
    }
    
    // ServeHTTP handle all http requests
    func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    	if !strings.HasPrefix(r.URL.Path, p.basePath) {
    		panic("HTTPPool serving unexpected path: " + r.URL.Path)
    	}
    	p.Log("%s %s", r.Method, r.URL.Path)
    	// /// required
    	parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2)
    	if len(parts) != 2 {
    		http.Error(w, "bad request", http.StatusBadRequest)
    		return
    	}
    
    	groupName := parts[0]
    	key := parts[1]
    
    	group := GetGroup(groupName)
    	if group == nil {
    		http.Error(w, "no such group: "+groupName, http.StatusNotFound)
    		return
    	}
    
    	view, err := group.Get(key)
    	if err != nil {
    		http.Error(w, err.Error(), http.StatusInternalServerError)
    		return
    	}
    
    	w.Header().Set("Content-Type", "application/octet-stream")
    	w.Write(view.ByteSlice())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • ServeHTTP 的实现逻辑是比较简单的,首先判断访问路径的前缀是否是 basePath,不是返回错误。
    • 我们约定访问路径格式为 ///,通过 groupname 得到 group 实例,再使用 group.Get(key) 获取缓存数据。
    • 最终使用 w.Write() 将缓存值作为 httpResponse 的 body 返回。

    到这里,HTTP 服务端已经完整地实现了。接下来,我们将在单机上启动 HTTP 服务,使用 curl 进行测试。

    3 测试

    实现 main 函数,实例化 group,并启动 HTTP 服务。

    day3-http-server/main.go - github

    package main
    
    import (
    	"fmt"
    	"geecache"
    	"log"
    	"net/http"
    )
    
    var db = map[string]string{
    	"Tom":  "630",
    	"Jack": "589",
    	"Sam":  "567",
    }
    
    func main() {
    	geecache.NewGroup("scores", 2<<10, geecache.GetterFunc(
    		func(key string) ([]byte, error) {
    			log.Println("[SlowDB] search key", key)
    			if v, ok := db[key]; ok {
    				return []byte(v), nil
    			}
    			return nil, fmt.Errorf("%s not exist", key)
    		}))
    
    	addr := "localhost:9999"
    	peers := geecache.NewHTTPPool(addr)
    	log.Println("geecache is running at", addr)
    	log.Fatal(http.ListenAndServe(addr, peers))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 同样地,我们使用 map 模拟了数据源 db。
    • 创建一个名为 scores 的 Group,若缓存为空,回调函数会从 db 中获取数据并返回。
    • 使用 http.ListenAndServe 在 9999 端口启动了 HTTP 服务。

    需要注意的点:
    main.go 和 geecache/ 在同级目录,但 go modules 不再支持 import <相对路径>,相对路径需要在 go.mod 中声明:
    require geecache v0.0.0
    replace geecache => ./geecache

    接下来,运行 main 函数,使用 curl 做一些简单测试:

    $ curl http://localhost:9999/_geecache/scores/Tom
    630
    $ curl http://localhost:9999/_geecache/scores/kkk
    kkk not exist
    
    • 1
    • 2
    • 3
    • 4

    GeeCache 的日志输出如下:

    2020/02/11 23:28:39 geecache is running at localhost:9999
    2020/02/11 23:29:08 [Server localhost:9999] GET /_geecache/scores/Tom
    2020/02/11 23:29:08 [SlowDB] search key Tom
    2020/02/11 23:29:16 [Server localhost:9999] GET /_geecache/scores/kkk
    2020/02/11 23:29:16 [SlowDB] search key kkk
    
    • 1
    • 2
    • 3
    • 4
    • 5

    节点间的相互通信不仅需要 HTTP 服务端,还需要 HTTP 客户端,这就是我们下一步需要做的事情。


    4 总结

    使用HTTP标准库建立服务端,通过不同的路径实现访问缓存内容。

  • 相关阅读:
    JVM垃圾收集器--分代收集器
    深入浅出 Spring Boot 多种设定档管里(Spring Profiles)
    zabbix配置钉钉报警
    latex小节标题如何靠左显示
    TCP/IP(六)TCP的连接管理(三)
    大学毕业去什么样的公司工作不后悔?
    [Ext JS]图片显示方式
    pg_dump执行流程简单记录
    项目中应该使用nginx还是拦截器来封禁IP
    一文总结 Google I/O 2023
  • 原文地址:https://blog.csdn.net/weixin_45750972/article/details/127860390