HTTP/2协议引入的二进制分帧层让同一个TCP连接可以同时承载多条独立的数据流,客户端在一个连接上交错发送请求,服务端也能乱序返回响应。这种多路复用能力显著缓解了HTTP/1.1中前端请求排队的问题,但Ruby标准库的Net::HTTP对HTTP/2支持有限,无法完全发挥该优势。http-2 gem是一个纯Ruby实现的HTTP/2协议库,提供帧编解码、流状态管理和事件回调,适合构建需要主动发起HTTP/2请求的客户端工具。本文将围绕这个gem,展示如何在Ruby中完成连接建立、ALPN协商、并发请求发送以及响应处理。

HTTP/2多路复用与http-2 gem的定位
HTTP/2把通信单元拆成帧,每个帧属于某个流,流由唯一的整数标识。客户端发起的流使用奇数ID,服务端推送使用偶数ID。多路复用意味着多个流的帧可以在连接上交错传输,不需要像HTTP/1.1那样为每个请求单独建立连接或串行等待。对于Ruby这类动态语言,直接解析二进制帧需要小心处理字节序和变长整数,http-2 gem正好封装了这些复杂逻辑。
需要注意的是,http-2 gem并不负责套接字管理、TLS握手或事件循环。它只维护协议状态机:输入字节流后产生事件,调用者需要把要发送的帧写回套接字。这样的设计一方面带来灵活性,可以集成进任意IO框架;另一方面也提高了使用门槛。相比之下,像Net::HTTP这样的高层库更适合普通HTTP请求,而http-2 gem适合需要多路复用、服务端推送或协议级测试的场景。
引入该库非常简单,在Gemfile中添加依赖即可。安装后通过require加载HTTP/2模块,后续代码统一使用HTTP2::Client类。
# Gemfile source 'https://rubygems.org' gem 'http-2'
建立连接与TLS ALPN协商
HTTP/2在互联网上几乎总与TLS一起使用,因为主流浏览器只支持基于TLS的h2。客户端需要先在TLS握手中通过ALPN扩展声明自己支持h2协议,如果服务端也支持h2,协商结果才会是h2,否则可能回退到http/1.1。Ruby的OpenSSL绑定提供了alpn_protocols属性,可以在创建SSLContext时设置。
下面代码创建一个TCP连接,将其包装为SSL连接,并检查协商后的协议。实际地址使用测试域名ipipp.com,读者可替换为自己的服务端。连接建立后,需要创建HTTP2::Client实例,并开始处理套接字读写。
require 'socket'
require 'openssl'
require 'http/2'
tcp = TCPSocket.new('ipipp.com', 443)
ctx = OpenSSL::SSL::SSLContext.new
ctx.alpn_protocols = ['h2']
ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
ssl.hostname = 'ipipp.com'
ssl.connect
raise 'ALPN negotiation failed' unless ssl.alpn_protocol == 'h2'
conn = HTTP2::Client.new
buffer = ''.dup
loop do
data = ssl.read_nonblock(65536, exception: false)
break if data == :wait_readable && buffer.empty?
# processing happens in next step
end
这段代码只完成了连接前置工作,还不能收发数据。HTTP/2协议要求连接建立后双方交换SETTINGS帧,因此客户端必须不断读取SSLSocket中的字节并喂给conn对象,同时把conn产生的输出帧写回socket。如果忽略这一步,后续创建流时数据不会真正发送。
并发发起请求与事件处理
创建请求的核心是conn.new_stream。每个流对象可以注册多个事件,包括headers、data和close。headers在收到响应头时触发,data在收到响应体分片时触发,close在流结束时触发。发送请求时调用stream.headers并设置end_stream为true,表示请求体已经结束。
多个流可以连续创建,不需要等待前一个流完成。这正是多路复用的体现:所有请求共享同一个连接,只是在协议层用不同的流ID区分。下面定义一个send_request方法,用不同路径发起三个并发请求。
def send_request(conn, path)
stream = conn.new_stream
stream.on(:headers) do |headers|
puts "响应头: #{headers[':status']}"
end
stream.on(:data) do |chunk|
print chunk
end
stream.on(:close) do
puts "\n流 #{stream.id} 完成"
end
stream.headers({
':method' => 'GET',
':path' => path,
':scheme' => 'https',
':authority' => 'ipipp.com'
}, end_stream: true)
end
def pump(conn, ssl)
loop do
data = ssl.read_nonblock(65536, exception: false)
case data
when :wait_readable
outgoing = conn.data_to_send
ssl.write(outgoing) unless outgoing.empty?
sleep 0.01
when :wait_writable
sleep 0.01
when nil
break
else
conn << data
outgoing = conn.data_to_send
ssl.write(outgoing) unless outgoing.empty?
end
end
end
send_request(conn, '/first')
send_request(conn, '/second')
send_request(conn, '/third')
pump(conn, ssl)
在pump方法中,读取使用了nonblock模式并处理:wait_readable、:wait_writable和nil三种情况。当读到数据时,调用conn << data将字节送入协议解析器;解析器可能会产生新的待发送数据,通过conn.data_to_send获取并写回SSL套接字。如果暂时没有可读数据,就写入待发送数据并短暂休眠,避免忙等。
这种手动事件循环虽然比不上异步框架优雅,但足够透明。每个响应帧可以携带不同流的ID,客户端根据事件回调区分数据来源。读者会看到三个响应的输出交错打印,直观展示多路复用效果。
流控窗口与常见错误处理
HTTP/2的流控机制作用于每条流和整个连接,接收方通过窗口大小告知发送方还能发送多少数据。客户端在接收响应时,如果应用层处理速度跟不上,就需要及时发送WINDOW_UPDATE帧扩大窗口。http-2 gem会在窗口接近耗尽时触发window_update事件,调用者可以打印日志或执行增量调整。
conn.on(:window_update) do |stream_id|
puts "窗口更新,流ID: #{stream_id}"
end
另一方面,服务端可能发送GOAWAY帧通知连接即将关闭,此时需要停止新建流。http-2 gem同样提供goaway事件。实际使用中还要注意流ID冲突、SETTINGS超时、自身心跳等细节。如果连接被关闭,客户端应重新协商并重试尚未完成的请求,这需要业务层维护幂等性。
使用该gem的常见误区是把所有请求塞进同一连接而不处理背压,导致内存中积压过多未读数据。虽然HTTP/2解决了队头阻塞,但应用层仍需要根据业务吞吐量控制并发流数量,可以在创建流之前检查conn.active_stream_count,避免超过服务端SETTINGS_MAX_CONCURRENT_STREAMS限制。
总体而言,http-2 gem适合Ruby生态中需要精细协议控制的场景,例如内部RPC网关、压力测试工具或自定义推送客户端。对于普通业务调用,使用基于该gem的高层封装会更省心,但理解底层帧流通机制依然有助于排查协议相关问题。
HTTP/2http-2 gemRuby多路复用修改时间:2026-08-26 14:18:15