参考资料
前置知识
在 Go 的 HTTP 服务器开发中,ServeHTTP
方法的参数 w http.ResponseWriter
和 r *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-Type
为application/json
。
- 写入响应内容:使用
2. r *http.Request
r
是一个指向http.Request
的指针,包含了关于 HTTP 请求的所有信息。http.Request
是一个结构体,包含许多字段和方法,帮助开发者获取请求的详细信息。- 常用字段和方法包括:
r.Method
:获取请求方法,例如GET
、POST
等。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.ResponseWriter
和 r *http.Request
就构成了一个完整的 HTTP 请求-响应流程,服务器能够根据请求内容生成并返回相应的响应。
http.ListenAndServe
中 ServeHTTP
的隐式调用
在 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
的&s
是server
类型的一个指针,而server
类型实现了ServeHTTP
方法,因此&s
是一个有效的http.Handler
实例。
- 第二个参数是一个
3. 隐式调用 ServeHTTP
- 当
ListenAndServe
启动服务器并监听localhost:9999
后,每当收到一个 HTTP 请求,它会检查传入的handler
(即&s
)并调用其ServeHTTP
方法。 - 这个调用过程是 Go 的 HTTP 服务器框架自动处理的,因此不需要在代码中显式调用
ServeHTTP
。
运行流程概述
http.ListenAndServe("localhost:9999", &s)
启动服务器并监听端口9999
。- 当收到一个请求时,服务器会自动调用
&s
的ServeHTTP
方法来处理该请求。 - 在
ServeHTTP
中,使用w
和r
参数构建响应。
因此,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)
}
ServeHTTP
是http.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 处理器。
方法流程
-
路径验证:
- 检查请求路径是否以
basePath
开头。如果不符合,则触发 panic,说明请求路径异常。
- 检查请求路径是否以
-
日志记录:
- 调用
p.Log
记录请求的 HTTP 方法和路径。
- 调用
-
路径解析:
- 去除
basePath
前缀后,将路径按/
分割成两部分,期望格式为<groupname>/<key>
。 - 如果解析失败(如路径格式不正确),返回
400 Bad Request
错误。
- 去除
-
缓存分组获取:
- 使用
GetGroup(groupName)
获取指定的缓存分组group
。 - 如果找不到对应的缓存分组,返回
404 Not Found
错误。
- 使用
-
获取缓存数据:
- 调用
group.Get(key)
获取指定key
的缓存数据view
。 - 如果获取数据时出现错误,返回
500 Internal Server Error
。
- 调用
-
返回数据:
- 设置响应头
Content-Type
为application/octet-stream
表示二进制数据。 - 将缓存数据写入响应体,返回给客户端。
- 设置响应头