为什么 Golang http.ResponseWriter 执行被延迟?

Why is Golang http.ResponseWriter execution being delayed?

我试图在收到请求后立即发送页面响应,然后进行处理,但我发现响应没有发送出去 "first",即使它在代码中排在第一位 sequence.In 现实生活中我有一个用于上传 excel sheet 的页面,它被保存到数据库中,这需要时间(50,0000 多行)并且想更新用户进度。这是一个简化的例子; (取决于你有多少 RAM,你可能需要添加几个零来计数器才能看到结果)

package main

import (
    "fmt"
    "net/http"
)

func writeAndCount(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Starting to count"))

    for i := 0; i < 1000000; i++ {

        if i%1000 == 0 {
            fmt.Println(i)
        }
    }
    w.Write([]byte("Finished counting"))

}

func main() {
    http.HandleFunc("/", writeAndCount)
    http.ListenAndServe(":8080", nil)

}

您可以检查 ResponseWriter 是否是 http.Flusher,如果是,则强制刷新到网络:

if f, ok := w.(http.Flusher); ok {
    f.Flush()
}

但是,请记住这是一个非常非常规的 HTTP 处理程序。将进度消息像终端一样流式传输到响应会带来一些问题,尤其是当客户端是 Web 浏览器时。

您可能需要考虑一些更符合 HTTP 性质的东西,例如立即返回 202 Accepted 响应,并带有一个唯一标识符,客户端可以使用它来检查使用后续调用的处理状态你的 API.

HTTP protocol 的原始概念是一个简单的 request-response server-client 计算模型。没有流媒体或 "continuous" 客户端更新支持。如果需要某种信息,首先联系服务器的始终是客户端。

此外,由于大多数网络服务器缓存响应直到它完全准备好(或达到某个限制——通常是缓冲区大小),您写入(发送)到客户端的数据不会立即传输。

有几种技术 "developed" 可以解决这个问题 "limitation" 以便服务器能够通知客户端有关更改或进度的信息,例如 HTTP 长轮询、HTTP 流、HTTP/2 服务器推送或 Websockets。您可以在这个答案中阅读更多关于这些的信息:Is there a real server push over http?

所以要实现你想要的,你必须绕过HTTP协议的原始"borders"。

如果你想定期发送数据,或者流式传输数据到客户端,你必须告诉服务器。最简单的方法是检查 http.ResponseWriter handed to you implements the http.Flusher interface (using a type assertion) 是否存在,如果存在,则调用其 Flusher.Flush() 方法会将任何缓冲数据发送到客户端。

使用http.Flusher只是解决方案的一半。由于这是 HTTP 协议的 non-standard 用法,通常还需要客户端支持才能正确处理。

首先,您必须通过设置 ContentType=text/event-stream 响应 header.

让客户知道响应的 "streaming" 性质

接下来,为避免客户端缓存响应,请务必同时设置 Cache-Control=no-cache

最后,让客户端知道您可能不会将响应作为单个单元发送(而是作为定期更新或作为流),这样客户端应该保持连接并等待进一步的数据, 设置 Connection=keep-alive 响应 header.

如上设置响应header后,您就可以开始漫长的工作了,每当您想向客户端更新进度时,写入一些数据并调用Flusher.Flush()

让我们看一个可以完成所有操作的简单示例 "right":

func longHandler(w http.ResponseWriter, r *http.Request) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Server does not support Flusher!",
            http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    start := time.Now()
    for rows, max := 0, 50*1000; rows < max; {
        time.Sleep(time.Second) // Simulating work...
        rows += 10 * 1000
        fmt.Fprintf(w, "Rows done: %d (%d%%), elapsed: %v\n",
            rows, rows*100/max, time.Since(start).Truncate(time.Millisecond))
        flusher.Flush()
    }
}

func main() {
    http.HandleFunc("/long", longHandler)
    panic(http.ListenAndServe("localhost:8080", nil))
}

现在,如果您在浏览器中打开 http://localhost:8080/long,您将每秒看到一个输出 "growing":

Rows done: 10000 (20%), elapsed: 1s
Rows done: 20000 (40%), elapsed: 2s
Rows done: 30000 (60%), elapsed: 3s
Rows done: 40000 (80%), elapsed: 4.001s
Rows done: 50000 (100%), elapsed: 5.001s

另请注意,在使用 SSE 时,您应该 "pack" 更新到 SSE 帧中,也就是说您应该以 "data:" 前缀开始它们,并以 2 个换行符结束每个帧:"\n\n".

"Literature" 和进一步阅读/教程

详细了解 Server-sent events on Wikipedia

看到一个Golang HTML5 SSE example.

参见 Golang SSE server example with client codes using it

Server-Sent Events - One Way Messaging 上查看 w3school.com 的教程。