Netty網絡框架


Netty網絡框架

Netty是一個異步的基於事件驅動的網絡框架。

為什么要使用Netty而不直接使用JAVA中的NIO

1.Netty支持三種IO模型同時支持三種Reactor模式。

2.Netty支持很多應用層的協議,提供了很多decoder和encoder。

3.Netty能夠解決TCP長連接所帶來的缺陷(粘包、半包等)

4.Netty支持應用層的KeepAlive。

5.Netty規避了JAVA NIO中的很多BUG,性能更好。


Netty啟動服務端

1.創建bossGroup和workerGroup(bossGroup負責接收連接,workerGroup負責處理連接的讀寫就緒事件)

2.創建ServerBootstrap服務端啟動對象,並且調用group()方法傳入剛剛創建的bossGroup和workerGroup。

3.調用ServerBootstrap的channel()方法,配置父Channel,一般為NioServerSocketChannel。

4.調用ServerBootstrap的childHandler()方法,配置子Channel與ChannelHandler之間的關系,傳入ChannelInitializer實現類,實現initChannel()方法,方法中獲取Channel的ChannelPileline,然后往ChannelPipeline中添加ChannelHandler。

5.調用ServerBootstrap的option()方法給父Channel配置參數。

6.調用ServerBootstrap的childOption()方法給子Channel配置參數。

7.綁定端口,啟動服務。

private void start() {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class) // 配置父Channel
                .childHandler(new ChannelInitializer<SocketChannel>() { // 配置子Channel與ChannelHandler之間的關系
                    @Override
                    protected void initChannel(SocketChannel socketChannel) {
                        // 往ChannelPipeline中添加ChannelHandler
                        socketChannel.pipeline().addLast(
                                new HttpRequestDecoder(),
                                new HttpObjectAggregator(65535),
                                new HttpResponseEncoder(),
                                new HttpServerHandler()
                        );
                    }
                })
                .option(ChannelOption.SO_BACKLOG, 128) // 給父Channel配置參數
                .childOption(ChannelOption.SO_KEEPALIVE, true); // 給子Channel配置參數

        try {
            // 綁定端口,啟動服務
            System.out.println("start server and bind 8888 port ...");
            serverBootstrap.bind(8888).sync();
        } catch (InterruptedException e) {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

Netty啟動客戶端

1.創建workerGroup,負責處理連接的讀寫就緒事件。

2.創建Bootstrap客戶端啟動對象,並且調用group()方法傳入剛剛創建的workerGroup。

3.調用Bootstrap的channel()方法,配置父Channel,一般為NioSocketChannel。

4.調用Bootstrap的option()方法,給父Channel配置參數。

5.調用Bootstrap的handler()方法,配置父Channel與ChannelHandler之間的關系。

6.連接服務器。

private void start() {
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup)
                .channel(NioSocketChannel.class) // 配置父Channel
                .option(ChannelOption.SO_KEEPALIVE, true) // 給父Channel配置參數
                .handler(new ChannelInitializer<SocketChannel>() { // 配置父Channel與ChannelHandler之間的關系
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new TimeClientHandler());
                    }
                });
                
        try {
            bootstrap.connect(new InetSocketAddress(8888)).sync(); // 連接服務器
        } catch (InterruptedException e) {
            workerGroup.shutdownGracefully();
        }
    }

ChannelInBoundHandler接口聲明了事件的處理方法

channelActive():當接收到一個新的連接時調用該方法

handlerAdd():當往Channel的ChannelPipeline中添加ChannelHandler時調用該方法

handlerRemove():當移除ChannelPipeline中的ChannelHandler時調用該方法

channelRead():當Channel有數據可讀時調用該方法

exceptionCaught():當在處理事件發生異常時調用該方法

ServerSocketChannel每接收到一個新的連接時都會調用ChannelInitializer的initChannel()方法,初始化Channel與ChannelHandler之間的關系,最后調用ChannelHandler的handlerAdd()和channelActive()方法。

關於ChannelPipeline

ChannelPipeline底層使用雙向鏈表。

當Channel有數據可讀時,會沿着鏈表從前往后尋找有IN性質的Handler進行處理。

當Channel寫入數據時,會沿着鏈表從后往前尋找有OUT性質的Handler進行處理。

關於write()和flush()方法

graph TB; S1[Channel的write方法] --將數據寫入到緩沖區--> buffer[緩沖區]; S2[Channel的flush方法] --發送緩沖區中的數據並清空--> buffer[緩沖區]; buffer --發送--> S3[SocketChannel];
write():將數據寫入到緩沖區
flush():發送緩沖區中的數據並進行清空
writeAndFlush():將數據寫入到緩沖區,同時發送緩沖區中的數據並進行清空

