1. 程式人生 > >Netty入門教程

Netty入門教程

Netty入門教程——認識Netty

什麼是Netty?

Netty 是一個利用 Java 的高階網路的能力,隱藏其背後的複雜性而提供一個易於使用的 API 的客戶端/伺服器框架。
Netty 是一個廣泛使用的 Java 網路程式設計框架(Netty 在 2011 年獲得了Duke’s Choice Award,見https://www.java.net/dukeschoice/2011)。它活躍和成長於使用者社群,像大型公司 Facebook 和 Instagram 以及流行 開源專案如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其強大的對於網路抽象的核心程式碼。

以上是摘自《Essential Netty In Action》這本書,本文的內容也是本人讀了這本書之後的一些整理心得,如有不當之處歡迎大蝦們指正
##Netty和Tomcat有什麼區別?
Netty和Tomcat最大的區別就在於通訊協議,Tomcat是基於Http協議的,他的實質是一個基於http協議的web容器,但是Netty不一樣,他能通過程式設計自定義各種協議,因為netty能夠通過codec自己來編碼/解碼位元組流,完成類似redis訪問的功能,這就是netty和tomcat最大的不同。

有人說netty的效能就一定比tomcat效能高,其實不然,tomcat從6.x開始就支援了nio模式,並且後續還有APR模式——一種通過jni呼叫apache網路庫的模式,相比於舊的bio模式,併發效能得到了很大提高,特別是APR模式,而netty是否比tomcat效能更高,則要取決於netty程式作者的技術實力了。


##為什麼Netty受歡迎?
如第一部分所述,netty是一款收到大公司青睞的框架,在我看來,netty能夠受到青睞的原因有三:

1.併發高
2.傳輸快
3.封裝好
##Netty為什麼併發高
Netty是一款基於NIO(Nonblocking I/O,非阻塞IO)開發的網路通訊框架,對比於BIO(Blocking I/O,阻塞IO),他的併發效能得到了很大提高,兩張圖讓你瞭解BIO和NIO的區別:
####阻塞IO的通訊方式
阻塞IO的通訊方式
####非阻塞IO的通訊方式
非阻塞IO的通訊方式
從這兩圖可以看出,NIO的單執行緒能處理連線的數量比BIO要高出很多,而為什麼單執行緒能處理更多的連線呢?原因就是圖二中出現的Selector。
當一個連線建立之後,他有兩個步驟要做,第一步是接收完客戶端發過來的全部資料,第二步是服務端處理完請求業務之後返回response給客戶端。NIO和BIO的區別主要是在第一步。
在BIO中,等待客戶端發資料這個過程是阻塞的,這樣就造成了一個執行緒只能處理一個請求的情況,而機器能支援的最大執行緒數是有限的,這就是為什麼BIO不能支援高併發的原因。
而NIO中,當一個Socket建立好之後,Thread並不會阻塞去接受這個Socket,而是將這個請求交給Selector,Selector會不斷的去遍歷所有的Socket,一旦有一個Socket建立完成,他會通知Thread,然後Thread處理完資料再返回給客戶端——這個過程是阻塞的,這樣就能讓一個Thread處理更多的請求了。
下面兩張圖是基於BIO的處理流程和netty的處理流程,輔助你理解兩種方式的差別:
####BIO的處理流程
BIO的處理流程


####NIO的處理流程
NIO的處理流程
除了BIO和NIO之外,還有一些其他的IO模型,下面這張圖就表示了五種IO模型的處理流程:
####五種常見的IO模型
五種常見的IO模型
BIO,同步阻塞IO,阻塞整個步驟,如果連線少,他的延遲是最低的,因為一個執行緒只處理一個連線,適用於少連線且延遲低的場景,比如說資料庫連線。
NIO,同步非阻塞IO,阻塞業務處理但不阻塞資料接收,適用於高併發且處理簡單的場景,比如聊天軟體。
多路複用IO,他的兩個步驟處理是分開的,也就是說,一個連線可能他的資料接收是執行緒a完成的,資料處理是執行緒b完成的,他比BIO能處理更多請求,但是比不上NIO,但是他的處理效能又比BIO更差,因為一個連線他需要兩次system call,而BIO只需要一次,所以這種IO模型應用的不多。
訊號驅動IO,這種IO模型主要用在嵌入式開發,不參與討論。
非同步IO,他的資料請求和資料處理都是非同步的,資料請求一次返回一次,適用於長連線的業務場景。

以上摘自Linux IO模式及 select、poll、epoll詳解
##Netty為什麼傳輸快
Netty的傳輸快其實也是依賴了NIO的一個特性——零拷貝。我們知道,Java的記憶體有堆記憶體、棧記憶體和字串常量池等等,其中堆記憶體是佔用記憶體空間最大的一塊,也是Java物件存放的地方,一般我們的資料如果需要從IO讀取到堆記憶體,中間需要經過Socket緩衝區,也就是說一個數據會被拷貝兩次才能到達他的的終點,如果資料量大,就會造成不必要的資源浪費。
Netty針對這種情況,使用了NIO中的另一大特性——零拷貝,當他需要接收資料的時候,他會在堆記憶體之外開闢一塊記憶體,資料就直接從IO讀到了那塊記憶體中去,在netty裡面通過ByteBuf可以直接對這些資料進行直接操作,從而加快了傳輸速度。
下兩圖就介紹了兩種拷貝方式的區別,摘自Linux 中的零拷貝技術,第 1 部分
####傳統資料拷貝
傳統資料拷貝
####零拷貝
零拷貝
上文介紹的ByteBuf是Netty的一個重要概念,他是netty資料處理的容器,也是Netty封裝好的一個重要體現,將在下一部分做詳細介紹。
##為什麼說Netty封裝好?
要說Netty為什麼封裝好,這種用文字是說不清的,直接上程式碼:

  • 阻塞I/O

public class PlainOioServer {

public void serve(int port) throws IOException {
    final ServerSocket socket = new ServerSocket(port);     //1
    try {
        for (;;) {
            final Socket clientSocket = socket.accept();    //2
            System.out.println("Accepted connection from " + clientSocket);

            new Thread(new Runnable() {                        //3
                @Override
                public void run() {
                    OutputStream out;
                    try {
                        out = clientSocket.getOutputStream();
                        out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8")));                            //4
                        out.flush();
                        clientSocket.close();                //5

                    } catch (IOException e) {
                        e.printStackTrace();
                        try {
                            clientSocket.close();
                        } catch (IOException ex) {
                            // ignore on close
                        }
                    }
                }
            }).start();                                        //6
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

}

  • 非阻塞I/O

public class PlainNioServer {

public void serve(int port) throws IOException {
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    serverChannel.configureBlocking(false);
    ServerSocket ss = serverChannel.socket();
    InetSocketAddress address = new InetSocketAddress(port);
    ss.bind(address);                                            //1
    Selector selector = Selector.open();                        //2
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);    //3
    final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
    for (;;) {
        try {
            selector.select();                                    //4
        } catch (IOException ex) {
            ex.printStackTrace();
            // handle exception
            break;
        }
        Set<SelectionKey> readyKeys = selector.selectedKeys();    //5
        Iterator<SelectionKey> iterator = readyKeys.iterator();
        while (iterator.hasNext()) {
            SelectionKey key = iterator.next();
            iterator.remove();
            try {
                if (key.isAcceptable()) {                //6
                    ServerSocketChannel server =
                            (ServerSocketChannel)key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_WRITE |
                            SelectionKey.OP_READ, msg.duplicate());    //7
                    System.out.println(
                            "Accepted connection from " + client);
                }
                if (key.isWritable()) {                //8
                    SocketChannel client =
                            (SocketChannel)key.channel();
                    ByteBuffer buffer =
                            (ByteBuffer)key.attachment();
                    while (buffer.hasRemaining()) {
                        if (client.write(buffer) == 0) {        //9
                            break;
                        }
                    }
                    client.close();                    //10
                }
            } catch (IOException ex) {
                key.cancel();
                try {
                    key.channel().close();
                } catch (IOException cex) {
                    // 在關閉時忽略
                }
            }
        }
    }
}

}

  • Netty

public class NettyOioServer {

public void server(int port) throws Exception {
    final ByteBuf buf = Unpooled.unreleasableBuffer(
            Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
    EventLoopGroup group = new OioEventLoopGroup();
    try {
        ServerBootstrap b = new ServerBootstrap();        //1

        b.group(group)                                    //2
         .channel(OioServerSocketChannel.class)
         .localAddress(new InetSocketAddress(port))
         .childHandler(new ChannelInitializer<SocketChannel>() {//3
             @Override
             public void initChannel(SocketChannel ch) 
                 throws Exception {
                 ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {            //4
                     @Override
                     public void channelActive(ChannelHandlerContext ctx) throws Exception {
                         ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5
                     }
                 });
             }
         });
        ChannelFuture f = b.bind().sync();  //6
        f.channel().closeFuture().sync();
    } finally {
        group.shutdownGracefully().sync();        //7
    }
}

}

從程式碼量上來看,Netty就已經秒殺傳統Socket程式設計了,但是這一部分博大精深,僅僅貼幾個程式碼豈能說明問題,在這裡給大家介紹一下Netty的一些重要概念,讓大家更理解Netty。

  • Channel
    資料傳輸流,與channel相關的概念有以下四個,上一張圖讓你瞭解netty裡面的Channel。
    ####Channel一覽
    Channel一覽

  • Channel,表示一個連線,可以理解為每一個請求,就是一個Channel。

  • ChannelHandler,核心處理業務就在這裡,用於處理業務請求。

  • ChannelHandlerContext,用於傳輸業務資料。

  • ChannelPipeline,用於儲存處理過程需要用到的ChannelHandler和ChannelHandlerContext。

  • ByteBuf
    ByteBuf是一個儲存位元組的容器,最大特點就是使用方便,它既有自己的讀索引和寫索引,方便你對整段位元組快取進行讀寫,也支援get/set,方便你對其中每一個位元組進行讀寫,他的資料結構如下圖所示:
    ####ByteBuf資料結構
    ByteBuf資料結構
    他有三種使用模式:

1.Heap Buffer 堆緩衝區
堆緩衝區是ByteBuf最常用的模式,他將資料儲存在堆空間。
2.Direct Buffer 直接緩衝區
直接緩衝區是ByteBuf的另外一種常用模式,他的記憶體分配都不發生在堆,jdk1.4引入的nio的ByteBuffer類允許jvm通過本地方法呼叫分配記憶體,這樣做有兩個好處

  • 通過免去中間交換的記憶體拷貝, 提升IO處理速度; 直接緩衝區的內容可以駐留在垃圾回收掃描的堆區以外。
  • DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的記憶體, GC對此”無能為力”,也就意味著規避了在高負載下頻繁的GC過程對應用執行緒的中斷影響.
    3.Composite Buffer 複合緩衝區
    複合緩衝區相當於多個不同ByteBuf的檢視,這是netty提供的,jdk不提供這樣的功能。
    除此之外,他還提供一大堆api方便你使用,在這裡我就不一一列出了,具體參見ByteBuf位元組快取。
  • Codec
    Netty中的編碼/解碼器,通過他你能完成位元組與pojo、pojo與pojo的相互轉換,從而達到自定義協議的目的。
    在Netty裡面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。