任何实现过简单 HTTP 1.1 服务器的人都会告诉你,它其实是一个非常简单的协议。基本上,它是一个文本文件,有一些特定的规则使其更容易解析。
所有 HTTP 请求看起来都是这样的:
GET /path HTTP/1.1\r\n
Host: aarol.dev\r\n
Accept-Language: en,fi-FI\r\n
Accept-Encoding: gzip, deflate\r\n
\r\n
第一行是“请求行”,包含请求的方法、路径和 HTTP 版本。接下来的行是头部,每行以“回车换行符”字符终止。还有一个额外的 CRLF 来标记头部分的结束。之后,消息体可以包含你想发送的任何数据。
下面是一个使用原始 TCP 套接字作为客户端的简单 Go 程序来说明这一点:
func main() {
// 设置一个 HTTP 服务器来响应路径 "/test"
http.HandleFunc("GET /test", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello"))
w.Write([]byte(" world!"))
})
go http.ListenAndServe("localhost:2024", nil)
// 使用 TCP 连接到服务器
conn, err := net.Dial("tcp", "localhost:2024")
if err != nil {
panic(err)
}
// 写入 HTTP 请求(没有正文,只有请求行和 Host 头)
_, err = conn.Write([]byte("GET /test HTTP/1.1\r\nHost: localhost\r\n\r\n"))
if err != nil {
panic(err)
}
buf := make([]byte, 1024) // 1 kb
// 读取响应
n, err := conn.Read(buf)
if err != nil {
panic(err)
}
fmt.Println(string(buf[:n]))
}
当我尝试时,得到了以下响应:
HTTP/1.1 200 OK
Date: Sun, 06 Oct 2024 14:51:13 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8
Hello world!
关于这一点有一个有趣之处,即 Content-Length
头部其值为 12。这正好是 "Hello world!"
在 UTF-8 编码中的长度。在上面的 Go 代码中,写响应分为两个部分:首先我们写 "Hello"
,然后写 " world!"
。请注意,我们无需调用任何函数来写入头部,但它们仍然存在于响应中。在 Go 的 http 包中,如果在调用 w.Write()
之前没有调用 w.WriteHeader()
,则状态码 200 和头部将自动写入。此后的任何 w.WriteHeader()
调用都会无用,并且输出警告。
请记住,在 HTTP 中头部始终在正文之前写入。在没有将 “Hello” 写入连接之前,服务器已经知道了响应的长度是如何可能的?如果我想写一个 “Hello”,然后再写一千个感叹号怎么办?或者一百万个呢?服务器是否需要在发送之前知道每个响应的长度?这意味着每个响应都需要在整个处理器期间保留在内存中。
我想弄清楚这个问题,而且并不需要深入研究太多。我在标准库 net/http/client.go
文件中找到了这条 精彩评论。基本上,这条评论说如果响应足够小以适应单个“分块缓冲区”,则可以很容易地计算其长度,并一次性发送。如果响应大于缓冲区,则分段发送。在实践中意味着什么?我已经修改了上面的代码来展示这一点:
func main() {
http.HandleFunc("GET /test", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello"))
w.Write([]byte(strings.Repeat("!", 3000)))
})
go http.ListenAndServe("localhost:2024", nil)
// 省略所有错误处理以使代码简洁
conn, _ := net.Dial("tcp", "localhost:2024")
conn.Write([]byte("GET /test HTTP/1.1\r\nHost: localhost\r\n\r\n"))
buf := make([]byte, 1024) // 1 kb
for {
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
n, err := conn.Read(buf)
if err != nil {
break
}
fmt.Println(string(buf[:n]))
}
}
处理程序现在返回 “Hello!!!!!!!!!!! …” 加上 3000 个感叹号。这大于配置的分块大小,因此我们可以看到会发生什么。这是响应内容:
HTTP/1.1 200 OK
Date: Sun, 06 Oct 2024 16:43:28 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
800
Hello!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!