Netty系列文章之構建HTTP(HTTPS)應用程式

pjmike_pj發表於2019-03-01

原文部落格地址: 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

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通過一個名為SslHandlerChannelHandler實現加密和解密的功能,其中SslHandler在內部使用SSLEngine來完成實際的工作,SSLEngine的實現可以是JDK的SSLEngine,也可以是 Netty 的OpenSslEngine,當然推薦使用Netty的OpenSslEngine,因為它效能更好,通過SslHandler進行解密和加密的過程如下圖所示(摘自《Netty In Action》):

sslhandler

大多數情況下,SslHandler 將是 ChannelPipeline 中的第一個 ChannelHandler。這確保了只有在所有其他的 ChannelHandler 將它們的邏輯應用到資料之後,才會進行加密。

HTTP請求和響應組成部分

HTTP是基於請求/響應模型的的: 客戶端向服務端傳送一個HTTP請求,然後服務端將會返回一個HTTP響應,Netty提供了多種編碼器和解碼器以簡化對這個協議的使用。

HTTP請求的組成部分如下圖:

httpRequest

HTTP響應的組成部分如下圖:

httpreponse

如上面兩圖所示,一個HTTP請求/響應可能由多個資料部分組成,並且它總是以一個 LastHttpContent 部分作為結束。 FullHttpRequestFullHttpResponse訊息是特殊的子型別,分別代表了完整的請求和響應。

所有型別的HTTP訊息都實現了 HttpObject 介面

HTTP解碼器、編碼器和編解碼器

Netty為HTTP訊息提供了編碼器和解碼器:

  • HttpRequestEncoder: 編碼器,用於客戶端,向伺服器傳送請求
  • HttpResponseEecoder: 編碼器,用於服務端,向服務端傳送響應
  • HttpRequestDecoder:解碼器,用於服務端,接收來自客戶端的請求
  • HttpResponseDecoder: 解碼器,用於客戶端,接收來自服務端的請求

編解碼器

  • HttpClientCodec: 用於客戶端的編解碼器,等效於 HttpRequestEncoderHttpResponseDecoder的組合
  • HttpServerCodec:用於服務端的編解碼器,等效於 HttpRequsetDecoderHttpResponseEncoder的組合

HttpServerCodec為例,它的類繼承結構圖如下:

httpservercodec

HttpServerCodec 同時實現了 ChannelInboundHandlerChannelOutboundHandler介面,以達到同時具有編碼和解碼的能力。

聚合器

  • HttpObjectAggregator: 聚合器,可以將多個訊息部分合併為 FullHttpRequest或者 FullHttpResponse訊息。使用該聚合器的原因是HTTP解碼器會在每個HTTP訊息中生成多個訊息物件,如HttpRequest/HttpResponse,HttpContent,LastHttpContent,使用聚合器將它們聚合成一個完整的訊息內容,這樣就不用關心訊息碎片了。

應用程式程式碼

構建基於Netty的HTTP/HTTPS 應用程式的原始碼出自於Netty官方提供的demo,我略微做了一些改動,原地址是:github.com/netty/netty…

原始碼:

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-typeConnectionContent-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型別有更好的效能。

測試

客戶端測試:

curl

服務端日誌:

server

小結

以上總結了如何使用Netty構建一個簡單的HTTP/HTTPS應用程式。當然上面的程式參考的是Netty官方提供的Demo,Netty官方還提供了很多其他方面的例子,對於入門學習來說還不錯,詳細地址是: github.com/netty/netty…

參考資料 & 鳴謝

相關文章