背景
在研究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())
}