Golang http如何流式輸出

背景

在研究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())
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章