首頁 > 軟體

Netty粘包拆包及使用原理詳解

2022-08-01 14:00:15

為什麼使用Netty框架

  • NIO的類庫和API繁雜,使用麻煩,你需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  • 需要具備其他的額外技能做鋪墊,例如熟悉Java多執行緒程式設計。這是因為NIO程式設計涉及到 Reactor 模式,你必須對多執行緒和網路程式設計非常熟悉,才能編寫出高質量的NIO程式。
  • 可靠效能力補齊,工作量和難度都非常大。例如使用者端面臨斷連重連、網路閃斷、半包讀寫、失敗快取、網路擁塞和異常碼流的處理等問題,NIO程式設計的特點是功能開發相對容易,但是可靠效能力補齊的工作量和難度都非常大。
  • JDK NIO的BUG,例如臭名昭著的 epoll bug,它會導致Selector空輪詢,最終導致CPU 100%。官方聲稱在JDK1.6版本的update18修復了該問題,但是直到JDK1.7版本該問題仍舊存在,只不過該BUG發生概率降低了一些而已,它並沒有被根本解決。該BUG以及與該BUG相關的問題單可以參見以下連結內容。

由於上述原因,在大多數場景下,不建議大家直接使用JDK的NIO類庫,除非你精通NIO程式設計或者有特殊的需求。在絕大多數的業務場景中,我們可以使用NIO框架Netty來進行NIO程式設計,它既可以作為使用者端也可以作為伺服器端,同時支援UDP和非同步檔案傳輸,功能非常強大。

Netty框架介紹

Netty是業界最流行的NIO框架之一,它的健壯性、功能、效能、可客製化性和可延伸性在同類框架中都是首屈一指的,它已經得到成百上千的商用專案驗證,例如Hadoop的RPC框架Avro就使用了Netty作為底層通訊框架,其他還有業界主流的RPC框架,也使用Netty來構建高效能的非同步通訊能力。

優點總結:

  • API使用簡單,開發門檻低;
  • 功能強大,預置了多種編解碼功能,支援多種主流協定;
  • 客製化能力強,可以通過ChannelHandler對通訊框架進行靈活地擴充套件;
  • 效能高,通過與其他業界主流的NIO框架對比,Netty的綜合效能最優;
  • 成熟、穩定,Netty修復了已經發現的所有JDK NIO BUG,業務開發人員不需要再為NIO的BUG而煩惱;
  • 社群活躍,版本迭代週期短,發現的BUG可以被及時修復,同時,更多的新功能會加入;
  • 經歷了大規模的商業應用考驗,質量得到驗證。Netty在網際網路、巨量資料、網路遊戲、企業應用、電信軟體等眾多行業已經得到了成功商用,證明它已經完全能夠滿足不同行業的商業應用了。

Netty實戰

首先引入Netty的jar包。

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.42.Final</version>
</dependency>

Netty編寫伺服器端

NettyServer 類

public class NettyServer {
    /**
     * netty啟動埠號
     */
    private static int port = 8080;
    public static void main(String[] args) {
        /**
         *  使用者端建立兩個執行緒池組分別為 boss執行緒組和工作執行緒組
         */
        // 用於接受使用者端連線的請求 (並沒有處理請求)
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        // 用於處理使用者端連線的讀寫操作(處理請求操作)
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        //NioServerSocketChannel   標記當前是伺服器端
        serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        // 設定我們分割最大長度為1024
                        socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
                        // 獲取資料的結果為string型別
                        socketChannel.pipeline().addLast(new StringEncoder());
                         //處理每個handler(也就是每次使用者端請求)
                        socketChannel.pipeline().addLast(new ServerHandler());
                    }
                });
        try {
            //繫結埠號
            ChannelFuture bind = serverBootstrap.bind(port);
            ChannelFuture sync = bind.sync();
            System.out.println("伺服器端啟動成功:" + port);
            //等待監聽我們的請求
            sync.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //優雅的關閉我們的執行緒池
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

ServerHandler 類

public class ServerHandler extends SimpleChannelInboundHandler {
    /*
     * @Author kaico
     * @Date 9:56 2020/10/8
     * @Description //TODO 獲取資料
     * @Param [channelHandlerContext, o]
     * @return void
     **/
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        ByteBuf byteBuf = (ByteBuf) o;
        String request = byteBuf.toString(CharsetUtil.UTF_8);
        System.out.println("request:" + request);
        // 響應內容:
        channelHandlerContext.writeAndFlush(Unpooled.copiedBuffer("這裡是Netty伺服器端n", CharsetUtil.UTF_8));
    }
}

Netty使用者端

NettyClient 類

