Keep-Alive 模式

在 Content-Length 和 Transfer Encoding 之前,先要理解 Keep-Alive 模式,这就要提到http1.0协议的缺点。

http1.0协议的主要缺点就是,每个TCP连接只能发一个请求, 每次数据发送完毕,连接就关闭。 如果还要请求其他资源,就必须新建连接。

因为三次握手、慢启动等,TCP连接新建的成本很高。 随着web的发展,需要请求的资源越来越多,这个问题就越来越突出。

在http1.0时代,为了解决这个问题, 有些浏览器在请求时会在请求头里加上一个自定义字段:

Connection: keep-alive

这个字段就是告诉服务器不要关闭TCP连接,以便其他请求复用,但是,这还不是标准字段。

1997年1月, http1.1版本发布,引入了持久连接,即默认情况下TCP连接不关闭,可以被复用,也就是默认 keep-alive 。同时,支持通过 Connection:close 来通知关闭TCP连接。

所以实际上,http1.0协议下客户端和服务端可以通过 Connection: keep-alive 告知对方发送完数据后不要关闭TCP连接,而http1.1协议下默认就是 Connection: keep-alive 持久连接。

示例:

服务端:

[shell]# vim server.go

package main

import (
    "io"
    "log"
    "net/http"
)

func indexServer(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hei !\n")
}
func main() {

    http.HandleFunc("/index", indexServer)
    log.Fatal(http.ListenAndServe(":8888", nil))

}

[shell]# go run server.go

http1.1协议默认情况下就加了Connection: keep-alive,会持久连接,如下面三次请求复用同一个TCP连接,而且在等待一段时间没有新请求的之后,关闭连接。

GET /index HTTP/1.1
Host    192.168.56.101:8888
Cache-Control    max-age=0
Upgrade-Insecure-Requests    1
User-Agent    Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Accept    text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding    gzip, deflate
Accept-Language    zh-CN,zh;q=0.9,en;q=0.8
Connection    keep-alive

图上本地端口始终是 51411:

http01.png

当 Connection: close 时,数据发送完毕会断开TCP连接,每次请求都需要新建连接。

GET /index HTTP/1.1
Host    192.168.56.101:8888
Cache-Control    max-age=0
Upgrade-Insecure-Requests    1
User-Agent    Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Accept    text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding    gzip, deflate
Accept-Language    zh-CN,zh;q=0.9,en;q=0.8
Connection    close

http02.png

管道机制(Pipelining)

http1.0还有一个问题是当客户端复用TCP连接发送请求时, 要先发送一个请求,等到服务器响应之后,再发出一个请求,这也会造成效率低。

http1.1为了解决这个问题引入了管道机制(Pipelining),管道机制允许客户端同时发出多个请求,然后服务端按照顺序响应。

Content-Length

在非持久连接下可以可以通过连接关闭得知数据已经发送完毕, 在持久连接下显然不行。http1.1开始, 一个TCP连接可以发出多个请求,并传送多个返回, 这时就需要区分数据包是哪一个请求返回的。有一个方法就是通过 Content-Length 字段, 请求双方可以通过

Content-Length: 6

告知对方响应数据字节数,比如Content-Length:6,6个字节后的数据就属于下一个请求。

http03.png

Transfer Encoding Chunked

接着上面提到的持久连接下如何区分数据包边界, 在客户端请求的是一个静态页面或者一张图片时,服务端可以明确的计算出Content-Length返回给客户端。

但是当请求的内容是需要耗时计算的动态数据时,服务端不可能预先知道数据包大小,这时服务端就可以通过 Transfer Encoding:chunked 的方式来传输数据。

chunked 编码将数据分成一个个数据块发送, 每个数据块之前会有一个16进制的数值表示这个数据块的长度。最后是一个小为0的块,表示数据发送完毕。

服务端代码调整

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

func indexServer(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Connection", "Keep-Alive")
    w.Header().Set("Transfer-Encoding", "chunked")
    w.Header().Set("X-Content-Type-Options", "nosniff")

    ticker := time.NewTicker(time.Second)
    go func() {
        for t := range ticker.C {
            io.WriteString(w, "Chunk")
            fmt.Println("Tick at", t)
        }
    }()
    time.Sleep(time.Second * 5)
    ticker.Stop()
    fmt.Println("Finished: should return Content-Length: 0 here")
    w.Header().Set("Content-Length", "0")
}
func main() {

    http.HandleFunc("/index", indexServer)
    log.Fatal(http.ListenAndServe(":8888", nil))

}

客户端:

http04.png

标签: none

添加新评论