Netty 框架學習 —— 基於 Netty 的 HTTP/HTTPS 應用程式

低吟不作語發表於2021-06-27

通過 SSL/TLS 保護應用程式

SSL 和 TLS 安全協議層疊在其他協議之上,用以實現資料安全。為了支援 SSL/TLS,Java 提供了 javax.net.ssl 包,它的 SSLContext 和 SSLEngine 類使得實現解密和加密變得相當簡單。Netty 通過一個名為 SsLHandler 的 ChannelHandler 實現了這個 API,其中 SSLHandler 在內部使用 SSLEngine 來完成實際工作

Netty 還提供了基於 OpenSSL 工具包的 SSLEngine 實現,比 JDK 提供的 SSLEngine 具有更好的效能。如果 OpenSSL 可用,可以將 Netty 應用程式配置為預設使用 OpenSSLEngine。如果不可用,Netty 將會退回到 JDK 實現

下述程式碼展示瞭如何使用 ChannelInitializer 來將 SslHandler 新增到 ChannelPipeline 中

public class SslChannelInitializer extends ChannelInitializer<Channel> {

    private final SslContext context;
    private final boolean startTls;

    public SslChannelInitializer(SslContext context, boolean startTls) {
        this.context = context;
        this.startTls = startTls;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        SSLEngine engine = context.newEngine(ch.alloc());
        ch.pipeline().addFirst("ssl", new SslHandler(engine, startTls));
    }
}

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

SSLHandler 具有一些有用的方法,如表所示,例如,在握手階段,兩個節點將相互驗證並且商定一種加密方式,你可以通過配置 SslHandler 來修改它的行為,或者在 SSL/TLS 握手一旦完成之後提供通知,握手階段之後,所有的資料都將會被加密

方法名稱 描述
setHandshakeTimeout(long, TimeUnit)
setHandshakeTimeoutMillis(long)
getHandshakeTimeoutMillis()
設定和獲取超時時間,超時之後,握手 ChannelFuture 將會被通知失敗
setCloseNotifyTimeout(long, TimeUnit)
setCloseNotifyTimeoutMillis(long)
getCloseNotifyTimeoutMillis()
設定和獲取超時時間,超時之後,將會觸發一個關閉通知並關閉連線,這也會導致通知該 ChannelFuture 失敗
handshakeFuture() 返回一個在握手完成後將會得到通知的 ChannelFuture,如果握手先前已經執行過,則返回一個包含了先前握手結果的 ChannelFuture
close()
close(ChannelPipeline)
close(ChannelHandlerContext, ChannelPromise)
傳送 close_notify 以請求關閉並銷燬底層的 SslEngine

HTTP 編解碼器

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

下圖分別展示了生產和消費 HTTP 請求和 HTTP 響應的方法

如圖所示,一個 HTTP 請求/響應可能由多個資料部分組成,並且總以一個 LastHttpContent 部分作為結束

下表概要地介紹了處理和生成這些訊息的 HTTP 解碼器和編碼器

名稱 描述
HttpRequestEncoder 將 HTTPRequest、HttpContent 和 LastHttpContent 訊息編碼為位元組
HttpResponseEncoder 將 HTTPResponse、HttpContent 和 LastHttpContent 訊息編碼為位元組
HttpRequestDecoder 將位元組編碼為 HTTPRequest、HttpContent 和 LastHttpContent 訊息
HttpResponseDecoder 將位元組編碼為 HTTPResponse、HttpContent 和 LastHttpContent 訊息

下述程式碼中的 HttpPipelineInitializer 類展示了將 HTTP 支援新增到你的應用程式是多麼簡單 —— 只需要將正確的 ChannelHandler 新增到 ChannelPipeline 中

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 {
            // 如果是服務端,則新增 HttpRequestDecoder 處理來自客戶端的請求
            pipeline.addLast("decoder", new HttpRequestDecoder());
            // 如果是客戶端,則新增 HttpResponseEncoder 向客戶端傳送響應
            pipeline.addLast("encoder", new HttpResponseEncoder());
        }
    }
}

聚合 HTTP 訊息

在 ChannelInitializer 將 ChannelHandler 安裝到 ChannelPipeline 中之後,你就可以處理不同型別的 HTTPObject 訊息了。但由於 HTTP 請求和響應可能由許多部分組成,因此你需要聚合它們以形成完整的訊息。Netty 提供了一個聚合器,它可以將多個訊息部分合併為 FullHttpRequest 或者 FullHttpResponse 訊息

