本文代码地址:https://gitee.com/lymgoforIT/gee-cache/tree/master/day3-http-server
本文是7天用Go从零实现分布式缓存GeeCache
的第三篇。
- 介绍如何使用
Go
语言标准库http
搭建HTTP Server
- 并实现
main
函数启动HTTP Server
测试API
,代码约60
行
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)
}
- 创建任意类型
server
,并实现ServeHTTP
方法。 - 调用
http.ListenAndServe
在9999
端口启动http
服务,处理请求的对象为s server
。
接下来我们执行 go run .
启动服务,借助 curl
来测试效果:
$ curl http://localhost:9999
Hello World!
$ curl http://localhost:9999/abc
Hello World!
Go
程序日志输出
2024/07/20 22:56:32 /
2020/07/20 22:56:34 /abc
http.ListenAndServe 接收 2 个参数,第一个参数是服务启动的地址,第二个参数是 Handler,任何实现了 ServeHTTP 方法的对象都可以作为 HTTP 的 Handler。
在标准库中,http.Handler
接口的定义如下:
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
2 GeeCache HTTP 服务端
分布式缓存需要实现节点间通信,建立基于 HTTP
的通信机制是比较常见和简单的做法。如果一个节点启动了 HTTP
服务,那么这个节点就可以被其他节点访问。今天我们就为单机节点搭建 HTTP Server
。
不与其他部分耦合,我们将这部分代码放在新的 http.go
文件中,当前的代码结构如下:
geecache/
|--lru/
|--lru.go // lru 缓存淘汰策略
|--byteview.go // 缓存值的抽象与封装
|--cache.go // 并发控制
|--geecache.go // 负责与外部交互,控制缓存存储和获取的主流程
|--http.go // 提供被其他节点访问的能力(基于http)
首先我们创建一个结构体 HTTPPool
,作为承载节点间 HTTP
通信的核心数据结构(包括服务端和客户端,今天只实现服务端)。
day3-http-server/geecache/http.go
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,
}
}
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...))
}
// 该http服务实例完全接管所有的http请求,用于缓存的获取
func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 如果请求的路径前缀不是期望的basePath则报错
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)
// 约定路由请求格式为/<basepath>/<groupname>/<key>
parts := strings.SplitN(r.URL.Path[len(p.basePath)+1:], "/", 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())
}
ServeHTTP
的实现逻辑是比较简单的:
- 首先判断访问路径的前缀是否是
basePath
,不是就返回错误。 - 我们约定访问路径格式为
/<basepath>/<groupname>/<key>
,通过groupname
得到group
实例,再使用group.Get(key)
获取缓存数据。 - 最终使用
w.Write()
将缓存值作为httpResponse
的body
返回。
到这里,HTTP
服务端已经完整地实现了。接下来,我们将在单机上启动 HTTP
服务,使用 curl
进行测试。
3 测试
实现 main
函数,实例化 group
,并启动 HTTP
服务。
day3-http-server/main.go
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))
}
- 同样地,我们使用
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/Tom
630
$ curl http://localhost:9999/_geecache/scores/Tom
630
$ curl http://localhost:9999/_geecache/scores/kkk
kkk not exist
GeeCache
的日志输出如下:
可以看到Tom
请求了三次,第一次是回源函数获取的,后两次则命中了缓存
2024/07/20 18:05:13 geecache is running at localhost:9999
2024/07/20 18:05:19 [Server localhost:9999] GET /_geecache/scores/Tom
2024/07/20 18:05:19 [SlowDB] search key Tom
2024/07/20 18:05:30 [Server localhost:9999] GET /_geecache/scores/Tom
2024/07/20 18:05:30 [GeeCache] hit
2024/07/20 18:06:11 [Server localhost:9999] GET /_geecache/scores/Tom
2024/07/20 18:06:11 [GeeCache] hit
2024/07/20 18:06:25 [Server localhost:9999] GET /_geecache/scores/kkk
2024/07/20 18:06:25 [SlowDB] search key kkk
节点间的相互通信不仅需要 HTTP
服务端,还需要 HTTP
客户端,这就是我们下一步需要做的事情。
原文地址:https://geektutu.com/post/geecache-day3.html