当前位置: 首页 > article >正文

7天用Go从零实现分布式缓存GeeCache(学习)

参考资料

前置知识

在 Go 的 HTTP 服务器开发中,ServeHTTP 方法的参数 w http.ResponseWriterr *http.Request 用于处理 HTTP 请求和构建响应。以下是它们的详细解释:

1. w http.ResponseWriter

  • w 是一个 http.ResponseWriter 类型,用于构建并发送 HTTP 响应。
  • 它提供了一组方法,用于向客户端写入响应数据或设置 HTTP 响应头。
  • 常见的操作包括:
    • 写入响应内容:使用 w.Write([]byte("response content")) 将字节内容写入响应。
    • 设置响应状态码:使用 w.WriteHeader(http.StatusCode),例如 w.WriteHeader(http.StatusNotFound) 设置状态为 404。
    • 设置响应头:通过 w.Header().Set("Content-Type", "application/json") 设置响应头,比如设置 Content-Typeapplication/json

2. r *http.Request

  • r 是一个指向 http.Request 的指针,包含了关于 HTTP 请求的所有信息。
  • http.Request 是一个结构体,包含许多字段和方法,帮助开发者获取请求的详细信息。
  • 常用字段和方法包括:
    • r.Method:获取请求方法,例如 GETPOST 等。
    • r.URL:包含请求的 URL 和路径信息,如 r.URL.Path 可以获取请求的路径部分。
    • r.Header:包含请求头信息,可以通过 r.Header.Get("Header-Name") 获取指定的请求头。
    • r.Body:包含请求的主体数据(通常用于 POST 请求),可以通过 io.ReadAll(r.Body) 读取内容。
    • r.Form:包含解析后的表单数据,适用于 POST 表单提交的数据。

示例说明

func (h *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 打印请求的路径
    log.Println("Request URL Path:", r.URL.Path)
    
    // 设置响应头
    w.Header().Set("Content-Type", "text/plain")
    
    // 写入响应内容
    w.Write([]byte("Hello World!"))
}

在这个示例中:

  • r.URL.Path 获取请求路径并打印出来。
  • 使用 w.Header().Set 设置 Content-Type 响应头为 text/plain
  • w.Write([]byte("Hello World!")) 向客户端写入响应内容 Hello World!

这样一来,w http.ResponseWriterr *http.Request 就构成了一个完整的 HTTP 请求-响应流程,服务器能够根据请求内容生成并返回相应的响应。

http.ListenAndServeServeHTTP 的隐式调用

http.ListenAndServe("localhost:9999", &s) 中,ServeHTTP 的调用是由 Go 的 HTTP 服务器框架隐式完成的。虽然代码中没有直接调用 ServeHTTP,但当服务器接收到 HTTP 请求时,Go 的 HTTP 服务器会自动调用 ServeHTTP 方法来处理该请求。这是通过接口机制实现的。以下是具体的过程:

1. http.Handler 接口

  • Go 的 net/http 包定义了一个 http.Handler 接口,其中只有一个方法:

    type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
    }
    
    • 任何实现了 ServeHTTP 方法的类型都满足 http.Handler 接口。
    • 在这个例子中,server 类型实现了 ServeHTTP 方法,因此 server 类型满足 http.Handler 接口。

2. ListenAndServe 函数

  • http.ListenAndServe 函数的签名如下:

    func ListenAndServe(addr string, handler Handler) error
    
    • 第二个参数是一个 Handler 接口(即 http.Handler)。
    • 传递给 ListenAndServe&sserver 类型的一个指针,而 server 类型实现了 ServeHTTP 方法,因此 &s 是一个有效的 http.Handler 实例。

3. 隐式调用 ServeHTTP

  • ListenAndServe 启动服务器并监听 localhost:9999 后,每当收到一个 HTTP 请求,它会检查传入的 handler(即 &s)并调用其 ServeHTTP 方法。
  • 这个调用过程是 Go 的 HTTP 服务器框架自动处理的,因此不需要在代码中显式调用 ServeHTTP

运行流程概述

  1. http.ListenAndServe("localhost:9999", &s) 启动服务器并监听端口 9999
  2. 当收到一个请求时,服务器会自动调用 &sServeHTTP 方法来处理该请求。
  3. ServeHTTP 中,使用 wr 参数构建响应。

因此,ServeHTTP 的调用是 http.ListenAndServe 函数在处理请求时自动完成的。

http 标准库

Go 语言提供了 http 标准库,可以方便地搭建 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)
}
  • ServeHTTPhttp.Handler 接口的一个方法,用于处理 HTTP 请求。
  • 参数 w http.ResponseWriter:用于构造 HTTP 响应。
  • 参数 r *http.Request:包含了 HTTP 请求的信息。
  • log.Println(r.URL.Path):将请求的 URL 路径记录到控制台。
  • w.Write([]byte("Hello World!")):向客户端返回一个简单的 Hello World! 字符串作为响应内容。

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

GeeCache HTTP服务器

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

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

const defaultBasePath = "/_geecache/"

type HTTPPool struct{
	self string
	basePath string
}

func NewHTTPPool(self string) *HTTPPool{
	return &HTTPPool{
		self: self,
		basePath: defaultBasePath,
	}
}
  • HTTPPool 只有 2 个参数,一个是 self,用来记录自己的地址,包括主机名/IP 和端口。
  • 另一个是 basePath,作为节点间通讯地址的前缀,默认是 /_geecache/,那么 http://example.com/_geecache/ 开头的请求,就用于节点间的访问。因为一个主机上还可能承载其他的服务,加一段 Path 是一个好习惯。比如,大部分网站的 API 接口,一般以 /api 作为前缀。