由於訊息分段需要被緩衝,直到可以轉發下一個完整的訊息給下一個 ChannelInboundHandler,所以這個操作有輕微的開銷,其所帶來的好處就是你可以不必關心訊息碎片了

引入這種自動聚合機制只不過是向 ChannelPipeline 中新增另外一個 ChannelHandler 罷了,下述程式碼展示瞭如何做到這一點:

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());
        }
        // 將最大的訊息大小為 512KB 的 HTTPObjectAggregator 新增到 ChannelPipeline
        pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024));
    }
}

HTTP 壓縮

當使用 HTTP 時,建議開啟壓縮功能以儘可能多地減小傳輸資料的大小。雖然壓縮會帶來一些消耗,但通常來說它都是一個好主意,尤其是對於文字資料而言

Netty 為壓縮和解壓都提供了 ChannelHandler 實現,它們同時支援 gzip 和 deflate 編碼

客戶端可以通過提供以下頭部資訊來指示伺服器它所支援的壓縮格式

GET /encrypted-area HTTP/1.1

Host: www.example.com

Accept-Encoding: gzip, deflate

然而,需要注意的是,伺服器沒有義務壓縮它所傳送的資料

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());
            // 如果是伺服器,則新增 HttpContentDecompressor 來壓縮資料
            pipeline.addLast("decompressor", new HttpContentDecompressor());
        }
    }
}

HTTPS

啟用 HTTPS 只需要將 SslHandler 新增到 ChannelPipeline 的 ChannelHandler 組合中

public class HttpsCodecInitializer extends ChannelInitializer<Channel> {

    private final SslContext context;
    private final boolean isClient;

    public HttpsCodecInitializer(SslContext context, boolean isClient) {
        this.context = context;
        this.isClient = isClient;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        SSLEngine engine = context.newEngine(ch.alloc());
        pipeline.addLast("ssl", new SslHandler(engine));
        if (isClient) {
            pipeline.addLast("codec", new HttpClientCodec());
        } else {
            pipeline.addLast("codec", new HttpServerCodec());
        } 
    }
}

WebSocket

WebSocket 解決了一個長期存在的問題:既然底層協議(HTTP)是一個請求/響應模式的互動序列,那麼如何實時地釋出資訊呢?AJAX一定程度上解決了這個問題,但資料流仍然是由客戶端所傳送的請求驅動的

WebSocket 提供了在單個 TCP 連線上提供雙向的通訊,它為網頁和遠端伺服器之間的雙向通訊提供了一種替代 HTTP 輪詢的方案

要想向你的應用程式新增對於 WebSocket 的支援,你需要將適當的客戶端或者伺服器 WebSocketChannelHandler 新增到 ChannelPipeline 中。這個類將處理由 WebSocket 定義的稱為幀的特殊訊息型別,如表所示,WebSocketFrame 可以被歸類為資料幀或者控制幀

名稱 描述
BinaryWebSocketFrame 資料幀:二進位制資料
TextWebSocketFrame 資料幀:文字資料
ContinuationWebSocketFrame 資料幀:屬於上一個 BinaryWebSocketFrame 或者 TextWebSocketFrame 的文字或者二進位制的資料
CloseWebSocketFrame 控制幀:一個 CLOSE 請求,關閉的狀態碼以及關閉的原因
PingWebSocketFrame 控制幀:請求一個 PongWebSocketFrame
PongWebSocketFrame 控制幀:對 PingWebSocketFrame 請求的響應

因為 Netty 主要是一種伺服器端技術,所以我們重點建立 WebSocket 伺服器。下述程式碼展示了使用 WebSocketChannelHandler 的簡單示例,這個類會處理協議升級握手,以及三種控制幀 —— Close、Ping 和 Pong,Text 和 Binary 資料幀將會被傳遞給下一個 ChannelHandler 進行處理

public class WebSocketServerInitializer extends ChannelInitializer<Channel> {

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(
                new HttpServerCodec(),
                new HttpObjectAggregator(65536),
                // 如果被請求的端點是 /websocket,則處理該升級握手
                new WebSocketServerProtocolHandler("/websocket"),
                // TextFrameHandler 處理 TextWebSocketFrame
                new TextFrameHandler(),
                // BinaryFrameHandler 處理 BinaryWebSocketFrame
                new BinaryFrameHandler(),
                // ContinuationFrameHandler 處理 Continuation WebSocketFrame
                new ContinuationFrameHandler());
    }

    public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
            // do something
        }
    }

    public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> {

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {
            // do something
        }
    }

    public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler<ContinuationWebSocketFrame> {

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception {
            // do something
        }
    }
}

相關文章