Netty中的SPDY實(shí)現(xiàn)

2020-11-06 16:16 更新

SPDY 使用 TLS 的擴(kuò)展稱為 Next Protocol Negotiation (NPN)。在Java 中,我們有兩種不同的方式選擇的基于 NPN 的協(xié)議:

  • 使用 ssl_npn,NPN 的開源 SSL 提供者。
  • 使用通過 Jetty 的 NPN 擴(kuò)展庫。

在這個(gè)例子中使用 Jetty 庫。如果你想使用 ssl_npn,請參閱https://github.com/benmmurphy/ssl_npn項(xiàng)目文檔

Jetty NPN 庫

Jetty NPN 庫是一個(gè)外部的庫,而不是 Netty 的本身的一部分。它用于處理 Next Protocol Negotiation, 這是用于檢測客戶端是否支持 SPDY。

集成 Next Protocol Negotiation

Jetty 庫提供了一個(gè)接口稱為 ServerProvider,確定所使用的協(xié)議和選擇哪個(gè)鉤子。這個(gè)的實(shí)現(xiàn)可能取決于不同版本的 HTTP 和 SPDY 版本的支持。下面的清單顯示了將用于我們的示例應(yīng)用程序的實(shí)現(xiàn)。

Listing 12.1 Implementation of ServerProvider

public class DefaultServerProvider implements NextProtoNego.ServerProvider {
    private static final List<String> PROTOCOLS =
            Collections.unmodifiableList(Arrays.asList("spdy/2", "spdy/3", "http/1.1"));  //1

    private String protocol;

    @Override
    public void unsupported() {
        protocol = "http/1.1";   //2
    }

    @Override
    public List<String> protocols() {
        return PROTOCOLS;   //3
    }

    @Override
    public void protocolSelected(String protocol) {
        this.protocol = protocol;  //4
    }

    public String getSelectedProtocol() {
        return protocol;  //5
    }
}
  1. 定義所有的 ServerProvider 實(shí)現(xiàn)的協(xié)議
  2. 設(shè)置如果 SPDY 協(xié)議失敗了就轉(zhuǎn)到 http/1.1
  3. 返回支持的協(xié)議的列表
  4. 設(shè)置選擇的協(xié)議
  5. 返回選擇的協(xié)議

在 ServerProvider 的實(shí)現(xiàn),我們支持下面的3種協(xié)議:

  • SPDY 2
  • SPDY 3
  • HTTP 1.1

如果客戶端不支持 SPDY ,則默認(rèn)使用 HTTP 1.1

實(shí)現(xiàn)各種 ChannelHandler

第一個(gè) ChannelInboundHandler 是用于不支持 SPDY 的情況下處理客戶端 HTTP 請求,如果不支持 SPDY 就回滾使用默認(rèn)的 HTTP 協(xié)議。

清單12.2顯示了HTTP流量的處理程序。

Listing 12.2 Implementation that handles HTTP

