背景
在研究chatgpt的時候發現,別人的輸出效果是一字一句進行輸出的,進行抓包的話發現他是一步一步進行輸出,這就引起我的好奇,我們知道http是無狀態交互,就是你問我,我回答,這樣一步步進行,那是如何進行流式輸出的呢?
http是以請求(request)和響應(response)爲主體的,一個請求對應一個響應。而在後端服務器方,後端代碼就是收到前端傳過來的request,然後給出相應的response。這個response有很多種形式,有可能是返回一些數據庫中的數據,可能是返回二次處理後的數據,也有可能僅僅返回一個”操作成功“(背後藏着對數據庫操作的行爲)。
一般write
我們知道go封裝了http對象,我們寫完數據之後就會返回。
那這部分已經是封裝好的了,我們是怎麼手動觸發的呢?
http write:
// either dataB or dataS is non-zero.
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
if w.conn.hijacked() {
if lenData > 0 {
caller := relevantCaller()
w.conn.server.logf("http: response.Write on hijacked connection from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
}
return 0, ErrHijacked
}
if w.canWriteContinue.isSet() {
// Body reader wants to write 100 Continue but hasn't yet.
// Tell it not to. The store must be done while holding the lock
// because the lock makes sure that there is not an active write
// this very moment.
w.writeContinueMu.Lock()
w.canWriteContinue.setFalse()
w.writeContinueMu.Unlock()
}
if !w.wroteHeader {
w.WriteHeader(StatusOK)
}
if lenData == 0 {
return 0, nil
}
if !w.bodyAllowed() {
return 0, ErrBodyNotAllowed
}
w.written += int64(lenData) // ignoring errors, for errorKludge
if w.contentLength != -1 && w.written > w.contentLength {
return 0, ErrContentLength
}
if dataB != nil {
return w.w.Write(dataB)
} else {
return w.w.WriteString(dataS)
}
}
我們看http的write方法只是將數據寫到buff裏,並沒有進行flush,也就和我們之前理解的一樣,需要一定時機纔會flush掉
response的w *bufio.Writer
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
const (
defaultBufSize = 4096 //默認緩衝區大小
)
// Flush writes any buffered data to the underlying io.Writer.
func (b *Writer) Flush() error {
if b.err != nil {
return b.err
}
if b.n == 0 {
return nil
}
n, err := b.wr.Write(b.buf[0:b.n])
if n < b.n && err == nil {
err = io.ErrShortWrite
}
if err != nil {
if n > 0 && n < b.n {
copy(b.buf[0:b.n-n], b.buf[n:b.n])
}
b.n -= n
b.err = err
return err
}
b.n = 0
return nil
}
func (b *Writer) Write(p []byte) (nn int, err error) {
for len(p) > b.Available() && b.err == nil {
var n int
if b.Buffered() == 0 {
// Large write, empty buffer.
// Write directly from p to avoid copy.
n, b.err = b.wr.Write(p)
} else {
n = copy(b.buf[b.n:], p)
b.n += n
b.Flush()
// flush時機爲數據大於緩衝區 && 之前已經緩衝過數據了(我們知道http輸出一般都是一次進行),因此邏輯都會是上面的b.Buffered() == 0部分
}
nn += n
p = p[n:]
}
if b.err != nil {
return nn, b.err
}
n := copy(b.buf[b.n:], p)
b.n += n
nn += n
return nn, nil
}
我們看到源碼裏有Flush方法,那很明顯我們可以取出來手動flush
解決
簡單的代碼展示,可以封裝下解決
writer := ctx.Response().Writer()
flush, flushOk := writer.(http.Flusher)
if flushOk {
_, _ = fmt.Fprintf(writer, "%s", string(bts))
if i < len(runes)-1 {
_, _ = fmt.Fprint(writer, "\n")
}
flush.Flush() // 手動flush
} else {
bf.Write(bts)
if i < len(runes)-1 {
bf.WriteByte('\n')
}
}
if !flushOk {
ctx.Response().Writer().Write(bf.Bytes())
}