深入理解Netty框架


前言

本文討論的主題是Netty框架,本着3W原則 (What 是什么?->Why 為什么?->How 如何做?)來一步步探究Netty原理和本質以及運用場景。

了解基本名詞

1.BIO、NIO和AIO是什么?

BIO:同步阻塞,一個連接一個線程,客戶端有連接請求時服務器端就需要啟動一個線程進行處理,面向流的,各種流是阻塞的,流是單向的。
NIO:同步非阻塞,一個請求一個線程,但客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理,面向緩沖區的,非阻塞,流是雙向的,事件驅動模型,基於block的傳輸比基於流的傳輸更高效、更高級的IO函數zero-copy、IO多路復用大大提高了Java網絡應用的可伸縮性和實用性,基於Reactor線程模型。
AIO:異步非阻塞,一個有效請求一個線程,客戶端的I/O請求都是由OS先完成了再通知服務器應用去啟動線程進行處理。

2.什么是Netty零拷貝?

Netty 的零拷貝主要包含三個方面:

Netty 的接收和發送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接內存進行 Socket 讀寫,不需要進行字節緩沖區的二次拷貝。如果使用傳統的堆內存(HEAP BUFFERS)進行 Socket 讀寫,JVM 會將堆內存 Buffer 拷貝一份到直接內存中,然后才寫入 Socket 中。相比於堆外直接內存,消息在發送過程中多了一次緩沖區的內存拷貝。

Netty 提供了組合 Buffer 對象,可以聚合多個 ByteBuffer 對象,用戶可以像操作一個 Buffer 那樣方便的對組合 Buffer 進行操作,避免了傳統通過內存拷貝的方式將幾個小 Buffer 合並成一個大的 Buffer。

Netty 的文件傳輸采用了 transferTo 方法,它可以直接將文件緩沖區的數據發送到目標 Channel,避免了傳統通過循環 write 方式導致的內存拷貝問題。

3.select、poll、epoll的機制及其區別?

select,poll,epoll都是IO多路復用的機制。I/O多路復用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。

select:單個進程可監視的fd數量被限制,消息傳遞方式通過內核需要將消息傳遞到用戶空間,都需要內核拷貝動作。

poll:同select,主要區別是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的。

epoll:沒有最大並發連接的限制,效率提升,不是輪詢的方式,不會隨着FD數目的增加效率下降。只有活躍可用的FD才會調用callback函數;消息傳遞方式通過內核和用戶空間共享一塊內存來實現的。

4.TCP的粘包/拆包原因及其解決方法是什么?

原因:

要發送的數據大於TCP發送緩沖區剩余空間大小,將會發生拆包。

待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。

要發送的數據小於TCP發送緩沖區的大小,TCP將多次寫入緩沖區的數據一次發送出去,將會發生粘包。

接收數據端的應用層沒有及時讀取接收緩沖區中的數據,將發生粘包

解決方案:

發送端給每個數據包添加包首部,首部中應該至少包含數據包的長度,這樣接收端在接收到數據后,通過讀取包首部的長度字段,便知道每一個數據包的實際長度了。

發送端將每個數據包封裝為固定長度(不夠的可以通過補0填充),這樣接收端每次從接收緩沖區中讀取固定長度的數據就自然而然的把每個數據包拆分開來。

可以在數據包之間設置邊界,如添加特殊符號,這樣,接收端通過這個邊界就可以將不同的數據包拆分開。

何為Netty?

Netty是一個異步事件驅動(NIO)的網絡應用程序框架,用於快速開發可維護的搞性能協議服務器和客戶端。極大的簡化了TCP和UDP套接字服務器等網絡編程。Netty支持多種協議,如FTP,SMTP,HTTP以及各種二進制和基於文本的傳輸協議。

為什么用Netty?

特點

  • 一個高性能、異步事件驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持
  • 使用更高效的socket底層,對epoll空輪詢引起的cpu占用飆升在內部進行了處理,避免了直接使用NIO的陷阱,簡化了NIO的處理方式。
  • 采用多種decoder/encoder 支持,對TCP粘包/分包進行自動化處理
  • 可使用接受/處理線程池,提高連接效率,對重連、心跳檢測的簡單支持
  • 可配置IO線程數、TCP參數, TCP接收和發送緩沖區使用直接內存代替堆內存,通過內存池的方式循環利用ByteBuf
  • 通過引用計數器及時申請釋放不再引用的對象,降低了GC頻率
  • 使用單線程串行化的方式,高效的Reactor線程模型
  • 大量使用了volitale、使用了CAS和原子類、線程安全類的使用、讀寫鎖的使用

設計

  • 適用於各種傳輸類型的統一API - 阻塞和非阻塞套接字
  • 基於靈活且可擴展的事件模型,可以清晰地分離關注點
  • 高度可定制的線程模型 - 單線程,一個或多個線程池,如SEDA
  • 真正的無連接數據報套接字支持(自3.1起)

性能

  • 更高的吞吐量,更低的延遲
  • 減少資源消耗
  • 最小化不必要的內存復制

安全

  • 完整的SSL / TLS和StartTLS支持

Netty如何使用?

首先引入Maven包

<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.39.Final</version>
</dependency>

 編寫服務端,代碼如下:

package netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;

/**
 * @author LWX-PC
 * @version 1.0
 * @class DiscardServerHandler
 * @date 2019/8/18 18:39
 * @description
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf) msg;
        try {
            while (in.isReadable()) { // (1)
                System.out.print((char) in.readByte());
                System.out.flush();
            }
        } finally {
            ReferenceCountUtil.release(msg); // (2)
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
package netty;

import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;


/**
 * Discards any incoming data.
 */
public class DiscardServer {
    private int port;
    public DiscardServer(int port) {
        this.port = port;
    }
    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // (3)
                    .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new DiscardServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)          // (5)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (7)

            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 9000;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }
        new DiscardServer(port).run();
    }
}

測試:

回車后,輸入內容:

控制台可打印出輸入信息,如下:

 

這樣整個通信就建立了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM