Golang中x/net/http2是否应该将头信息和数据包合并为一个?
Golang中x/net/http2是否应该将头信息和数据包合并为一个? 当发送大量小请求时,这里会出现性能损失。 那么是否应该合并头部和正文数据包,并且在写入头部后不立即刷新?
StreamID: streamID,
BlockFragment: chunk,
EndStream: endStream,
EndHeaders: endHeaders,
})
first = false
} else {
cc.fr.WriteContinuation(streamID, endHeaders, chunk)
}
}
cc.bw.Flush()
return cc.werr
}
// internal error values; they don't escape to callers
var (
// abort request body write; don't send cancel
errStopReqBodyWrite = errors.New("http2: aborting request body write")
// abort request body write, but send stream reset of cancel.
errStopReqBodyWriteAndCancel = errors.New("http2: canceling request")
更多关于Golang中x/net/http2是否应该将头信息和数据包合并为一个?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
非常感谢! 我为此做了一个测试。 我为此开了一个issue。
更多关于Golang中x/net/http2是否应该将头信息和数据包合并为一个?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
是的。Header 和 Body 具有不同的帧类型,并且我不想合并这些帧。我希望在写入头部后不立即刷新,这样,Linux 网络协议就不会将它们分成两个 TCP 数据包发送。
谢谢! 我认为这可能不是为了多路复用。
这里提到,多路复用是通过流实现的。每个HTTP请求/响应都与它自己的流相关联,彼此独立。
https://httpwg.org/specs/rfc7540.html#Overview
原文如下:
Multiplexing of requests is achieved by having each HTTP request/response exchange associated with its own stream (Section 5). Streams are largely independent of each other, so a blocked or stalled request or response does not prevent progress on other streams.
好的,也许它并不适用于多路复用。正如之前所说,我对HTTP2规范几乎一无所知。
但我(诚然基于猜测的)观点仍然成立。HTTP2严格区分不同的帧类型,这可能就是为什么 http2 包不合并头部和正文的原因。
// 代码示例:展示HTTP2帧处理
func handleFrame(frame http2.Frame) {
switch f := frame.(type) {
case *http2.HeadersFrame:
// 处理头部帧
case *http2.DataFrame:
// 处理数据帧
}
}
我对HTTP/2的内部机制完全不了解,但通过浏览网络,我得到的印象是HTTP/2总是将HEADERS帧与DATA帧分开,这可能是为了获得更好的多路复用能力。
RFC 7540 - 超文本传输协议版本2 (HTTP/2):
HTTP/2中的基本协议单元是帧(第4.1节)。每种帧类型都有不同的用途。例如,HEADERS和DATA帧构成了HTTP请求和响应的基础(第8.1节)
这样,Linux网络协议就不会将数据分开发送到两个TCP数据包中。
好奇:你测试过这个吗? 如果测试过,你看到了显著的性能提升吗?
根据我的基本理解,HTTP2帧被多路复用到TCP连接上,但对于网络栈如何管理TCP数据包并没有直接影响。
我的假设基于这个SO回答,特别是第一点和第二条评论:
- 要点在于,单个TCP连接可能包含来自许多不同HTTP/2流的帧,这些帧是交错排列的。与TCP数据包的关系在这里并不重要——TCP数据包由你的TCP栈重新组装成TCP流,不应影响你对HTTP/2的理解。
@laike9m:每个TCP“数据包”只包含TCP流的一部分。这些数据包与任何类型的消息都没有关系——因为TCP是单个流,而不是一系列消息。因此,一个应用层帧可能被打包在多个TCP数据包中,一个数据包可能包含多个应用层帧,等等。应用层帧与TCP数据包之间根本没有定义好的映射关系。
再次声明,我不是网络专家,但这些回答符合我对OSI模型及其网络栈各层之间关注点分离的理解。
所以,也许你不需要太担心刷新头部带来的性能损失。
在HTTP/2协议实现中,头部帧和数据帧的合并需要谨慎处理。HTTP/2规范要求头部帧必须在数据帧之前发送,但可以通过WriteHeaders方法的EndStream参数来控制是否立即结束流。对于大量小请求的场景,可以考虑以下优化方案:
// 示例:优化小请求的发送性能
func writeRequest(cc *http2ClientConn, req *http.Request, endStream bool) error {
streamID := cc.nextStreamID()
// 准备头部帧
hf := hpack.NewEncoder(&headerBuf)
// ... 编码头部 ...
// 如果请求体很小,可以尝试合并
var bodyData []byte
if req.Body != nil && endStream {
// 读取小请求体
bodyData, _ = io.ReadAll(req.Body)
req.Body.Close()
}
// 发送头部帧,根据情况设置EndStream
cc.fr.WriteHeaders(http2.HeadersFrameParam{
StreamID: streamID,
BlockFragment: headerBuf.Bytes(),
EndStream: len(bodyData) == 0 && endStream,
EndHeaders: true,
})
// 如果有小请求体,立即发送数据帧
if len(bodyData) > 0 {
cc.fr.WriteData(streamID, endStream, bodyData)
}
// 延迟刷新以提高批处理效果
if cc.bw.Available() < 1024 {
cc.bw.Flush()
}
return nil
}
对于x/net/http2包的实现,当前的设计选择是合理的:
- 协议合规性:HTTP/2规范要求头部帧必须首先发送,数据帧随后
- 流控制:分离头部和数据允许更好的流控制管理
- 优先级处理:头部帧可以携带优先级信息,需要优先处理
性能优化建议在应用层实现:
// 应用层批处理示例
type BatchedRequest struct {
Header http.Header
Body []byte
}
func sendBatchedRequests(cc *http2ClientConn, requests []BatchedRequest) error {
for _, req := range requests {
// 发送头部帧
cc.fr.WriteHeaders(http2.HeadersFrameParam{
StreamID: cc.nextStreamID(),
BlockFragment: encodeHeaders(req.Header),
EndStream: len(req.Body) == 0,
EndHeaders: true,
})
// 发送数据帧(如果有)
if len(req.Body) > 0 {
cc.fr.WriteData(streamID, true, req.Body)
}
}
// 批量刷新
cc.bw.Flush()
return cc.werr
}
当前的x/net/http2实现已经考虑了性能优化,通过缓冲和适当的刷新策略来减少系统调用。强制合并头部和数据包可能违反协议规范,并导致以下问题:
- 流重置处理复杂化
- 优先级信号延迟
- 流量控制窗口更新延迟
- 与中间件的兼容性问题
对于性能敏感的应用,建议:
- 使用连接池复用连接
- 实施请求批处理
- 调整
Flush阈值 - 监控实际性能指标进行调优