func (p *HTTPPool) Log(format string, v ...interface{}){
    // 自定义日志方法,使用特定的格式来输出日志信息
    // fmt.Sprintf(format, v...) 格式化输出,将所有参数 v 格式化成字符串
    log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...))
}

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)  // 如果路径不匹配,触发 panic
    }
    
    // 使用自定义的 Log 方法记录请求方法和路径
    p.Log("%s %s", r.Method, r.URL.Path)

    // 将路径去除 basePath 前缀部分后按 '/' 分割为两部分,以获取 <groupname> 和 <key>
    // 格式要求为 /<basepath>/<groupname>/<key>
    parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2)
    if len(parts) != 2 {
        // 如果分割后的部分长度不为 2,说明格式不正确,返回 400 错误
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }

    // 提取分割后的第一个部分作为 groupName,第二个部分作为 key
    groupName := parts[0]
    key := parts[1]

    // 获取 group 对象,使用 groupName 在缓存中查找对应的 Group
    group := GetGroup(groupName)
    if group == nil {
        // 如果没有找到对应的 group,返回 404 错误
        http.Error(w, "no such group: " + groupName, http.StatusNotFound)
        return
    }

    // 尝试在 group 中查找对应的 key 值,获取缓存数据
    view, err := group.Get(key)
    if err != nil {
        // 如果查找过程中出现错误,返回 500 错误
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // 设置响应头的内容类型为 "application/octet-stream"
    // 这表示响应的内容是二进制数据
    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 进行测试。

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,
	}
}

// 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)
	// /<basepath>/<groupname>/<key> 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. 常量定义

const defaultBasePath = "/_geecache/"
  • 定义了 defaultBasePath 常量,表示缓存服务的默认路径前缀,所有请求都应以该路径前缀开头(如 /_geecache/)。

2. 结构体定义:HTTPPool

type HTTPPool struct {
	self     string
	basePath string
}
  • HTTPPool 是一个结构体,代表一组 HTTP 节点组成的缓存池。它实现了 PeerPicker 接口,可以在集群中选择适当的节点。
  • 字段解释:
    • self string:表示当前节点的基本 URL,例如 https://example.net:8000
    • basePath string:当前节点路径的前缀。默认是 defaultBasePath,用于区分普通的 HTTP 请求和缓存服务的请求。

3. 构造函数:NewHTTPPool

func NewHTTPPool(self string) *HTTPPool {
	return &HTTPPool{
		self:     self,
		basePath: defaultBasePath,
	}
}
  • NewHTTPPool 是一个构造函数,用于初始化一个 HTTPPool 实例。
  • 接收 self string 参数作为当前节点的 URL,并将默认路径前缀 defaultBasePath 赋给 basePath
  • 返回值类型为指针 *HTTPPool,可以有效避免数据拷贝,便于共享该实例。

4. 方法:Log

func (p *HTTPPool) Log(format string, v ...interface{}) {
	log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...))
}
  • Log 是一个日志方法,用于记录日志信息,便于调试。
  • 接收 format string 和可变参数 v ...interface{},通过 fmt.Sprintf 格式化后输出到日志中。
  • 日志信息前包含 [Server <self>],用来标记日志属于哪个节点,方便排查分布式系统中的问题。

5. 方法:ServeHTTP

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)

	// 解析路径为 <groupname>/<key>
	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())
}
  • ServeHTTP 方法用于处理所有 HTTP 请求。它使 HTTPPool 结构体实现了 http.Handler 接口,这样它可以作为 HTTP 处理器。
方法流程
  1. 路径验证

    • 检查请求路径是否以 basePath 开头。如果不符合,则触发 panic,说明请求路径异常。
  2. 日志记录

    • 调用 p.Log 记录请求的 HTTP 方法和路径。
  3. 路径解析

    • 去除 basePath 前缀后,将路径按 / 分割成两部分,期望格式为 <groupname>/<key>
    • 如果解析失败(如路径格式不正确),返回 400 Bad Request 错误。
  4. 缓存分组获取

    • 使用 GetGroup(groupName) 获取指定的缓存分组 group
    • 如果找不到对应的缓存分组,返回 404 Not Found 错误。
  5. 获取缓存数据

    • 调用 group.Get(key) 获取指定 key 的缓存数据 view
    • 如果获取数据时出现错误,返回 500 Internal Server Error
  6. 返回数据

    • 设置响应头 Content-Typeapplication/octet-stream 表示二进制数据。
    • 将缓存数据写入响应体,返回给客户端。

http://www.kler.cn/a/386947.html

相关文章:

  • windows 11编译安装ffmpeg(包含ffplay)
  • ELK-Logstash配置
  • [vulnhub] DarkHole: 1
  • 大数据技术之Hadoop :我是恁爹
  • Linux——简单认识vim、gcc以及make/Makefile
  • 初始JavaEE篇 —— 文件操作与IO
  • Oracle OCP认证考试考点详解082系列11
  • 数据分析:16s差异分析DESeq2 | Corncob | MaAsLin2 | ALDEx2
  • Spring框架之模板方法模式 (Template Method Pattern)
  • 关于上采样&下采样
  • R语言实战——一些批量对地理数据进行操作的方法
  • 最新开源DCL-SLAM:一种用于机器人群体的分布式协作激光雷达 SLAM 框架
  • QT版发送邮件程序
  • qt QShortcut详解
  • Docker Compose V2 安装
  • 大数据时代的数据分析:策略、方法与实践
  • 区块链技术在数字版权管理中的应用
  • Python安装与配置
  • 多路转接之Reactor
  • 定长内存池设计
  • 模型训练中GPU利用率低?
  • 在openwrt上跑golang程序
  • 缓存淘汰策略:Redis中的内存管理艺术
  • 前端-懒加载
  • 提瓦特幸存者4
  • Linux - 信号