[博客翻译]HTTP服务器如何计算内容长度?


原文地址:https://aarol.dev/posts/go-contentlength/


任何实现过简单 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!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!