1. 概述
Netty 是一个基于 NIO 的高性能客户端-服务端框架,赋予 Java 开发者直接操作网络协议栈的能力。借助 Netty,开发者不仅能实现标准协议(如 HTTP、WebSocket),还能构建自定义协议。
如果你对 Netty 基础还不熟悉,可以先阅读我们的 Netty 入门指南。
本文重点聚焦:✅ 如何使用 Netty 构建一个完整的 HTTP/2 服务端与客户端。
我们不会停留在“Hello World”级别的简单示例,而是深入 Netty 对 HTTP/2 帧(frame)的处理机制,帮助你在实际项目中避开常见坑点。
2. 什么是 HTTP/2?
顾名思义,HTTP/2 是超文本传输协议的第二版,相比 1997 年发布的 HTTP/1.1,直到 2015 年才正式标准化。
虽然目前 HTTP/3 已逐步推广,但主流浏览器和服务器的默认支持仍以 HTTP/2 为主。
HTTP/2 的核心优势在于:
- ✅ 多路复用(Multiplexing):单个 TCP 连接上可并行传输多个请求/响应,彻底解决 HTTP/1.x 的队头阻塞问题
- ✅ 服务器推送(Server Push):服务端可主动向客户端推送资源,减少往返延迟
- ✅ 二进制帧结构:通信基于二进制帧(frame),而非文本,解析更高效
在 Netty 中,HTTP/2 的通信由一系列帧构成:
HEADERS
帧:携带请求/响应头DATA
帧:携带请求体或响应体SETTINGS
帧:用于协商连接参数(如最大并发流数)
接下来的代码示例中,我们会看到 Netty 如何处理这些关键帧类型。
3. HTTP/2 服务端实现
本节将手把手搭建一个支持 HTTPS 的 HTTP/2 服务端。
3.1. 构建 SslContext
Netty 通过 ALPN(Application-Layer Protocol Negotiation)在 TLS 层协商使用 HTTP/2。因此,服务端必须配置 SslContext
。
SelfSignedCertificate ssc = new SelfSignedCertificate();
SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.sslProvider(SslProvider.JDK)
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.applicationProtocolConfig(
new ApplicationProtocolConfig(Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE,
SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2))
.build();
关键点说明:
- 使用
SelfSignedCertificate
生成自签名证书(生产环境应使用可信 CA 证书) sslProvider(SslProvider.JDK)
指定使用 JDK 原生 SSL 实现(也可用 OpenSSL)applicationProtocolConfig
明确只支持h2
协议标识,拒绝降级到 HTTP/1.1
⚠️ 踩坑提示:若客户端不支持 ALPN(如旧版 JDK),协商会失败。建议生产环境优先使用 OpenSSL 提供商以获得更好性能。
3.2. 服务端启动与 ChannelInitializer
服务端引导流程与传统 Netty 服务类似,核心在于通过 ChannelInitializer
构建正确的 pipeline。
public final class Http2Server {
static final int PORT = 8443;
public static void main(String[] args) throws Exception {
SslContext sslCtx = // 上述创建的 SslContext
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024);
b.group(group)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
if (sslCtx != null) {
ch.pipeline()
.addLast(sslCtx.newHandler(ch.alloc()), Http2Util.getServerAPNHandler());
}
}
});
Channel ch = b.bind(PORT).sync().channel();
logger.info("HTTP/2 Server is listening on https://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
其中 Http2Util.getServerAPNHandler()
是关键,它返回一个 ApplicationProtocolNegotiationHandler
,负责在 ALPN 协商成功后初始化 HTTP/2 处理链:
public static ApplicationProtocolNegotiationHandler getServerAPNHandler() {
return new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) {
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
ctx.pipeline().addLast(
Http2FrameCodecBuilder.forServer().build(),
new Http2ServerResponseHandler());
return;
}
throw new IllegalStateException("Protocol: " + protocol + " not supported");
}
};
}
协商成功后,向 pipeline 添加:
Http2FrameCodec
:Netty 提供的 HTTP/2 帧编解码器Http2ServerResponseHandler
:自定义业务处理器
3.3. 响应处理器实现
Http2ServerResponseHandler
继承 ChannelDuplexHandler
,处理入站请求并生成响应。
先定义一个静态响应内容:
static final ByteBuf RESPONSE_BYTES = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8));
在 channelRead
中处理 HEADERS
帧并返回响应:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Http2HeadersFrame) {
Http2HeadersFrame msgHeader = (Http2HeadersFrame) msg;
if (msgHeader.isEndStream()) {
ByteBuf content = ctx.alloc().buffer();
content.writeBytes(RESPONSE_BYTES.duplicate());
Http2Headers headers = new DefaultHttp2Headers().status(HttpResponseStatus.OK.codeAsText());
ctx.write(new DefaultHttp2HeadersFrame(headers).stream(msgHeader.stream()));
ctx.write(new DefaultHttp2DataFrame(content, true).stream(msgHeader.stream()));
}
} else {
super.channelRead(ctx, msg);
}
}
逻辑说明:
- 接收到
HEADERS
帧且isEndStream()
为真,表示请求头结束,可立即响应 - 先写入
HEADERS
帧(含状态码),再写入DATA
帧(含响应体),并标记endStream=true
- 所有帧必须通过
.stream(streamId)
关联到同一 stream
✅ 测试验证:
curl -k -v --http2 https://127.0.0.1:8443
预期输出:
> GET / HTTP/2
> Host: 127.0.0.1:8443
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 4294967295)!
< HTTP/2 200
<
* Connection #0 to host 127.0.0.1 left intact
Hello World* Closing connection 0
4. HTTP/2 客户端实现
客户端需主动发起请求,并处理服务端返回的帧流。整体结构包含:
SslContext
配置- 若干自定义处理器
ChannelInitializer
组装 pipeline- 启动逻辑与请求发送
4.1. 客户端 SslContext
客户端 SslContext
配置与服务端类似,但需注意信任策略:
@Before
public void setup() throws Exception {
SslContext sslCtx = SslContextBuilder.forClient()
.sslProvider(SslProvider.JDK)
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.applicationProtocolConfig(
new ApplicationProtocolConfig(Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE,
SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2))
.build();
}
⚠️ 重要提醒:InsecureTrustManagerFactory.INSTANCE
会信任所有证书,仅用于测试环境。生产环境应使用可信 CA 证书或自定义 X509TrustManager
。
4.2. 核心处理器
✅ Http2SettingsHandler
用于等待服务端的 SETTINGS
帧,标志连接初始化完成。
public class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> {
private final ChannelPromise promise;
public Http2SettingsHandler(ChannelPromise promise) {
this.promise = promise;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception {
promise.setSuccess();
ctx.pipeline().remove(this); // 一次性处理器,处理完移除
}
public void awaitSettings(long timeout, TimeUnit unit) throws Exception {
if (!promise.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting for settings");
}
}
}
✅ Http2ClientResponseHandler
处理服务端返回的响应数据,按 stream ID 管理异步结果。
public class Http2ClientResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
private final Map<Integer, MapValues> streamidMap = new ConcurrentHashMap<>();
public static class MapValues {
ChannelFuture writeFuture;
ChannelPromise promise;
String response;
public MapValues(ChannelFuture writeFuture, ChannelPromise promise) {
this.writeFuture = writeFuture;
this.promise = promise;
}
// getter 省略
}
public MapValues put(int streamId, ChannelFuture writeFuture, ChannelPromise promise) {
return streamidMap.put(streamId, new MapValues(writeFuture, promise));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
if (streamId == null) {
logger.error("Unexpected message received: " + msg);
return;
}
MapValues value = streamidMap.get(streamId);
if (value == null) {
logger.error("Message received for unknown stream id " + streamId);
} else {
ByteBuf content = msg.content();
if (content.isReadable()) {
int contentLength = content.readableBytes();
byte[] arr = new byte[contentLength];
content.readBytes(arr);
value.response = new String(arr, 0, contentLength, CharsetUtil.UTF_8);
}
value.getPromise().setSuccess(); // 标记响应处理完成
}
}
public String awaitResponses(long timeout, TimeUnit unit) {
Iterator<Entry<Integer, MapValues>> itr = streamidMap.entrySet().iterator();
String response = null;
while (itr.hasNext()) {
Entry<Integer, MapValues> entry = itr.next();
ChannelFuture writeFuture = entry.getValue().getWriteFuture();
if (!writeFuture.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting to write for stream id " + entry.getKey());
}
if (!writeFuture.isSuccess()) {
throw new RuntimeException(writeFuture.cause());
}
ChannelPromise promise = entry.getValue().getPromise();
if (!promise.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting for response on stream id " + entry.getKey());
}
if (!promise.isSuccess()) {
throw new RuntimeException(promise.cause());
}
logger.info("---Stream id: " + entry.getKey() + " received---");
response = entry.getValue().getResponse();
itr.remove();
}
return response;
}
}
4.3. Http2ClientInitializer
组装客户端 pipeline,核心是 HttpToHttp2ConnectionHandler
,它能将 FullHttpRequest
自动转换为 HTTP/2 帧流。
public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> {
private final SslContext sslCtx;
private final int maxContentLength;
private Http2SettingsHandler settingsHandler;
private Http2ClientResponseHandler responseHandler;
private final String host;
private final int port;
public Http2ClientInitializer(SslContext sslCtx, int maxContentLength, String host, int port) {
this.sslCtx = sslCtx;
this.maxContentLength = maxContentLength;
this.host = host;
this.port = port;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
settingsHandler = new Http2SettingsHandler(ch.newPromise());
responseHandler = new Http2ClientResponseHandler();
if (sslCtx != null) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(sslCtx.newHandler(ch.alloc(), host, port));
pipeline.addLast(Http2Util.getClientAPNHandler(maxContentLength, settingsHandler, responseHandler));
}
}
// getter 省略
}
getClientAPNHandler
创建 ALPN 处理器,并在协商成功后添加 HttpToHttp2ConnectionHandler
和自定义处理器:
public static ApplicationProtocolNegotiationHandler getClientAPNHandler(
int maxContentLength, Http2SettingsHandler settingsHandler, Http2ClientResponseHandler responseHandler) {
final Http2FrameLogger logger = new Http2FrameLogger(INFO, Http2ClientInitializer.class);
final Http2Connection connection = new DefaultHttp2Connection(false);
HttpToHttp2ConnectionHandler connectionHandler =
new HttpToHttp2ConnectionHandlerBuilder().frameListener(
new DelegatingDecompressorFrameListener(connection,
new InboundHttp2ToHttpAdapterBuilder(connection)
.maxContentLength(maxContentLength)
.propagateSettings(true)
.build()))
.frameLogger(logger)
.connection(connection)
.build();
return new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) {
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
ChannelPipeline p = ctx.pipeline();
p.addLast(connectionHandler);
p.addLast(settingsHandler, responseHandler);
return;
}
ctx.close();
throw new IllegalStateException("Protocol: " + protocol + " not supported");
}
};
}
4.4. 客户端启动与请求发送
使用 JUnit 测试类整合所有组件:
@Test
public void whenRequestSent_thenHelloWorldReceived() throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
Http2ClientInitializer initializer = new Http2ClientInitializer(sslCtx, Integer.MAX_VALUE, "127.0.0.1", 8443);
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.remoteAddress("127.0.0.1", 8443);
b.handler(initializer);
Channel channel = b.connect().syncUninterruptibly().channel();
logger.info("Connected to [127.0.0.1:8443]");
Http2SettingsHandler http2SettingsHandler = initializer.getSettingsHandler();
http2SettingsHandler.awaitSettings(60, TimeUnit.SECONDS);
logger.info("Sending request(s)...");
FullHttpRequest request = Http2Util.createGetRequest("127.0.0.1", 8443);
Http2ClientResponseHandler responseHandler = initializer.getResponseHandler();
int streamId = 3;
responseHandler.put(streamId, channel.write(request), channel.newPromise());
channel.flush();
String response = responseHandler.awaitResponses(60, TimeUnit.SECONDS);
assertEquals("Hello World", response);
logger.info("Finished HTTP/2 request(s)");
} finally {
workerGroup.shutdownGracefully();
}
}
关键步骤:
- 等待
SETTINGS
帧完成握手 - 创建
FullHttpRequest
并写入 channel - 通过
streamIdMap
关联请求与响应 Promise - 阻塞等待响应完成并验证结果
5. 总结
本文通过一个完整的示例,展示了如何使用 Netty 实现 HTTP/2 服务端与客户端:
- ✅ 基于 ALPN 的 TLS 握手与协议协商
- ✅
Http2FrameCodec
处理二进制帧通信 - ✅
HttpToHttp2ConnectionHandler
简化高层 API 调用 - ✅ 自定义处理器管理 stream 生命周期
虽然 Netty 的 HTTP/2 API 相对底层,但提供了极高的灵活性。随着 Netty 社区持续优化,未来有望看到更简洁的高层封装。
所有源码已托管至 GitHub:https://github.com/eugenp/tutorials/tree/master/server-modules/netty