每个 HTTP 要求大概都长这样:

GET /path HTTP/1.1\r\nHost: example.com\r\nAccept-Language: en\r\nAccept-Encoding: gzip\r\n\r\n

第一行是“要求行”,包含要求方法、路径和 HTTP 版本。
接下来的几行是要求头,每行用 CRLF(回车换行)符号结束。
末了,会有一个额外的 CRLF 标记头部的结束,接下来是体,可以包含你想发送的数据。

在 Go 措辞中,通过原生的 net/http 包可以很随意马虎实现一个 HTTP 做事器。

contentlengthphp懂得 HTTP 办事器若何盘算 ContentLength Node.js

创建一个大略的 HTTP 做事端和 TCP 客户端:

package mainimport ( "fmt" "net" "net/http" "time")func main() { // 设置 HTTP 做事器,相应路径 "/test" http.HandleFunc("/test", func(w http.ResponseWriter, r http.Request) { w.Write([]byte("Hello")) w.Write([]byte(" dhub!")) }) // 启动 HTTP 做事器在 2024 端口上 go func() { err := http.ListenAndServe("localhost:2024", nil) if err != nil { panic(err) } }() // 确保做事器已经启动,等待一段韶光再进行 TCP 连接 time.Sleep(1 time.Second) // 利用 TCP 连接到做事器 conn, err := net.Dial("tcp", "localhost:2024") if err != nil { panic(err) } defer conn.Close() // 确保连接关闭 // 发送 HTTP 要求 request := "GET /test HTTP/1.1\r\nHost: localhost\r\n\r\n" _, err = conn.Write([]byte(request)) if err != nil { panic(err) } // 读取相应 buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { panic(err) } // 打印相应 fmt.Println(string(buf[:n]))}

当运行这个程序(go run http.go)时,得到了如下的相应:

个中最有趣的是 Content-Length 头,它的值为 11,这恰好是 "Hello dhub!" 在 UTF-8 编码中的长度。

代码中,两次调用 w.Write() 方法,分别写入了 "Hello" 和 " dhub!",但是我们并没有显式调用函数去设置 Content-Length,它却仍旧涌如今了相应头中。

这是由于在 Go 的 net/http 包中,只要在调用 w.Write() 之前没有调用 w.WriteHeader(),状态码和头部都会自动天生。

HTTP 做事器如何知道 Content-Length?

HTTP 规定,头部信息必须在体之前发送。

那么在写入 "Hello" 之前,做事器是怎么知道相应的总长度的?

答案是:

如果相应足够小,可以直接一次性发送出去,做事器可以很轻松地皮算出它的长度;

如果相应较大,做事器就会分块传输。

为了演示这一点,可以修正了上面的代码,发送一个更大的相应数据:

func main() { http.HandleFunc("/test", func(w http.ResponseWriter, r http.Request) { w.Write([]byte("Hello")) w.Write([]byte(strings.Repeat("!", 3000))) // 发送 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) 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 个感叹号,这个相应超过了做事器的单个缓冲区大小,因此做事器会将相应分块传输。

运行结果如下:

可以看到,相应头中没有了 Content-Length,取而代之的是 Transfer-Encoding: chunked,表示做事器采取了分块传输编码。

相应的第一个块大小为 800(2048),接下来的块大小为 3bd(957)。

做事器无需知道全体相应的长度,而是将数据分成多个块,每个块的长度以十六进制表示。

这种分块传输办法许可做事器在处理大数据时不用将全体相应保存在内存中,非常高效。

它在 HTTP 1.1 中引入,如今险些所有的做事器和客户端都支持这种编码办法。

值得一提的是,HTTP/2 和 HTTP/3 引入了自己的流机制,因此不再利用这种分块传输。

总结

net/http 包虽然供应了一个大略的 API,但在背后有很多“邪术”在运行,包括自动的头部写入和内容类型检测等。

这些机制在大多数情形下可以帮我们免去繁琐的细节处理,但作为程序员,理解这些机制的底层事理,能够帮助我们在须要的时候进行更好的掌握和优化。

参考:https://aarol.dev/posts/go-contentlength/