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();
    }
}

关键步骤:

  1. 等待 SETTINGS 帧完成握手
  2. 创建 FullHttpRequest 并写入 channel
  3. 通过 streamIdMap 关联请求与响应 Promise
  4. 阻塞等待响应完成并验证结果

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


原始标题:HTTP/2 in Netty