Channel的writeAndFlush()和flush()方法會從鏈表的最后一個節點開始從后往前尋找有OUT性質的Handler進行處理。

ChannelHandlerContext的writeAndFlush()和flush()方法會從當前節點從后往前尋找有OUT性質的Handler進行處理。

關於寫就緒事件

當SocketChannel可以寫入數據時,將會觸發寫就緒事件,所以一般不能隨便監聽,否則將會一直觸發。

當SocketChannel在寫入數據寫不進時(緩沖區已經滿了),此時可以將Channel注冊到Selector當中並且向Selector傳遞要監聽此Channel的寫就緒事件,然后強制發送緩沖區中的數據並進行清空,此時將會觸發寫就緒事件,當處理完寫就緒事件后,應該從Selector當中剔除對此Channel的監聽。

為什么說Netty中的所有操作都是異步的

Channel中的所有任務都會放入到其綁定的EventLoop的任務隊列中,然后等待被EventLoop中的線程處理。

關於ChannelFuture

由於Netty中的所有操作都是異步的,因此一般會返回ChannelFuture對象,用於存儲Channel異步執行的結果。

當創建ChannelFuture實例時,isDone()方法返回false,僅當ChannelFuture被設置成成功或者失敗時,isDone()方法才返回true。

可以往ChannelFuture中添加ChannelFutureListener,當任務被執行完畢后由IO線程自動調用。


Netty中的ByteBuf

ByteBuf有readerIndex和writerIndex兩個指針,默認都為0,當進行寫操作時移動writerIndex指針,讀操作時移動readerIndex指針。

可讀容量 = writerIndex - readerIndex

*只有read()/write()方法才會移動指針,get()/set()方法不會移動指針。

*ByteBuf支持動態擴容。

ByteBuf的創建和管理

使用ByteBufAllocator來創建和管理ByteBuf,其分別提供PooledByteBufAllocator和UnpooledByteBufAllocator實現類,分別代表池化和非池化。

*Netty同時也提供了Pooled和Unpooled工具類來創建和管理ByteBuf。

池化的ByteBuf(Pooled)

每次使用時都從池中取出一個ByteBuf對象,當使用完畢后再放回到池中。

每個ByteBuf都有一個refCount屬性,僅當refCount屬性為0時才將ByteBuf對象放回到池中。

ByteBuf的release()方法可以使refCount屬性減1(一般由最后一個訪問ByteBuf的Handler進行處理)

非池化的ByteBuf(Unpooled)

每次使用時都創建一個新的ByteBuf對象。

使用池化ByteBuf的風險

如果每次使用ByteBuf后卻不進行釋放,那么有可能發生內存泄漏,對象池中會不停的創建ByteBuf對象。

非池化的ByteBuf對象能夠依賴JVM自動進行回收。

關於堆內和堆外的ByteBuf

池化和非池化的ByteBufAllocator中都可以創建堆內和堆外的ByteBuf對象。

堆外的ByteBuf可以避免在進行IO操作時數據從堆內內存復制到操作系統內存的過程,所以對於IO操作來說一般使用堆外的ByteBuf,而對於內部業務數據處理來說使用堆內的ByteBuf。


Netty支持的IO模型

Netty支持BIO、NIO、AIO三種IO模型。

*其中AIO模型只在Netty的5.x版本有提供,但不建議使用,因為Netty不再維護同時也廢除了5.x版本,其原因是在Linux中AIO比NIO強不了多少。

Netty如何切換IO模型

只需要將EventLoopGroup和ServerSocketChannel換成相應IO模型的API即可。


Netty中使用Reactor模式

Reactor單線程模式

EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);

Reactor多線程模式

EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

*默認CPU核數 x 2個EventLoop。

主從Reactor多線程模式

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

關於TCP的粘包和半包

粘包(多個數據包被合並成一個進行發送)

graph LR; data1[ABC] --> compact[ABCDEF]; data2[DEF] --> compact; compact --send--> net[網絡]

半包(一個數據包被拆分成多個進行發送)

graph LR; data1[ABCDEF] --> part1[ABC]; data1 --> part2[DEF]; part1 --send--> net1[網絡]; part2 --send--> net2[網絡];

發生粘包的原因

1.寫入的數據遠小於緩沖區的大小,TCP協議為了性能的考慮,合並后再進行發送。

發生半包的原因

