作为一个通讯框架,通讯数据的安全性也是不可或缺的一部分。一般常见的像TLS/SSL这样的安全协议我们都应该熟悉。 我们在访问安全网站时都遇到过这些协议,但是它们也可用于其他不是基于HTTP的应用程序,如安全SMTP(SMTPS)邮件服务器甚至是关系型数据库系统。
像Java就提供了javax.net.ssl 包来支持SSL/TLS,它的 SSLContext 和 SSLEngine类使得实现解密和加密相当简单直接。Netty 则是通过一个名为 SslHandler 的 ChannelHandler实现利用了这个 API,其中 SslHandler 在内部使用 SSLEngine 来完成实际的工作。
Netty 的 OpenSSL/SSLEngine 实现:
Netty 还提供了使用 OpenSSL工具包(www.openssl.org)的 SSLEngine 实现。这个 OpenSslEngine 类提供了比 JDK 提供的SSLEngine 实现更好的性能。如果OpenSSL库可用,可以将Netty应用程序(客户端和服务器)配置为默认使用OpenSslEngine。 如果不可用,Netty将会回退到 JDK 实现。 注意,无论你使用 JDK 的 SSLEngine 还是使用 Netty 的 OpenSslEngine,SSL API 和数据流都 是一致的。
下面这张图展示了SslHandler 进行解密和加密数据流:
下面我们使用ChannelInitializer来将SslHandler添加到ChannelPipeline 中,ChannelInitializer在channel注册好时设置ChannelPipeline 我们之前说过,忘记可以看看前面的内容。
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import javax.net.ssl.SSLEngine;
/**
* Author: lhd
* Data: 2023/6/11
* Annotate: Netty 添加SSL/TLS支持
*/
public class SslChannelInitializer extends ChannelInitializer<Channel> {
private final SslContext context;
private final boolean startTls;
//传入要使用的SslContext,startTls如果设置为 true,第一个写入的消息将不会被加密(客户端应该设置为 true)
public SslChannelInitializer(SslContext context, boolean startTls) {
this.context = context;
this.startTls = startTls;
}
@Override
protected void initChannel(Channel ch) throws Exception {
//对于每个 SslHandler 实例,都使用 Channel 的 ByteBufAllocator 从 SslContext 获取一个新的 SSLEngine
SSLEngine engine = context.newEngine(ch.alloc());
//将 SslHandler 作为第一个ChannelHandler 添加到 ChannelPipeline 中
ch.pipeline().addFirst("ssl", new SslHandler(engine, startTls));
}
}
SslHandler同样包含其他的方法,例如:在握手阶段,两个节点将相互验证并且商定一种加密方式。我们可以配置 SslHandler 来修改它的行为,或者在 SSL/TLS握手一旦完成之后提供通知,握手阶段完成之后,所有的数据都将会被加密。SSL/TLS 握手将会被自动执行。
下面是一些SslHandler的方法:
方 法 名 称 | 描 述 |
---|---|
setHandshakeTimeout (long,TimeUnit);setHandshakeTimeoutMillis (long);getHandshakeTimeoutMillis() | 设置和获取超时时间,超时之后,握手ChannelFuture 将会被通知失败 |
setCloseNotifyTimeout (long,TimeUnit);setCloseNotifyTimeoutMillis (long);getCloseNotifyTimeoutMillis() | 设置和获取超时时间,超时之后,将会触发一个关闭通知并关闭连接。这也将会导致通知该 ChannelFuture 失败 |
handshakeFuture() | 返回一个在握手完成后将会得到通知的ChannelFuture。如果握手先前已经执行过了,则返回一个包含了先前的握手结果的 ChannelFuture |
close();close(ChannelPromise);close(ChannelHandlerContext,ChannelPromise) | 发送 close_notify 以请求关闭并销毁 |
底层的 SslEngine |
HTTP/HTTPS大部分同学都不会陌生,它是我们常用的协议之一。我们熟悉的另一个协议 WebService API 一般也是基于HTTP/HTTPS的。
下面我们使用Netty 提供的 ChannelHandler,来处理 HTTP 和 HTTPS协议。
HTTP 是基于请求/响应模式的:客户端向服务器发送一个 HTTP 请求,然后服务器将会返回一个 HTTP 响应。Netty 提供了多种编码器和解码器简化了对这个协议的使用。
我们先来看看如生产和消费HTTP请求以及HTTP响应的方法:
一个 HTTP 请求/响应可能由多个数据部分组成,并且它总是以一个 LastHttpContent部分作为结束。FullHttpRequest 和 FullHttpResponse 消息是特殊的子类型,分别代表了完整的请求和响应。所有类型的 HTTP 消息都实现了 HttpObject 接口。
那么HTTP的编码器和解码器都包含哪些方法呢?
名 称 | 描 述 |
---|---|
HttpRequestEncoder | 将HttpRequest、HttpContent 和 LastHttpContent 消息编码为字节 |
HttpResponseEncoder | 将HttpResponse、HttpContent 和LastHttpContent 消息编码为字节 |
HttpRequestDecoder | 将字节解码为HttpRequest、HttpContent 和 LastHttpContent 消息 |
HttpResponseDecoder | 将字节解码为HttpResponse、HttpContent 和LastHttpContent 消息 |
了解了HTTP协议后,我们将它添加到我们的应用程序中:
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpRequestEncoder;
import io.netty.handler.codec.http.HttpResponseDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
/**
* Author: lhd
* Data: 2023/6/11
* Annotate: 添加HTTP协议支持
*/
public class HttpPipelineInitializer extends ChannelInitializer<Channel> {
private final boolean client;
public HttpPipelineInitializer(boolean client) {
this.client = client;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (client) {
//如果是客户端,则添加HttpResponseDecoder 以处理来自服务器的响应
pipeline.addLast("decoder", new HttpResponseDecoder());
//如果是客户端,则添加 HttpRequestEncoder以向服务器发送请求
pipeline.addLast("encoder", new HttpRequestEncoder());
} else {
//如果是服务器,则添加 HttpResponseEncoder以向客户端发送响应
pipeline.addLast("decoder", new HttpRequestDecoder());
//如果是服务器,则添加 HttpRequestDecoder以接收来自客户端的请求
pipeline.addLast("encoder", new HttpResponseEncoder());
}
}
}
为什么要聚合HTTP消息?
ChannelInitializer 将 ChannelHandler 安装到 ChannelPipeline中之后,便可以处理不同类型的 HttpObject 消息了。但是由于 HTTP
的请求和响应可能由许多部分组成,因此需要聚合它们以形成完整的消息。
Neey是如何做的?
Netty 提供了一个聚合器,它可以将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息。通过这样的方式,我们将看到完整的消息内容。由于消息分段需要被缓冲,直到可以转发一个完整的消息给下一个 ChannelInboundHandler,所以这个操作有轻微的开销。
下面展示一下这种操作是如何进行的:
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
/**
* Author: lhd
* Data: 2023/6/11
* Annotate: HTTP消息聚合
*/
public class HttpAggregatorInitializer extends ChannelInitializer<Channel> {
private final boolean isClient;
public HttpAggregatorInitializer(boolean isClient) {
this.isClient = isClient;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (isClient) {
//如果是客户端,则添加 HttpClientCodec
pipeline.addLast("codec", new HttpClientCodec());
} else {
//如果是服务器,则添加 HttpServerCodec
pipeline.addLast("codec", new HttpServerCodec());
}
//将最大的消息大小为 512 KB的 HttpObjectAggregator 添加到 ChannelPipeline
pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024));
}
}
HTTP为什么要压缩?不是已经聚合了么?
当使用 HTTP 时,压缩可以尽可能的多地减小传输数据的大小。虽然压缩会带来一些 CPU时钟周期上的开销,但是通常来说它都是一个好主意,特别是对于文本数据来说。
Netty 为压缩和解压缩提供了 ChannelHandler 实现,它们同时支持 gzip 和 deflate 编 码。
HTTP 请求的头部信息
客户端可以通过提供以下头部信息来指示服务器它所支持的压缩格式:
GET /encrypted-area HTTP/1.1
Host: www.example.com
Accept-Encoding: gzip, deflate
然而,需要注意的是,服务器没有义务压缩它所发送的数据。
下面展示一下如何自动的压缩HTTP消息:
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentCompressor;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpServerCodec;
/**
* Author: lhd
* Data: 2023/6/11
* Annotate: HTTP消息压缩
*/
public class HttpCompressionInitializer extends ChannelInitializer<Channel> {
private final boolean isClient;
public HttpCompressionInitializer(boolean isClient) {
this.isClient = isClient;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (isClient) {
//如果是客户端,则添加 HttpClientCodec
pipeline.addLast("codec", new HttpClientCodec());
//如果是客户端,则添加HttpContentDecompressor 以处理来自服务器的压缩内容
pipeline.addLast("decompressor", new HttpContentDecompressor());
} else {
//如果是服务器,则添加 HttpServerCodec
pipeline.addLast("codec", new HttpServerCodec());
//如果是服务器,则添加HttpContentCompressor来压缩数据(如果客户端支持它)
pipeline.addLast("compressor", new HttpContentCompressor());
}
}
}
压缩及其依赖
如果你正在使用的是 JDK 6 或者更早的版本,那么你需要将 JZlib(www.jcraft.com/jzlib/)添加到CLASSPATH 中以支持压缩功能。
对于 Maven,请添加以下依赖项:
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jzlib</artifactId>
<version>1.1.3</version>
</dependency>