原文博客地址: pjmike 的博客
前言
这篇文章主要介绍如何用 Netty 构建一个 HTTP/HTTPS 应用程序, 用一个 HelloWorld 级 Demo 进行阐述
SSL/TLS 协议简介
因为要同时构建 HTTPS 应用程序, 所以我们需要通过使用 SSL/TLS 保护 Netty 应用程序, 这里先简单介绍下 SSL/TLS 协议.
SSL 和 TLS 都是运输层的安全协议, 它们发展历史如下:
1995: SSL 2.0 , 由 Netscape 提出, 这个版本由于设计缺陷, 并不安全, 很快被发现有严重漏洞, 已经废弃
1996:SSL 3.0 写成 RFC, 开始流行, 目前 (从 2015 年) 已经不安全, 必须禁用
1999:TLS1.0 互联网标准化组织 ISOC 接替 NetScape 公司, 发布了 SSL 的升级版 TLS1.0 版
2006: TLS 1.1. 作为 RFC 4346 发布. 主要 fix 了 CBC 模式相关的如 BEAST 攻击等漏洞
2008: TLS 1.2. 作为 RFC 5246 发布 . 增进安全性. 目前 (2015 年) 应该主要部署的版本, 请确保你使用的是这个版本
2015 之后: TLS 1.3, 还在制订中, 支持 0-rtt, 大幅增进安全性, 砍掉了 aead 之外的加密方式
由于 SSL 的 2 个版本都已经退出历史舞台, 现在一般所说的 SSL 就是 TLS
SSL/TLS 安全协议示意图如下:
SSL/TLS 协议是一个位于 HTTP 层与 TCP 层之间的可选层, 其提供的服务主要有:
认证用户和服务器, 确保数据发送到正确的客户机和服务器
加密数据以防止数据中途被窃取
维护数据的完整性, 确保数据在传输过程中不被改变
关于 SSL/TLS 协议更加详细的介绍可以查找相关资料, 这里就不细说了.
JDK 的 javax.NET.ssl 包 VS Netty 的 OpenSSL/SSLEngine
为了支持 SSL/TLS,Java 提供了 javax.NET.ssl 包, 它的 SSLContext 和 SSLEngine 类使得解密和加密相当简单和高效. SSLContext 是 SSL 链接的上下文, SSLEngine 主要用于出站和入站字节流的操作.
Netty 还提供了使用 OpenSSL 工具包的 SSLEngine 实现, 该 SSLEngine 比 JDK 提供的 SSLEngine 实现有更好的性能
Netty 通过一个名为 SslHandler 的 ChannelHandler 实现加密和解密的功能, 其中 SslHandler 在内部使用 SSLEngine 来完成实际的工作, SSLEngine 的实现可以是 JDK 的 SSLEngine, 也可以是 Netty 的 OpenSslEngine, 当然推荐使用 Netty 的 OpenSslEngine, 因为它性能更好, 通过 SslHandler 进行解密和加密的过程如下图所示(摘自《Netty In Action》):
大多数情况下, SslHandler 将是 ChannelPipeline 中的第一个 ChannelHandler. 这确保了只有在所有其他的 ChannelHandler 将它们的逻辑应用到数据之后, 才会进行加密.
HTTP 请求和响应组成部分
HTTP 是基于请求 / 响应模型的的: 客户端向服务端发送一个 HTTP 请求, 然后服务端将会返回一个 HTTP 响应, Netty 提供了多种编码器和解码器以简化对这个协议的使用.
HTTP 请求的组成部分如下图:
HTTP 响应的组成部分如下图:
如上面两图所示, 一个 HTTP 请求 / 响应可能由多个数据部分组成, 并且它总是以一个 LastHttpContent 部分作为结束. FullHttpRequest 和 FullHttpResponse 消息是特殊的子类型, 分别代表了完整的请求和响应.
所有类型的 HTTP 消息都实现了 HttpObject 接口
HTTP 解码器, 编码器和编解码器
Netty 为 HTTP 消息提供了编码器和解码器:
HttpRequestEncoder: 编码器, 用于客户端, 向服务器发送请求
HttpResponseEecoder
: 编码器, 用于服务端, 向服务端发送响应
HttpRequestDecoder: 解码器, 用于服务端, 接收来自客户端的请求
HttpResponseDecoder
: 解码器, 用于客户端, 接收来自服务端的请求
编解码器:
HttpClientCodec: 用于客户端的编解码器, 等效于 HttpRequestEncoder 和
HttpResponseDecoder
的组合
HttpServerCodec: 用于服务端的编解码器, 等效于 HttpRequsetDecoder 和
HttpResponseEncoder
的组合
以 HttpServerCodec 为例, 它的类继承结构图如下:
HttpServerCodec 同时实现了 ChannelInboundHandler 和 ChannelOutboundHandler 接口, 以达到同时具有编码和解码的能力.
聚合器:
HttpObjectAggregator
: 聚合器, 可以将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息. 使用该聚合器的原因是 HTTP 解码器会在每个 HTTP 消息中生成多个消息对象, 如
HttpRequest/HttpResponse,HttpContent,LastHttpContent
, 使用聚合器将它们聚合成一个完整的消息内容, 这样就不用关心消息碎片了.
应用程序代码
构建基于 Netty 的 HTTP/HTTPS 应用程序的源代码出自于 Netty 官方提供的 demo, 我略微做了一些改动, 原地址是:
源代码:
- public class HttpHelloWorldServer {
- static final boolean SSL = System.getProperty("ssl") != null;
- static final int PORT = Integer.parseInt(System.getProperty("port", SSL ? "8443" : "8080"));
- public static void main(String[] args) throws Exception {
- final SslContext sslContext;
- // 判断 SSL 是否为 true, 为 true 表示使用 HTTPS 连接, 反之, 使用 HTTP
- if (SSL) {
- // 使用 Netty 自带的证书工具生成一个数字证书
- SelfSignedCertificate certificate = new SelfSignedCertificate();
- sslContext = SslContextBuilder.forServer(certificate.certificate(), certificate.privateKey()).build();
- } else {
- sslContext = null;
- }
- EventLoopGroup boss = new NioEventLoopGroup(1);
- EventLoopGroup worker = new NioEventLoopGroup();
- try {
- ServerBootstrap Bootstrap = new ServerBootstrap();
- Bootstrap.group(boss, worker)
- .channel(NioServerSocketChannel.class)
- .handler(new LoggingHandler(LogLevel.INFO))
- .childHandler(new ChannelInitializer<SocketChannel>() {
- @Override
- protected void initChannel(SocketChannel ch) throws Exception {
- ChannelPipeline pipeline = ch.pipeline();
- if (sslContext != null) {
- pipeline.addLast(sslContext.newHandler(ch.alloc()));
- }
- // 添加一个 HTTP 的编解码器
- pipeline.addLast(new HttpServerCodec());
- // 添加 HTTP 消息聚合器
- pipeline.addLast(new HttpObjectAggregator(64 * 1024));
- // 添加一个自定义服务端 Handler
- pipeline.addLast(new HttpHelloWorldServerHandler());
- }
- });
- ChannelFuture future = Bootstrap.bind(PORT).sync();
- System.err.println("Open your web browser and navigate to" +
- (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');
- future.channel().closeFuture().sync();
- } finally {
- boss.shutdownGracefully().sync();
- worker.shutdownGracefully().sync();
- }
- }
- }
代码解读
首先判断系统属性 ssl 是否存在, 如果存在, 则表明使用安全连接, 反之, 则使用一般的 HTTP 连接.
- final SslContext sslContext;
- if (SSL) {
- SelfSignedCertificate certificate = new SelfSignedCertificate();
- sslContext = SslContextBuilder.forServer(certificate.certificate(), certificate.privateKey()).build();
- } else {
- sslContext = null;
- }
上面代码所示, 当 SSL 为 true 时, 使用 Netty 自带的签名证书工具自定义服务端发送给客户端的数字证书.
接下来和一般的 Netty 服务端程序步骤一样, 先创建 ServerBootstrap 启动类, 设置和绑定 NioEventLoopGroup 线程池, 创建服务端 Channel, 添加 ChannelHandler. 值得注意的是, 添加的 ChannelHandler 都是与 HTTP 相关的 Handler.
HttpHelloWorldServerHandler
自定义的 Handler 代码如下:
- public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> {
- private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type");
- private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length");
- private static final AsciiString CONNECTION = AsciiString.cached("Connection");
- private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive");
- @Override
- protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
- if (msg instanceof HttpRequest) {
- HttpRequest req = (HttpRequest) msg;
- System.out.println("浏览器请求方式:"+req.method().name());
- String content = "";
- if ("/hello".equals(req.uri())) {
- content = "hello world";
- response2Client(ctx,req,content);
- } else {
- content = "Connect the Server";
- response2Client(ctx,req,content);
- }
- }
- }
- private void response2Client(ChannelHandlerContext ctx, HttpRequest req, String content) {
- boolean keepAlive = HttpUtil.isKeepAlive(req);
- FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(content.getBytes()));
- response.headers().set(CONTENT_TYPE, "text/plain");
- response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
- if (!keepAlive) {
- ctx.write(response).addListener(ChannelFutureListener.CLOSE);
- } else {
- response.headers().set(CONNECTION, KEEP_ALIVE);
- ctx.write(response);
- }
- }
- @Override
- public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
- ctx.flush();
- }
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
- cause.printStackTrace();
- ctx.close();
- }
- }
在此 Handler 中处理入站数据流, 但该代码只是处理 GET 请求, 没有对 POST 请求做出处理, 所以当浏览器发送一个 GET 请求时, 此 Handler 定义一个 HTTP 响应体 FullHttpResponse, 设置一些响应头, 如. Content-type,Connection,Content-Length 等, 设置响应内容, 然后通过 ctx.write 方法写入 HTTP 消息
AsciiString
在设置响应头时我们用到了 AsciiString, 从 Netty 4.1 开始, 提供了实现了 CharSequence 接口的 AsciiString, 至于 CharSequence 就是 String 的父类. AsciiString 包含的字符只占 1 个字节, 当你处理 US-ASCII 或者 ISO-8859-1 字符串时可以节省空间. 例如, HTTP 编解码器使用 AsciiString 处理 header name , 因为将 AsciiString 编码到 ByteBuf 中不会有类型转换的代价, 其内部实现就是用的 byte, 而对于 String 来说, 内部是存 char[], 使用 String 就需要将 char 转换成 byte, 所以 AsciiString 比 String 类型有更好的性能.
测试
客户端测试:
服务端日志:
小结
以上总结了如何使用 Netty 构建一个简单的 HTTP/HTTPS 应用程序. 当然上面的程序参考的是 Netty 官方提供的 Demo,Netty 官方还提供了很多其他方面的例子, 对于入门学习来说还不错, 详细地址是:
参考资料 & 鸣谢
SSL/TLS 协议运行机制的概述
TLS 协议分析 与 现代加密通信协议设计
Netty In Action https://book.douban.com/subject/27038538/
来源: https://juejin.im/post/5baa2d78e51d450e4a1bc8bf