@ChannelHandler.Sharable
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    @Override
    public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { //1
        if (HttpHeaders.is100ContinueExpected(request)) {
            send100Continue(ctx); //2
        }

        FullHttpResponse response = new DefaultFullHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK); //3
        response.content().writeBytes(getContent().getBytes(CharsetUtil.UTF_8));  //4
        response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");  //5

        boolean keepAlive = HttpHeaders.isKeepAlive(request);

        if (keepAlive) {  //6
            response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());
            response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
        }
        ChannelFuture future = ctx.writeAndFlush(response);  //7

        if (!keepAlive) {
            future.addListener (ChannelFutureListener.CLOSE); //8
        }
    }

    protected String getContent() {  //9
        return "This content is transmitted via HTTP\r\n";
    }

    private static void send100Continue(ChannelHandlerContext ctx) {  //10
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        ctx.writeAndFlush(response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {  //11
        cause.printStackTrace();
        ctx.close();
    }
}
  1. 重寫 channelRead0() ,可以被所有的接收到的 FullHttpRequest 調(diào)用
  2. 檢查如果接下來的響應(yīng)是預(yù)期的,就寫入
  3. 新建 FullHttpResponse,用于對請求的響應(yīng)
  4. 生成響應(yīng)的內(nèi)容,將它寫入 payload
  5. 設(shè)置頭文件,這樣客戶端就能知道如何與 響應(yīng)的 payload 交互
  6. 檢查請求設(shè)置是否啟用了 keepalive;如果是這樣,將標(biāo)題設(shè)置為符合HTTP RFC
  7. 寫響應(yīng)給客戶端,并獲取到 Future 的引用,用于寫完成時(shí),獲取到通知
  8. 如果響應(yīng)不是 keepalive,在寫完成時(shí)關(guān)閉連接
  9. 返回內(nèi)容作為響應(yīng)的 payload
  10. Helper 方法生成了100 持續(xù)的響應(yīng),并寫回給客戶端
  11. 若執(zhí)行階段拋出異常,則關(guān)閉管道

這就是 Netty 處理標(biāo)準(zhǔn)的 HTTP 。你可能需要分別處理特定 URI ,應(yīng)對不同的狀態(tài)代碼,這取決于資源存在與否,但基本的概念將是相同的。

我們的下一個(gè)任務(wù)將會提供一個(gè)組件來支持 SPDY 作為首選協(xié)議。 Netty 提供了簡單的處理 SPDY 方法。這些將使您能夠重用FullHttpRequest 和 FullHttpResponse 消息,通過 SPDY 透明地接收和發(fā)送他們。

HttpRequestHandler 雖然是我們可以重用代碼,我們將改變我們的內(nèi)容寫回客戶端只是強(qiáng)調(diào)協(xié)議變化;通常您會返回相同的內(nèi)容。下面的清單展示了實(shí)現(xiàn),它擴(kuò)展了先前的 HttpRequestHandler。

Listing 12.3 Implementation that handles SPDY

@ChannelHandler.Sharable
public class SpdyRequestHandler extends HttpRequestHandler {   //1
    @Override
    protected String getContent() {
        return "This content is transmitted via SPDY\r\n";  //2
    }
}
  1. 繼承 HttpRequestHandler 這樣就能共享相同的邏輯
  2. 生產(chǎn)內(nèi)容寫到 payload。這個(gè)重寫了 HttpRequestHandler 的 getContent() 的實(shí)現(xiàn)

SpdyRequestHandler 繼承自 HttpRequestHandler,但區(qū)別是:寫入的內(nèi)容的 payload 狀態(tài)的響應(yīng)是在 SPDY 寫的。

我們可以實(shí)現(xiàn)兩個(gè)處理程序邏輯,將選擇一個(gè)相匹配的協(xié)議。然而添加以前寫過的處理程序到 ChannelPipeline 是不夠的;正確的編解碼器還需要補(bǔ)充。它的責(zé)任是檢測傳輸字節(jié)數(shù),然后使用 FullHttpResponse 和 FullHttpRequest 的抽象進(jìn)行工作。

Netty 的附帶一個(gè)基類,完全能做這個(gè)。所有您需要做的是實(shí)現(xiàn)邏輯選擇協(xié)議和選擇適當(dāng)?shù)奶幚沓绦颉?/p>

清單12.4顯示了實(shí)現(xiàn),它使用 Netty 的提供的抽象基類。

public class DefaultSpdyOrHttpChooser extends SpdyOrHttpChooser {

    public DefaultSpdyOrHttpChooser(int maxSpdyContentLength, int maxHttpContentLength) {
        super(maxSpdyContentLength, maxHttpContentLength);
    }

    @Override
    protected SelectedProtocol getProtocol(SSLEngine engine) {
        DefaultServerProvider provider = (DefaultServerProvider) NextProtoNego.get(engine);  //1
        String protocol = provider.getSelectedProtocol();
        if (protocol == null) {
            return SelectedProtocol.UNKNOWN; //2
        }
        switch (protocol) {
            case "spdy/2":
                return SelectedProtocol.SPDY_2; //3
            case "spdy/3.1":
                return SelectedProtocol.SPDY_3_1; //4
            case "http/1.1":
                return SelectedProtocol.HTTP_1_1; //5
            default:
                return SelectedProtocol.UNKNOWN; //6
        }
    }

    @Override
    protected ChannelInboundHandler createHttpRequestHandlerForHttp() {
        return new HttpRequestHandler(); //7
    }