1.寫入的數據大於緩沖區的大小,因此必須拆包后再進行傳輸(緩沖區已滿,強制flush)

2.寫入的數據大於協議的MTU(最大傳輸單元),因此必須拆包后再進行傳輸。

TCP長連接的缺陷

長連接中可以發送多個請求,同時TCP協議是流式協議,消息無邊界,所以有一個很棘手的問題,接收方怎么去知道一個請求中的數據到底是哪里到哪里,以及一個請求中的數據有可能是粘包后的結果,同時多個請求中的數據有可能是半包后的結果。

解決方案

1.使用短連接,每次發送請求時都建立一個連接。

2.使用固定的長度,每個請求中的數據都使用固定的長度,接收方以接收到固定長度的數據來確定一個完整的請求數據。

3.使用指定的分隔符,每個請求中的數據的末尾都加上一個分隔符,接收方以分隔符來確定一個完整的請求數據。

4.使用特定長度的字段去存儲請求數據的長度,接收方根據請求數據的長度來確定一個完整的請求數據。

Netty對TCP長連接缺陷的解決方案

FixedLengthFrameDecoder:使用固定的長度

DelimiterBasedFrameDecoder:使用指定的分隔符

LengthFieldBasedFrameDecoder:使用特定長度的字段去存儲請求數據的長度

關於TCP的KeepAlive

正常情況下雙方建立連接后是不會斷開的,KeepAlive就是防止連接雙方中的任意一方由於意外斷開而通知不到對方,導致對方一直持有連接,占用資源(發現對方不可用,斷開連接)。

*建立連接需要三次握手、正常斷開連接需要四次揮手。

KeepAlive有三個核心參數

net.ipv4.tcp_keepalive_timeout:連接的超時時間(默認7200s)

net.ipv4.tcp_keepalive_intvl:發送探測包的間隔(默認75s)

tnet.ipv4.cp_keepalive_probes:發送探測包的個數(默認9個)

這三個參數都是系統參數,會影響部署在機器上的所有應用。

KeepAlive的開關是在應用層開啟的,只有當應用層開啟了KeepAlive,KeepAlive才會生效。

java.net.Socket.setKeepAlive(boolean on);

當連接在指定時間內沒有發送請求時,開啟KeepAlive的一端就會向對方發送一個探測包,如果對方沒有回應,則每隔指定時間發送一個探測包,總共發送指定個探測包,如果對方都沒有回應則認為對方不可用,斷開連接。

為什么要做應用層的KeepAlive

1.KeepAlive參數是系統參數,對於應用來說不夠靈活。

2.默認檢測一個不可用的連接所需要的時間太長。

怎么做應用層的KeepAlive

1.定時任務

客戶端定期向所有已經建立連接的服務端發送心跳檢測,如果服務端連續沒有回應指定個心跳檢測,則認為對方不可用,此時客戶端應該重連。

服務端定期向所有已經建立連接的客戶端發送心跳檢測,如果客戶端連續沒有回應指定個心跳檢測,則認為對方不可用,此時應該斷開連接。

2.計時器

連接在指定時間內沒有發送請求則認為對方不可用

Netty對KeepAlive的支持

Netty開啟KeepAlive

Bootstrap.option(ChannelOption.SO_KEEPALIVE,true);
ServerBootstrap.childOption(ChannelOption.SO_KEEPALIVE,true);

Netty提供的KeepAlive機制

Netty提供的IdleStateHandler能夠檢測處於Idle狀態的連接。

Idle狀態類型

reader_idle:SocketChannel在指定時間內都沒有數據可讀

writer_idle:SocketChannel在指定時間內沒有寫入數據

all_idle:SocketChannel在指定時間內沒有數據可讀或者沒有寫入數據

直接將IdleStateHandler添加到ChannelPipeline即可,當Netty檢測到處於Idle狀態的連接時,將會自動調用其Handler的userEventTriggered()方法,用戶只需要在該方法中判斷Idle狀態的類型,然后做出相應的處理。

關於HTTP的KeepAlive

HTTP的KeepAlive是對長連接和短連接的選擇,並不是發現對方不可用,斷開連接。

HTTP是基於請求和響應的,客戶端發送請求給服務端然后等待服務端的響應,當服務端檢測到請求頭中包含Connection:KeepAlive時,表示客戶端使用長連接,此時服務端應該保持連接,當檢測到請求頭中包含Connection:close時,表示客戶端使用短連接,此時服務端應該主動斷開連接。

TCP並不是基於請求和響應的,客戶端可以發送請求給服務端,同時服務端也可以發送請求給客戶端。


免責聲明!

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



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