天天看點

Netty 架構學習 —— 基于 Netty 的 HTTP/HTTPS 應用程式

通過 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 實作

Netty 架構學習 —— 基于 Netty 的 HTTP/HTTPS 應用程式

下述代碼展示了如何使用 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 響應的方法

Netty 架構學習 —— 基于 Netty 的 HTTP/HTTPS 應用程式
Netty 架構學習 —— 基于 Netty 的 HTTP/HTTPS 應用程式

如圖所示,一個 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一定程度上解決了這個問題,但資料流仍然是由用戶端所發送的請求驅動的

BinaryWebSocketFrame 資料幀:二進制資料
TextWebSocketFrame 資料幀:文本資料
ContinuationWebSocketFrame 資料幀:屬于上一個 BinaryWebSocketFrame 或者 TextWebSocketFrame 的文本或者二進制的資料
CloseWebSocketFrame 控制幀:一個 CLOSE 請求,關閉的狀态碼以及關閉的原因
PingWebSocketFrame 控制幀:請求一個 PongWebSocketFrame
PongWebSocketFrame 控制幀:對 PingWebSocketFrame 請求的響應
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
        }
    }
}
           
上一篇: NIO 基礎
下一篇: java使用netty

繼續閱讀