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

原文博客地址: pjmike的博客html

前言

這篇文章主要介紹如何用Netty構建一個HTTP/HTTPS應用程序,用一個HelloWorld級Demo進行闡述java

SSL/TLS協議簡介

由於要同時構建HTTPS應用程序,因此咱們須要經過使用 SSL/TLS保護Netty應用程序,這裏先簡單介紹下 SSL/TLS協議。git

SSL和TLS都是運輸層的安全協議, 它們發展歷史以下:github

  • 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就是TLSweb

SSL/TLS安全協議示意圖以下:bootstrap

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實現有更好的性能app

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,我略微作了一些改動,原地址是:https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/helloworld

源代碼:

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官方還提供了不少其餘方面的例子,對於入門學習來講還不錯,詳細地址是: https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example

參考資料 & 鳴謝

相關文章
相關標籤/搜索