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