public class NettyClient {
    public static void main(String[] args) {
        //建立nioEventLoopGroup
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group).channel(NioSocketChannel.class)
                .remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        // 設定我們分割最大長度為1024
                        ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                        // 獲取資料的結果為string型別
                        ch.pipeline().addLast(new StringEncoder());
                        ch.pipeline().addLast(new ClientHandler());
                    }
                });
        try {
            // 發起同步連線
            ChannelFuture sync = bootstrap.connect().sync();
            sync.channel().closeFuture().sync();
        } catch (Exception e) {

        } finally {
            group.shutdownGracefully();
        }
    }
}

ClientHandler 類

public class ClientHandler extends SimpleChannelInboundHandler {
    /**
     * 活躍通道可以傳送訊息
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 10; i++) {
            // 傳送資料
            ctx.writeAndFlush(Unpooled.copiedBuffer("你是什麼型別的伺服器端啊?n", CharsetUtil.UTF_8));
        }
        //使用者端發十條訊息
    }
    /**
     * 讀取訊息
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("resp:" + byteBuf.toString(CharsetUtil.UTF_8));
    }
}

粘包與拆包

原因:因為我們現在tcp協定預設是長連線形式實現通訊,傳送請求完了之後整個連線暫時不會關閉

1.短連線

使用者端與伺服器端建立連線的時候,使用者端傳送一條訊息,使用者端與伺服器連線關閉

2.長連線

使用者端與伺服器端建立連線的時候,使用者端傳送多條訊息,使用者端與伺服器連線關閉

什麼是粘包:多次傳送的訊息,伺服器一次合併讀取msgmsg

什麼是拆包:多次傳送訊息 伺服器讀取第一條資料完整+第二條不完整資料 第二條不完整資料 Msgm sg

為什麼會造成拆包和粘包? 前提長連線、其次緩衝區

原因的造成:

Tcp協定為了能夠高效能的傳輸資料,傳送和接受時候都會採用緩衝區,必須等待緩衝區滿了以後才可以傳送或者讀取;

當我們的應用程式如果傳送的資料大於了我們的套位元組的緩衝區大小的話,就會造成了拆包。拆分成多條訊息讀取。當我們應用程式如果傳送的寫入的訊息如果小於套位元組緩衝區大小的時候

粘包與拆包產生的背景:

Tcp協定為了高效能的傳輸,傳送和接受的時候都採用了緩衝區

3. 當我們的應用程式傳送的資料大於套位元組緩衝區的時候,就會實現拆包。

4. 當我們的應用程式寫入的資料小於套位元組緩衝區的時候,多次傳送的訊息會合併到一起接受,這個過程我們可以稱做為粘包。

5. 接受端不夠及時的獲取緩衝區的資料,也會產生粘包的問題

6. 進行mss(最大報文長度)大小的TCP分段,當TCP報文長度-TCP頭部長度>mss的時候將發生拆包。

解決思路:

7. 以固定的長度傳送資料,到緩衝區

8. 可以在資料之間設定一些邊界(n或者rn)

9. 利用編碼器LineBaseDFrameDecoder解決tcp粘包的問題

常用編碼器:

  • DelimiterBasedFrameDecoder 解決TCP的粘包解碼器
  • StringDecoder 訊息轉成String解碼器
  • LineBasedFrameDecoder 自動完成識別符號分隔解碼器
  • FixedLengthFrameDecoder 固定長度解碼器,二進位制
  • Base64Decoder 解碼器

利用編碼器LineBaseDFrameDecoder解決tcp粘包的問題的Java程式碼案例,核心思路就是增加邊界 n

伺服器端類 NettyServer 的修改點

serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                // 設定我們分割最大長度為1024
                socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
                // 獲取資料的結果為string型別
                socketChannel.pipeline().addLast(new StringEncoder());
                //傳送資料的時候設定邊界 n
                socketChannel.pipeline().addLast(new ServerHandler());
            }
        });

伺服器端類 ServerHandler 的修改點

@Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        ByteBuf byteBuf = (ByteBuf) o;
        String request = byteBuf.toString(CharsetUtil.UTF_8);
        System.out.println("request:" + request);
        // 響應內容:
        channelHandlerContext.writeAndFlush(Unpooled.copiedBuffer("這裡是Netty伺服器端n", CharsetUtil.UTF_8));
    }

使用者端的類 NettyClient 的修改點

bootstrap.group(group).channel(NioSocketChannel.class)
                .remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        // 設定我們分割最大長度為1024
                        ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                        // 獲取資料的結果為string型別
                        ch.pipeline().addLast(new StringEncoder());
                        ch.pipeline().addLast(new ClientHandler());
                    }
                });

使用者端的類 ClientHandler 的修改點

 @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 10; i++) {
            // 傳送資料
            ctx.writeAndFlush(Unpooled.copiedBuffer("你是什麼型別的伺服器端啊?n", CharsetUtil.UTF_8));
        }
        //使用者端發十條訊息
    }

到此這篇關於Netty粘包拆包詳解及實戰流程的文章就介紹到這了,更多相關Netty粘包拆包內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com