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 表示二进制数据。
    • 将缓存数据写入响应体,返回给客户端。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/912754.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ARM死机(HardFault)调试技巧详解(栈回溯,不破坏现场)

目录 Keil调试技巧&#xff1a; 一.不破坏现场连接仿真器与进入debug 二.栈回溯 死机调试示例 J-Link调试方法 示例&#xff1a;空指针异常 不能连接烧录器或者读取内存怎么办&#xff1f; 在日常开发中&#xff0c;经常会遇到单片机卡死机等问题&#xff0c;经常很难定…

nodejs 020: React语法规则 props和state

props和state 在 React 中&#xff0c;props 和 state 是管理数据流的两种核心机制。理解它们之间的区别和用途是构建 React 应用程序的基础。 一、props 和 state的区别 特性propsstate定义方式由父组件传递给子组件的数据组件内部管理的本地数据是否可修改不可变&#xff…

【开源免费】基于SpringBoot+Vue.JS水果购物网站(JAVA毕业设计)

博主说明&#xff1a;本文项目编号 T 065 &#xff0c;文末自助获取源码 \color{red}{T065&#xff0c;文末自助获取源码} T065&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析…

python可视化进阶

引用&#xff1a; 首先需要安装 plotnine from plotnine import* import joypy数据可视化进阶操作 3.1 类别数据可视化 【例3-1】——绘制简单条形图 【代码框3-1】——绘制简单条形图 # 图3-1的绘制代码 import pandas as pd import matplotlib.pyplot as plt from cvxpy …

大模型入门自学资源汇总,很难找到比这还全的大模型学习资源总结了!

接触各种AI工具到现在也快两年了&#xff0c;今年和同学陆续做了一些AI应用的科普宣讲&#xff0c;在这过程中收集了不少自学资源&#xff0c;特地挑出一部分整理成以下的内容。 书籍 大模型应用开发极简入门&#xff1a;基于GPT-4和ChatGPT 首推今年年初出版的《大模型应用开…

为何选择Spring AI Alibaba开发智能客服平台?

0 前言 本文来看如何使用Spring AI Alibaba构建Agent应用。 1 需求 智能客服平台&#xff0c;可帮助用户完成机票预定、问题解答、机票改签、取消等动作&#xff0c;具体要求&#xff1a; 基于 AI 大模型与用户对话&#xff0c;理解用户自然语言表达的需求支持多轮连续对话…

Python学习从0到1 day27 第三阶段 Spark ② 数据计算Ⅰ

人总是会执着于失去的&#xff0c;而又不珍惜现在所拥有的 —— 24.11.9 一、map方法 PySpark的数据计算&#xff0c;都是基于RDD对象来进行的&#xff0c;采用依赖进行&#xff0c;RDD对象内置丰富的成员方法&#xff08;算子&#xff09; map算子 功能&#xff1a;map算子…

数据结构合并两个有序链表

数据结构 1.合并两个有序数组代码&#xff1a; 1.合并两个有序数组 这里我们可以创建一个新的对象作为合并后的新链表newHead&#xff0c;而NewHead.next就是我们要返回的头部的位置 在创建一个对象来获取nextHead.next下一个节点来作为我们新链表的起始位置防止我们的头部位置…

动态规划 —— dp 问题-买卖股票的最佳时机含手续费

1. 买卖股票的最佳时机含手续费 题目链接&#xff1a; 714. 买卖股票的最佳时机含手续费 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/ 2. 算法原理 状态表示&#xff1a;以某一个…

利用pythonstudio写的PDF、图片批量水印生成器,可同时为不同读者生成多组水印

现在很多场合需要将PDF或图片加水印&#xff0c;本程序利用pythonstudio编写。 第一步 界面 其中&#xff1a; LstMask:列表框 PopupMenu:PmnMark LstFiles:列表框 PopupMenu:PmnFiles OdFiles:文件选择器 Filter:PDF文件(.PDF)|.PDF|图像文件(.JPG)|.JPG|图像文件(.png…

基于python深度学习技术矩阵分解的推荐系统,通过学习隐含特征,实现推荐

实现了一个基于矩阵分解的推荐系统&#xff0c;用于预测用户对电影的评分。具体来说&#xff0c;该程序通过TensorFlow构建和训练一个模型&#xff0c;来学习用户和电影之间的隐含特征&#xff0c;并根据这些特征预测评分。以下是代码的主要功能和步骤的详细描述&#xff1a; …

[vulnhub] DarkHole: 1

https://www.vulnhub.com/entry/darkhole-1,724/ 端口扫描主机发现 探测存活主机&#xff0c;184是靶机 nmap -sP 192.168.75.0/24 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-08 09:59 CST Nmap scan report for 192.168.75.1 Host is up (0.00027s latency). MA…

4.1 WINDOWS XP,ReactOS对象与对象目录----1

系列文章目录 文章目录 系列文章目录4.1 对象与对象目录OBJECT_HEADERObpLookupEntryDirectory()NtCreateTimer() 4.1 对象与对象目录 “对象(Object)”这个词现在大家都已耳熟能详了&#xff0c;但是对象到底是什么呢?广义地说&#xff0c;对象就是“目标”&#xff0c;行为…

STM32H503开发(2)----STM32CubeProgrammer烧录

STM32H503开发----2.STM32CubeProgrammer烧录 概述硬件准备视频教学样品申请源码下载参考程序自举模式BOOT0设置UART烧录USB烧录 概述 STM32CubeProgrammer (STM32CubeProg) 是一款用于编程STM32产品的全功能多操作系统软件工具。 它提供了一个易用高效的环境&#xff0c;通过…

“双十一”电商狂欢进行时,在AI的加持下看网易云信IM、RTC如何助力商家!

作为一年一度的消费盛会&#xff0c;2024年“双十一”购物狂欢节早已拉开帷幕。蹲守直播间、在主播热情介绍中点开链接并加购&#xff0c;也已成为大多数人打开“双11”的重要方式。然而&#xff0c;在这火热的购物氛围背后&#xff0c;主播频频“翻车”、优质主播稀缺、客服响…

debian系统安装qt的时候 显示xcb相关文件缺失

如果是安装之后的问题 我们可以选择使用ldd的命令查看当前依赖的so那些文件确实 ldd /home/yinsir/Qt/5.15.2/gcc_64/plugins/platforms/libqxcb.so 本人在进行打包的时候 出现则会个报错 ERROR: ldd outputLine: “libxcb-util.so.1 > not found” ERROR: for binary: “/…

A023-基于SpringBoot的冷链物流系统的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…

【数据分析】如何构建指标体系?

有哪些指标体系搭建模型&#xff1f;五个步骤教你从0开始搭建指标体系 一、企业指标体系搭建存在什么问题 许多企业在搭建数据指标体系时遇到了诸多难题&#xff0c;如问题定位不准确、数据采集不完整、目标不一致、报表无序、指标覆盖不全面以及报表价值未充分利用等。 1、…

C++20 概念与约束(1)—— SFINAE

1、从模板说起 众所周知&#xff0c;C在使用模板时&#xff0c;如果有多个模板匹配&#xff0c;则编译器会选择最匹配的一个模板进行实例化&#xff0c;这也正是模板特化和偏特化的依据。 根据上面这张图中的现象&#xff0c;列举下面几个示例&#xff1a; 1、不存在模板的情况…

基于Spring Boot的在线装修管理系统的设计与实现,LW+源码+讲解

摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性差&#…