引言
Netty自带了许多ChannelHandler来简化开发,当在开发时可以直接使用自带的handler来简化开发,避免重复造轮子,减少程序BUG。
本章用于介绍与https、http2有关的ChannelHandler
HTTPS
SslContext
SslContext是netty自带的用于实现SSL协议的handler
SslContext sslCtx = SslContextBuilder.forServer(
// 加载证书
this.getClass().getClassLoader().getResourceAsStream("ssl/server.crt"),
// 加载私钥
this.getClass().getClassLoader().getResourceAsStream("ssl/server.key"),
// 加载密码,没有则为null
null
).build();
// .....
channel.pipeline().addLast(sslCtx.newHandler(channel.alloc()));
在使用 HTTPS 时要生成证书
我们可以使用openssl来生成crt证书win
// 先使用rsa算法生成私钥 .key openssl genrsa -out server.key 1024 // 然后根据私钥生成证书申请文件 .csr openssl req -new -key server.key -out server.csr // 根据key与csr生成自签名证书 .crt openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650生成私钥:私钥是整个过程的基础,用于加密和解密数据。
生成证书请求(CSR):CSR 包含了你的身份信息和公钥(由私钥派生)。这个文件将用于向证书颁发机构(CA)申请证书。
生成自签名证书:自签名证书使用私钥对 CSR 中的信息进行签名防伪造,生成一个有效的证书。
HTTP2
Http2具有二进制分帧,多路复用,头部压缩、服务器推送等功能,其中服务器必须具有解码二进制分帧、头部压缩的功能,并支持多路复用。
SSL/TLS + HTTP2 (h2)
HTTP2在默认是要求使用SSL/TLS加密的,所以必须先配置配置SSL。又因为HTTP2在默认是要求使用SSL,所以这种HTTP2实现方式也比较简单
ALPN
应用层协议协商(Application-Layer Protocol Negotiation,简称 ALPN)是由 RFC 7301 定义的一个 TLS 扩展,用于在协商加密连接时识别应用层协议,避免了额外的往返通讯开销。
由于HTTPS与H2都默认使用443端口,且如果客户端并不了解服务器所支持的协议就有可能产生问题。比如,如果服务器只支持 H2,但客户端发送的是 HTTPS 的数据,就会导致通信失败,为了保证双方可以获知应用层将使用的协议,TLS引入了ALPN功能
在使用ALPN时,客户端与服务器可以在SSL/TLS握手阶段协商应用层将使用的协议。
+------+ +------+
|client| |server|
+------+ +------+
| |
+--Client Hello:我支持Http1.1与Http2,你选择那个?-->|
| |
|<--Server Hello:http2不错,我选择http2------------- +
| |
客户端会在握手时发送一个列表,代表自己支持的协议;服务器会从中选择一个自己想用的协议并返回;此时双方都了解了接下来要使用什么协议通信。
ApplicationProtocolConfig
此类用于为SSL配置一个应用层协议协商器,共有四个需要配置的入参
public ApplicationProtocolConfig(Protocol protocol, SelectorFailureBehavior selectorBehavior,
SelectedListenerFailureBehavior selectedBehavior, String... supportedProtocols) {
this(protocol, selectorBehavior, selectedBehavior, toList(supportedProtocols));
}
Protocol
用于选择使用什么来解决协商,目前netty支持NPN,ALPN与NPN_AND_ALPN。
SelectorFailureBehavior
用于定义失败策略(服务器时),没有找到匹配的协议时触发,目前netty支持FATAL_ALERT(握手失败并警告),NO_ADVERTISE(假装不支持TLS扩展)、CHOOSE_MY_LAST_PROTOCOL(选择列表最后一个协议)
SelectedListenerFailureBehavior
用于定义失败时策略(客户端时),ACCEPT(如果不匹配或不支持TLS扩展,握手继续,并假定接受应用层协议)
FATAL_ALERT(如果不匹配或不支持TLS扩展,握手失败,发出警告),CHOOSE_MY_LAST_PROTOCOL(如果不匹配或不支持TLS扩展,握手继续,选择列表最后一个协议)
supportedProtocols
支持那些协议协商,目前netty支持h2 http/1.1 spdy/3.1 spdy/3 spdy/2 spdy/1
ApplicationProtocolNegotiationHandler
ApplicationProtocolNegotiationHandler 是 Netty 中用于处理 TLS 协商结果的一个重要组件。通过这个Handler,我们可以获取到客户端和服务器协商后的应用层协议(如 h2 或 http/1.1),并根据协商结果采取相应的行动。
主要使用方式即为实现configurePipeline方法
// 其中 protocol 为协商结果
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {}
Http2MultiplexHandler
Http2MultiplexHandler是用于实现H2多路复用功能的Handler,使得Netty具有处理H2多路复用的能力,负责将接收到的数据帧分发到相应的子Channel,并将子Channel的响应合并到主Channel,可以同时处理输入与输出
// 常用构造函数
public Http2MultiplexHandler(ChannelHandler inboundStreamHandler) {
this.inboundStreamHandler = ObjectUtil.checkNotNull(inboundStreamHandler, "inboundStreamHandler");
}
Http2FrameCodec
由于HTTP2具有二进制分帧与头部压缩的特点,netty提供了Http2FrameCodec来实现此支持;而且,此类还负责HTTP2的连接管理,可以同时处理输入与输出
该类需要使用构造器来创建 Http2FrameCodecBuilder.forServer().build()
明文HTTP2 (h2c)
HTTP2虽然默认要求使用SSL安全连接,但依然允许不使用SSL来运行,这种类型的HTTP2被称为h2c,但这种方式局限性很大,
主流浏览器并不支持h2c,也就意味着使用h2c就必须自己开发客户端。
与HTTPS、H2都运行在443端口的相同,HTTP与H2C都运行在80端口,所以同样需要一种机制来告知所使用的协议;H2C通过一种称为“协议升级”的方式来实现协商
// req
GET /index HTTP/1.1
Host: xxxxx.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: (SETTINGS payload)
// res
HTTP/1.1 101 Switching Protocols
Connection: Upgrad
Upgrade: h2c
(... HTTP/2 response ...)
根据上面的情况我们可以知道这种方式非常鸡肋,引入“协议升级”机制是为了区分
http1.1与h2c,但h2c目前只能运行在自己的客户端上,此时使用那种协议是可以人为规定的,如果想在自己的客户端上使用h2c,完全可以直接进行h2c通信,避免“协议升级”浪费带宽。
demo代码
H2
public class H2Initializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
SslContext sslCtx = SslContextBuilder.forServer(
this.getClass().getClassLoader().getResourceAsStream("ssl/server.crt"),
this.getClass().getClassLoader().getResourceAsStream("ssl/server.key"),
null
).applicationProtocolConfig(
new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
// 在前面的优先级更高
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1
)
).build();
ApplicationProtocolNegotiationHandler protocolNegotiationHandler = new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) {
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
switch (protocol) {
// 如果是H2
case ApplicationProtocolNames.HTTP_2: {
ctx.pipeline().addLast(Http2FrameCodecBuilder.forServer().build());
ctx.pipeline().addLast(new Http2MultiplexHandler(new Http2Handler()));
break;
}
// 如果是Http/1.1
case ApplicationProtocolNames.HTTP_1_1: {
ctx.pipeline().addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(1024 * 1024))
.addLast(new HttpHandler());
}
}
}
};
// 添加ChandlerHandler
ch.pipeline()
.addLast(sslCtx.newHandler(ch.alloc()))
.addLast(protocolNegotiationHandler);
}
}
@Slf4j
@ChannelHandler.Sharable
public class Http2Handler extends SimpleChannelInboundHandler<Http2StreamFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2StreamFrame obj) throws Exception {
log.info("Http2Handler channelRead0...");
if (obj instanceof Http2HeadersFrame) {
Http2HeadersFrame headersFrame = (Http2HeadersFrame) obj;
Http2Headers headers = headersFrame.headers();
// 如果访问路径为根目录
log.info("path:{}", headers.path());
String path = String.valueOf(headers.path());
if (path.equals("/")) {
sendIndexHtml(ctx);
} else if (path.equals("/hello")) {
sendHelloWorld(ctx);
}
}
}
private void sendHelloWorld(ChannelHandlerContext ctx) throws IOException {
byte[] content;
try (InputStream resource = this.getClass().getClassLoader().getResourceAsStream("hello.html")) {
assert resource != null;
content = new byte[resource.available()];
resource.read(content);
}
// 封装http2头帧
Http2Headers headers = new DefaultHttp2Headers()
.status(HttpResponseStatus.OK.codeAsText())
.set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8")
.set(HttpHeaderNames.CONTENT_LENGTH, "" + content.length)
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.write(new DefaultHttp2HeadersFrame(headers, false));
// 封装http2数据帧
Http2DataFrame dataFrame = new DefaultHttp2DataFrame(ctx.alloc().buffer().writeBytes(content), true);
ctx.write(dataFrame).addListener(ChannelFutureListener.CLOSE);
}
private void sendIndexHtml(ChannelHandlerContext ctx) throws IOException {
byte[] content;
try (InputStream resource = this.getClass().getClassLoader().getResourceAsStream("index.html")) {
assert resource != null;
content = new byte[resource.available()];
resource.read(content);
}
// 封装http2头帧
Http2Headers headers = new DefaultHttp2Headers()
.status(HttpResponseStatus.OK.codeAsText())
.set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8")
.set(HttpHeaderNames.CONTENT_LENGTH, "" + content.length)
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.write(new DefaultHttp2HeadersFrame(headers, false));
// 封装http2数据帧
Http2DataFrame dataFrame = new DefaultHttp2DataFrame(ctx.alloc().buffer().writeBytes(content), true);
ctx.write(dataFrame).addListener(ChannelFutureListener.CLOSE);
}
}