前言
在网上找了半天Netty发送HTTP2请求,各种资料写的要么不全,要么都不相关,还有好不容易找到最相关的,但是只有最核心的方法,缺少很多东西,类之间的引用也不正确。最后才在源码中发现真相。
该项目的逻辑大致是这样的。Netty发送ssl加密的http2请求到Nginx,Nginx负载均衡使用普通http请求转发至服务端处理。
关于为什么采用该架构,是因为项目的特殊性要求:
- 由于Netty发送请求那一侧代码逻辑复杂,项目较老,不能轻易做大的变动。所以要保持原来发送ssl加密的http2的请求的需求。
- 新的需求是要用Nginx来负载均衡接收到的http2请求。
- 至于Nginx到后端服务没有特殊需求。
如果是想SpringBoot项目接口HTTP2请求,可以参考其他人的文档,这里不再赘述。
注意
需要特别注意的是,我所使用的JDK版本是11。使用JDK1.8会出现不支持ALPN,至于如何解决可以查看最后的章节。
Netty发送请求。
如果想使用Netty发送HTTP2请求需要有两个HTTP2处理类,一个初始化类和一个客户端。关系如下:
完整代码(直接复制可以使用)
- 引入依赖
在maven项目中,需要引入如下依赖:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.93.Final</version>
</dependency>
- 客户端代码
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpScheme;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;
import java.util.concurrent.TimeUnit;
import static io.netty.buffer.Unpooled.wrappedBuffer;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
* HTTP2 客户端。
*/
public final class Http2Client {
static final boolean SSL = System.getProperty("ssl") != null;
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8080"));
static final String URL = System.getProperty("url", "/whatever");
static final String URL2 = System.getProperty("url2");
static final String URL2DATA = System.getProperty("url2data", "test data!");
public static void main(String[] args) throws Exception {
// 配置 SSL.
final SslContext sslCtx;
if (SSL) {
SslProvider provider = OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK;
sslCtx = SslContextBuilder.forClient()
.sslProvider(provider)
/* NOTE: the cipher filter may not include all ciphers required by the HTTP/2 specification.
* Please refer to the HTTP/2 specification for cipher requirements. */
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
Protocol.ALPN,
// NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers.
SelectorFailureBehavior.NO_ADVERTISE,
// ACCEPT is currently the only mode supported by both OpenSsl and JDK providers.
SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1))
.build();
} else {
sslCtx = null;
}
EventLoopGroup workerGroup = new NioEventLoopGroup();
Http2ClientInitializer initializer = new Http2ClientInitializer(sslCtx, Integer.MAX_VALUE);
try {
// Configure the client.
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.remoteAddress(HOST, PORT);
b.handler(initializer);
// Start the client.
Channel channel = b.connect().syncUninterruptibly().channel();
System.out.println("Connected to [" + HOST + ':' + PORT + ']');
// Wait for the HTTP/2 upgrade to occur.
Http2SettingsHandler http2SettingsHandler = initializer.settingsHandler();
http2SettingsHandler.awaitSettings(5, TimeUnit.SECONDS);
HttpResponseHandler responseHandler = initializer.responseHandler();
int streamId = 3;
HttpScheme scheme = SSL ? HttpScheme.HTTPS : HttpScheme.HTTP;
AsciiString hostName = new AsciiString(HOST + ':' + PORT);
System.err.println("Sending request(s)...");
// 第一个请求,该请求是GET。
if (URL != null) {
// Create a simple GET request.
FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, URL, Unpooled.EMPTY_BUFFER);
request.headers().add(HttpHeaderNames.HOST, hostName);
request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name());
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
responseHandler.put(streamId, channel.write(request), channel.newPromise());
streamId += 2;
}
// 第一个请求,该请求是POST
if (URL2 != null) {
// Create a simple POST request with a body.
FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, URL2,
wrappedBuffer(URL2DATA.getBytes(CharsetUtil.UTF_8)));
request.headers().add(HttpHeaderNames.HOST, hostName);
request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name());
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
responseHandler.put(streamId, channel.write(request), channel.newPromise());
}
channel.flush();
responseHandler.awaitResponses(5, TimeUnit.SECONDS);
System.out.println("Finished HTTP/2 request(s)");
// Wait until the connection is closed.
channel.close().syncUninterruptibly();
} finally {
workerGroup.shutdownGracefully();
}
}
}
- 客户端初始化代码
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpClientUpgradeHandler;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener;
import io.netty.handler.codec.http2.Http2ClientUpgradeCodec;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslContext;
import java.net.InetSocketAddress;
import static io.netty.handler.logging.LogLevel.INFO;
/**
* Configures the client pipeline to support HTTP/2 frames.
*/
public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> {
private static final Http2FrameLogger logger = new Http2FrameLogger(INFO, Http2ClientInitializer.class);
private final SslContext sslCtx;
private final int maxContentLength;
private HttpToHttp2ConnectionHandler connectionHandler;
private HttpResponseHandler responseHandler;
private Http2SettingsHandler settingsHandler;
public Http2ClientInitializer(SslContext sslCtx, int maxContentLength) {
this.sslCtx = sslCtx;
this.maxContentLength = maxContentLength;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
final Http2Connection connection = new DefaultHttp2Connection(false);
connectionHandler = new HttpToHttp2ConnectionHandlerBuilder()
.frameListener(new DelegatingDecompressorFrameListener(
connection,
new InboundHttp2ToHttpAdapterBuilder(connection)
.maxContentLength(maxContentLength)
.propagateSettings(true)
.build()))
.frameLogger(logger)
.connection(connection)
.build();
responseHandler = new HttpResponseHandler();
settingsHandler = new Http2SettingsHandler(ch.newPromise());
if (sslCtx != null) {
configureSsl(ch);
} else {
configureClearText(ch);
}
}
public HttpResponseHandler responseHandler() {
return responseHandler;
}
public Http2SettingsHandler settingsHandler() {
return settingsHandler;
}
protected void configureEndOfPipeline(ChannelPipeline pipeline) {
pipeline.addLast(settingsHandler, responseHandler);
}
/**
* Configure the pipeline for TLS NPN negotiation to HTTP/2.
*/
private void configureSsl(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// Specify Host in SSLContext New Handler to add TLS SNI Extension
pipeline.addLast(sslCtx.newHandler(ch.alloc(), Http2Client.HOST, Http2Client.PORT));
// We must wait for the handshake to finish and the protocol to be negotiated before configuring
// the HTTP/2 components of the pipeline.
pipeline.addLast(new ApplicationProtocolNegotiationHandler("") {
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
ChannelPipeline p = ctx.pipeline();
p.addLast(connectionHandler);
configureEndOfPipeline(p);
return;
}
ctx.close();
throw new IllegalStateException("unknown protocol: " + protocol);
}
});
}
/**
* Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2.
*/
private void configureClearText(SocketChannel ch) {
HttpClientCodec sourceCodec = new HttpClientCodec();
Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(connectionHandler);
HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536);
ch.pipeline().addLast(sourceCodec,
upgradeHandler,
new UpgradeRequestHandler(),
new UserEventLogger());
}
/**
* A handler that triggers the cleartext upgrade to HTTP/2 by sending an initial HTTP request.
*/
private final class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DefaultFullHttpRequest upgradeRequest =
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER);
// Set HOST header as the remote peer may require it.
InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress();
String hostString = remote.getHostString();
if (hostString == null) {
hostString = remote.getAddress().getHostAddress();
}
upgradeRequest.headers().set(HttpHeaderNames.HOST, hostString + ':' + remote.getPort());
ctx.writeAndFlush(upgradeRequest);
ctx.fireChannelActive();
// Done with this handler, remove it from the pipeline.
ctx.pipeline().remove(this);
configureEndOfPipeline(ctx.pipeline());
}
}
/**
* Class that logs any User Events triggered on this channel.
*/
private static class UserEventLogger extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("User Event Triggered: " + evt);
ctx.fireUserEventTriggered(evt);
}
}
}
- HTTP/2’s Settings 处理
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http2.Http2Settings;
import java.util.concurrent.TimeUnit;
/**
* Reads the first {@link Http2Settings} object and notifies a {@link io.netty.channel.ChannelPromise}
*/
public class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> {
private final ChannelPromise promise;
/**
* Create new instance
*
* @param promise Promise object used to notify when first settings are received
*/
public Http2SettingsHandler(ChannelPromise promise) {
this.promise = promise;
}
/**
* Wait for this handler to be added after the upgrade to HTTP/2, and for initial preface
* handshake to complete.
*
* @param timeout Time to wait
* @param unit {@link java.util.concurrent.TimeUnit} for {@code timeout}
* @throws Exception if timeout or other failure occurs
*/
public void awaitSettings(long timeout, TimeUnit unit) throws Exception {
if (!promise.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting for settings");
}
if (!promise.isSuccess()) {
throw new RuntimeException(promise.cause());
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception {
promise.setSuccess();
// Only care about the first settings message
ctx.pipeline().remove(this);
}
}
- HTTP2 响应处理
该类用于客户端读取服务器消息的处理逻辑。
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.PlatformDependent;
import java.util.AbstractMap.SimpleEntry;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
/**
* Process {@link io.netty.handler.codec.http.FullHttpResponse} translated from HTTP/2 frames
*/
public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
private final Map<Integer, Entry<ChannelFuture, ChannelPromise>> streamidPromiseMap;
public HttpResponseHandler() {
// Use a concurrent map because we add and iterate from the main thread (just for the purposes of the example),
// but Netty also does a get on the map when messages are received in a EventLoop thread.
streamidPromiseMap = PlatformDependent.newConcurrentHashMap();
}
/**
* Create an association between an anticipated response stream id and a {@link io.netty.channel.ChannelPromise}
*
* @param streamId The stream for which a response is expected
* @param writeFuture A future that represent the request write operation
* @param promise The promise object that will be used to wait/notify events
* @return The previous object associated with {@code streamId}
* @see HttpResponseHandler#awaitResponses(long, java.util.concurrent.TimeUnit)
*/
public Entry<ChannelFuture, ChannelPromise> put(int streamId, ChannelFuture writeFuture, ChannelPromise promise) {
return streamidPromiseMap.put(streamId, new SimpleEntry<ChannelFuture, ChannelPromise>(writeFuture, promise));
}
/**
* Wait (sequentially) for a time duration for each anticipated response
*
* @param timeout Value of time to wait for each response
* @param unit Units associated with {@code timeout}
* @see HttpResponseHandler#put(int, io.netty.channel.ChannelFuture, io.netty.channel.ChannelPromise)
*/
public void awaitResponses(long timeout, TimeUnit unit) {
Iterator<Entry<Integer, Entry<ChannelFuture, ChannelPromise>>> itr = streamidPromiseMap.entrySet().iterator();
while (itr.hasNext()) {
Entry<Integer, Entry<ChannelFuture, ChannelPromise>> entry = itr.next();
ChannelFuture writeFuture = entry.getValue().getKey();
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().getValue();
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());
}
System.out.println("---Stream id: " + entry.getKey() + " received---");
itr.remove();
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
if (streamId == null) {
System.err.println("HttpResponseHandler unexpected message received: " + msg);
return;
}
Entry<ChannelFuture, ChannelPromise> entry = streamidPromiseMap.get(streamId);
if (entry == null) {
System.err.println("Message received for unknown stream id " + streamId);
} else {
// Do stuff with the message (for now just print it)
ByteBuf content = msg.content();
if (content.isReadable()) {
int contentLength = content.readableBytes();
byte[] arr = new byte[contentLength];
content.readBytes(arr);
System.out.println(new String(arr, 0, contentLength, CharsetUtil.UTF_8));
}
entry.getValue().setSuccess();
}
}
}
- 启动项目
代码中配置项是从System
类中获取的,可以在项目启动时添加配置。如下是在idea中添加配置
在Java启动配置中增加的VM Option
选项,添加如下参数
-Dssl=true -Dhost=114.114.114.114 -Dport=8443 -Durl=/api/getMessage -Durl2=/send -Durl2data=123
参数含义如下:
- -Dssl=true : 启动ssl加密。
- -Dhost=114.114.114.114 : 发送请求的IP地址。
- -Dport=8443 : 发送请求的端口。
- -Durl=/api/getMessage : 第一个请求是GET请求。
- -Durl2=/send -Durl2data=123` : 第二个请求时POST请求,参数是123。
Nginx 接收http2请求并转发
Nginx的配置如下:
http {
include mime.types;
default_type application/octet-stream;
log_format main 'remote_addr:$remote_addr - remote_user:$remote_user [$time_local] request:$request '
'status:$status body_bytes_sent:$body_bytes_sent http_referer:$http_referer '
'request_time:$request_time request_body:$request_body '
'http_user_agent:$http_user_agent http_x_forward_for:$http_x_forwarded_for';
access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
gzip on;
server{
listen 端口 ssl http2;
server_name Nginx服务器IP;
ignore_invalid_headers on;
# 自签证书
ssl_certificate public.crt;
ssl_certificate_key private.key;
# 项目要求只能接收 TSLv1.2 协议的数据,所以这里只配置 V1.2。
ssl_protocols TLSv1.2;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_prefer_server_ciphers on;
# 转发请求
location / {
proxy_pass http://服务端IP:服务端端口;
}
}
注意点:
- 要修改监听的端口号,前面章节的请求发送到的端口是8443.
- 要修改
server_name
- 我们自己的项目要求http2所用的协议是TSLv1.2。其他协议未测试。
- 要配置转发的IP和port。
解决JDK1.8 兼容ALPN协议
在使用JDK11的时候,虽然不产应影响,但是会报如下的错误:
Failed to load any of the given libraries: [netty_tcnative_windows_x86_64, netty_tcnative_x86_64, netty_tcnative]
为了解决如下的问题,经过查阅文档和代码实践,引入了如下的JAR包。
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
</dependency>
在解决报错的同时竟意外解决了JDK1.8兼容ALPN协议的问题。只要引入上述依赖即可使用JDK1.8。