    @Override
    protected ChannelInboundHandler createHttpRequestHandlerForSpdy() {
        return new SpdyRequestHandler();  //8
    }
}
  1. 使用 NextProtoNego 用于獲取 DefaultServerProvider 的引用, 用于 SSLEngine
  2. 協(xié)議不能被檢測到。一旦字節(jié)已經(jīng)準(zhǔn)備好讀,檢測過程將重新開始。
  3. SPDY 2 被檢測到
  4. SPDY 3 被檢測到
  5. HTTP 1.1 被檢測到
  6. 未知協(xié)議被檢測到
  7. 將會被調(diào)用給 FullHttpRequest 消息添加處理器。該方法只會在不支持 SPDY 時(shí)調(diào)用,那么將會使用 HTTPS
  8. 將會被調(diào)用給 FullHttpRequest 消息添加處理器。該方法在支持 SPDY 時(shí)調(diào)用

該實(shí)現(xiàn)要注意檢測正確的協(xié)議并設(shè)置 ChannelPipeline 。它可以處理SPDY 版本 2、3 和 HTTP 1.1,但可以很容易地修改 SPDY 支持額外的版本。

設(shè)置 ChannelPipeline

通過實(shí)現(xiàn) ChannelInitializer 將所有的處理器連接到一起。正如你所了解的那樣,這將設(shè)置 ChannelPipeline 并添加所有需要的ChannelHandler 的。

SPDY 需要兩個(gè) ChannelHandler:

  • SslHandler,用于檢測 SPDY 是否通過 TLS 擴(kuò)展
  • DefaultSpdyOrHttpChooser,用于當(dāng)協(xié)議被檢測到時(shí),添加正確的 ChannelHandler 到 ChannelPipeline

除了添加 ChannelHandler 到 ChannelPipeline, ChannelInitializer 還有另一個(gè)責(zé)任;即,分配之前創(chuàng)建的 DefaultServerProvider 通過 SslHandler 到 SslEngine 。這將通過Jetty NPN 類庫的 NextProtoNego helper 類實(shí)現(xiàn)

Listing 12.5 Implementation that handles SPDY

public class SpdyChannelInitializer extends ChannelInitializer<SocketChannel> {  //1
    private final SslContext context;

    public SpdyChannelInitializer(SslContext context) //2 {
        this.context = context;
    }

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        SSLEngine engine = context.newEngine(ch.alloc());  //3
        engine.setUseClientMode(false);  //4

        NextProtoNego.put(engine, new DefaultServerProvider());  //5
        NextProtoNego.debug = true;

        pipeline.addLast("sslHandler", new SslHandler(engine));  //6
        pipeline.addLast("chooser", new DefaultSpdyOrHttpChooser(1024 * 1024, 1024 * 1024));
    }
}
  1. 繼承 ChannelInitializer 是一個(gè)簡單的開始
  2. 傳遞 SSLContext 用于創(chuàng)建 SSLEngine
  3. 新建 SSLEngine,用于新的管道和連接
  4. 配置 SSLEngine 用于非客戶端使用
  5. 通過 NextProtoNego helper 類綁定 DefaultServerProvider 到 SSLEngine
  6. 添加 SslHandler 到 ChannelPipeline 這將會在協(xié)議檢測到時(shí)保存在 ChannelPipeline
  7. 添加 DefaultSpyOrHttpChooser 到 ChannelPipeline 。這個(gè)實(shí)現(xiàn)將會監(jiān)測協(xié)議。添加正確的 ChannelHandler 到 ChannelPipeline,并且移除自身

實(shí)際的 ChannelPipeline 設(shè)置將會在 DefaultSpdyOrHttpChooser 實(shí)現(xiàn)之后完成,因?yàn)樵谶@一點(diǎn)上它可能只需要知道客戶端是否支持 SPDY

為了說明這一點(diǎn),讓我們總結(jié)一下,看看不同 ChannelPipeline 狀態(tài)期間與客戶連接的生命周期。圖12.2顯示了在 Channel 初始化后的 ChannelPipeline。

Figure%2012

Figure 12.2 ChannelPipeline after connection

現(xiàn)在,這取決于客戶端是否支持 SPDY,管道將修改DefaultSpdyOrHttpChooser 來處理協(xié)議。之后并不需要添加所需的 ChannelHandler 到 ChannelPipeline,所以刪除本身。這個(gè)邏輯是由抽象 SpdyOrHttpChooser 類封裝,DefaultSpdyOrHttpChooser 父類。

圖12.3顯示了支持 SPDY 的 ChannelPipeline 用于連接客戶端的配置。

Figure%2012

Figure 12.3 ChannelPipeline if SPDY is supported

每個(gè) ChannelHandler 負(fù)責(zé)的一小部分工作,這個(gè)就是對基于 Netty 構(gòu)造的應(yīng)用程序最完美的詮釋。每個(gè) ChannelHandler 的職責(zé)如表12.3所示。

Table 12.3 Responsibilities of the ChannelHandlers when SPDY is used

名稱職責(zé)
SslHandler加解密兩端交換的數(shù)據(jù)
SpdyFrameDecoder從接收到的 SPDY 幀中解碼字節(jié)
SpdyFrameEncoder編碼 SPDY 幀到字節(jié)
SpdySessionHandler處理 SPDY session
SpdyHttpEncoder編碼 HTTP 消息到 SPDY 幀
SpdyHttpDecoder解碼 SDPY 幀到 HTTP 消息
SpdyHttpResponseStreamIdHandler處理基于 SPDY ID 請求和響應(yīng)之間的映射關(guān)系
SpdyRequestHandler處理 FullHttpRequest, 用于從 SPDY 幀中解碼,因此允許 SPDY 透明傳輸使用

當(dāng)協(xié)議是 HTTP(s) 時(shí),ChannelPipeline 看起來相當(dāng)不同,如圖13.4所示。

Figure%2012

Figure 13.4 ChannelPipeline if SPDY is not supported

和之前一樣,每個(gè) ChannelHandler 都有職責(zé),定義在表12.4

Table 12.4 Responsibilities of the ChannelHandlers when HTTP is used

名稱職責(zé)
SslHandler加解密兩端交換的數(shù)據(jù)
HttpRequestDecoder從接收到的 HTTP 請求中解碼字節(jié)
HttpResponseEncoder編碼 HTTP 響應(yīng)到字節(jié)

HttpObjectAggregator 處理 SPDY session HttpRequestHandler | 解碼時(shí)處理 FullHttpRequest

所有東西組合在一起

所有的 ChannelHandler 實(shí)現(xiàn)已經(jīng)準(zhǔn)備好,現(xiàn)在組合成一個(gè) SpdyServer

Listing 12.6 SpdyServer implementation

public class SpdyServer {

    private final NioEventLoopGroup group = new NioEventLoopGroup();  //1
    private final SslContext context;
    private Channel channel;

    public SpdyServer(SslContext context) { //2
        this.context = context;
    }

    public ChannelFuture start(InetSocketAddress address) {
        ServerBootstrap bootstrap  = new ServerBootstrap(); //3
        bootstrap.group(group)
                .channel(NioServerSocketChannel.class)
                .childHandler(new SpdyChannelInitializer(context)); //4
        ChannelFuture future = bootstrap.bind(address); //5
        future.syncUninterruptibly();
        channel = future.channel();
        return future;
    }

    public void destroy() { //6
        if (channel != null) {
            channel.close();
        }
        group.shutdownGracefully();
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Please give port as argument");
            System.exit(1);
        }
        int port = Integer.parseInt(args[0]);

        SelfSignedCertificate cert = new SelfSignedCertificate();
        SslContext context = SslContext.newServerContext(cert.certificate(), cert.privateKey());  //7
        final SpdyServer endpoint = new SpdyServer(context);
        ChannelFuture future = endpoint.start(new InetSocketAddress(port));

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                endpoint.destroy();
            }
        });
        future.channel().closeFuture().syncUninterruptibly();
    }
}
  1. 構(gòu)建新的 NioEventLoopGroup 用于處理 I/O
  2. 傳遞 SSLContext 用于加密
  3. 新建 ServerBootstrap 用于配置服務(wù)器
  4. 配置 ServerBootstrap
  5. 綁定服務(wù)器用于接收指定地址的連接
  6. 銷毀服務(wù)器,用于關(guān)閉管道和 NioEventLoopGroup
  7. 從 BogusSslContextFactory 獲取 SSLContext 。這是一個(gè)虛擬實(shí)現(xiàn)進(jìn)行測試。真正的實(shí)現(xiàn)將為 SslContext 配置適當(dāng)?shù)拿荑€存儲庫。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號