netty(十四)Netty提升 - 粘包與半包 一、現象分析 二、粘包、半包分析 三、粘包和半包的解決方案

一、現象分析

1.1 粘包

通過代碼的方式演示下粘包的現象:

服務端:

public class HalfPackageServer {

    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            //設置服務器接收端緩衝區大小爲10
            serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            //此處打印輸出,看看收到的內容是10次16字節,還是一次160字節
                            printBuf((ByteBuf) msg);
                            super.channelRead(ctx, msg);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            //阻塞等待連接
            channelFuture.sync();
            //阻塞等待釋放連接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error:" + e);
        } finally {
            // 釋放EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

    static void printBuf(ByteBuf byteBuf){
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i< byteBuf.writerIndex();i++) {
            stringBuilder.append(byteBuf.getByte(i));
            stringBuilder.append(" ");
        }

        stringBuilder.append("| 長度:");
        stringBuilder.append(byteBuf.writerIndex());
        stringBuilder.append("字節");
        System.out.println(stringBuilder);
    }
}

客戶端:

public class HalfPackageClient {
    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        //建立連接成功後,會觸發active事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            //循環發送,每次16字節,共10次
                            for (int i = 0; i < 10; i++) {
                                ByteBuf buffer = ctx.alloc().buffer();
                                buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
                                ctx.writeAndFlush(buffer);
                            }
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
            //釋放連接
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            System.out.println("client error :" + e);
        } finally {
            //釋放EventLoopGroup
            worker.shutdownGracefully();
        }
    }
}

結果:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 長度:160字節

如上所示,發送了10次的16個字節,接收到了一個160字節,而不是10個16字節。這就是粘包現象。

1.2 半包

半包現象我們仍然使用前面的代碼,但是服務端需要多設置一個屬性,即修改服務端接收緩衝區的buffer大小,我們這裏修改爲10個字節。

serverBootstrap.option(ChannelOption.SO_RCVBUF,10);

這行代碼起初運行完我有點不理解,明明設置是10,但是接收到的內容確是40,可見這個設置的值,不是10個字節,通過源碼跟蹤,我發現這個數值ChannelOption的泛型是個Ingteger,估計是進行了類型的計算,一個Integer是4個字節,所以有了設置是10,最終卻接收40個字節。

如果同學們覺得這個問題說的不對的話,可以幫我指正一下。感謝!!

public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");

服務端代碼:

public class HalfPackageServer {

    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            //設置服務器接收端緩衝區大小爲10
            serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            //此處打印輸出,看看收到的內容是10次16字節,還是一次160字節
                            printBuf((ByteBuf) msg);
                            super.channelRead(ctx, msg);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            //阻塞等待連接
            channelFuture.sync();
            //阻塞等待釋放連接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error:" + e);
        } finally {
            // 釋放EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

    static void printBuf(ByteBuf byteBuf){
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i< byteBuf.writerIndex();i++) {
            stringBuilder.append(byteBuf.getByte(i));
            stringBuilder.append(" ");
        }

        stringBuilder.append("| 長度:");
        stringBuilder.append(byteBuf.writerIndex());
        stringBuilder.append("字節");
        System.out.println(stringBuilder);
    }
}

客戶端與前面的粘包相同。

結果:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 | 長度:36字節
4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 | 長度:40字節
12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 | 長度:40字節
4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 | 長度:40字節
12 13 14 15 | 長度:4字節

如上所示,總共160字節,總共發送了五次,每一次都含有不完整16字節的數據。這就是半包,其實裏面也包含的粘包。

至於爲什麼第一個只有36,這裏沒有具體分析,但是我們可以猜想下,每次連接的首次應該是有表示佔用了4個字節。

二、粘包、半包分析

產生粘包和半包的本質:TCP是流式協議,消息是無邊界的

2.1 滑動窗口

TCP是一種可靠地傳出協議,每發送一個段就需要進行一次確認應答(ack)處理。如何沒收到ack,則會再次發送。

但是如果對於一個客戶端來說,每次發送一個請求,都要等到另一個客戶端應該的話,才能發送下一個請求,那麼整個構成就成了串行化的過程,大大降低了連接間的傳輸效率。

TCP如何解決效率地下的問題?

引入滑動窗口。窗口大小即決定了無需等待應答而可以繼續發送的數據最大值。

簡易滑動過程如下所示:

窗口實際就起到一個緩衝區的作用,同時也能起到流量控制的作用。

  • 只有窗口內的數據才允許被髮送(綠色),當應答(藍色)未到達前,窗口必須停止滑動
  • 如果 0-100 這個段的數據 ack 回來了,窗口就可以向下滑動,新的數據段會被加入進來進行發送。
  • 接收方也會維護一個窗口,只有落在窗口內的數據才能允許接收

2.2 粘包現象分析

  • 現象,發送了10次的16個字節,接收到了一個160字節,而不是10個16字節。

  • 產生原因:

    • 應用層:接收方 ByteBuf 設置太大(Netty 默認 1024)
    • 滑動窗口(TCP):假設發送方 256 bytes 表示一個完整報文,但由於接收方處理不及時且窗口大小足夠大,這 256 bytes 字節就會緩衝在接收方的滑動窗口中,當接收方滑動窗口中緩衝了多個報文就會粘包。
    • Nagle 算法(TCP):會造成粘包

2.3 半包現象分析

  • 現象,前面的半包示例代碼發送了10次的16個字節,接收到的數據有部分是被階段的,不是完整的16字節。

  • 產生原因

    • 應用層:接收方 ByteBuf 小於實際發送數據量
    • 滑動窗口(TCP):假設接收方的窗口只剩了 128 bytes,發送方的報文大小是 256 bytes,這時放不下了,只能先發送前 128 bytes,等待 ack 後才能發送剩餘部分,這就造成了半包
    • MSS 限制:當發送的數據超過 MSS 限制後,會將數據切分發送,就會造成半包

2.4 引申:Nagle 算法 和 MSS限制

2.4.1 Nagle算法

TCP中爲了提高網絡的利用率,經常使用一個叫做Nagle的算法。

該算法是指發送端即使還有應該發送的數據,但如果這部分數據很少的話,則進行延遲發送的一種處理機制。

如果以下兩個條件都不滿足時,則進行一段時間延遲後,才進行發送:

  • 已發送的數據都已經收到確認應答時
  • 可以發送最大段長度(MSS)的數據時

根據這個算法雖然網絡利用率可以提高,但是可能會發生某種程度的延遲。對於時間要求準確的系統,往往需要關閉該算法。

在Netty中如下的條件:

  • 如果 SO_SNDBUF 的數據達到 MSS(maximum segment size),則需要發送。
  • 如果 SO_SNDBUF 中含有 FIN(表示需要連接關閉)這時將剩餘數據發送,再關閉。
  • 如果 TCP_NODELAY = true,則直接發送。
  • 已發送的數據都收到 ack 時,則需要發送。
  • 上述條件不滿足,但發生超時(一般爲 200ms)則需要發送。
  • 除上述情況,延遲發送

2.4.2 MSS限制

MSS 限制

數據鏈路層對一次能夠發送的最大數據有限制,每種數據鏈路的最大傳輸單元(MTU)都不盡相同。

  • 以太網的 MTU 是 1500
  • FDDI(光纖分佈式數據接口)的 MTU 是 4352
  • 本地迴環地址的 MTU 是 65535 - 本地測試不走網卡

MSS 是最大段長度(maximum segment size),它是 MTU 刨去 tcp 頭和 ip 頭後剩餘能夠作爲數據傳輸的字節數

  • ipv4 tcp 頭佔用 20 bytes,ip 頭佔用 20 bytes,因此以太網 MSS 的值爲 1500 - 40 = 1460
  • TCP 在傳遞大量數據時,會按照 MSS 大小將數據進行分割發送
  • MSS 的值在三次握手時通知對方自己 MSS 的值,然後在兩者之間選擇一個小值作爲 MSS

三、粘包和半包的解決方案

3.1 短連接

每當客戶端。發送一條消息後,就與服務器斷開連接。

我們需要對客戶端的代碼進行改造,其實就是,將內部發送10次消息的循環抽出來,一次連接只發送一個消息,需要建立10次連接,這個我就不演示了,不用想都能得到結果:

  • 客戶端發送一定不會產生粘包問題。
  • 服務端只要接收緩衝區足夠大,一定不會產生半包的問題,但是不能完全保證。

此方案的缺點:
1)建立大量的鏈接,效率低。
2)不能解決半包問題。

3.2 固定長度消息

Netty針對固定長度消息提供了一個入站處理器FixedLengthFrameDecoder,能夠制定接收消息的固定長度。

服務端:

public class FixedLengthServer {

    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            //此處打印輸出,看看收到的內容是10次16字節,還是一次160字節
                            printBuf((ByteBuf) msg);
                            super.channelRead(ctx, msg);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            //阻塞等待連接
            channelFuture.sync();
            //阻塞等待釋放連接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error:" + e);
        } finally {
            // 釋放EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

    static void printBuf(ByteBuf byteBuf) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < byteBuf.writerIndex(); i++) {
            stringBuilder.append(byteBuf.getByte(i));
            stringBuilder.append(" ");
        }

        stringBuilder.append("| 長度:");
        stringBuilder.append(byteBuf.writerIndex());
        stringBuilder.append("字節");
        System.out.println(stringBuilder);
    }
}

客戶端:

public class FixedLengthClient {
    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        //建立連接成功後,會觸發active事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 發送內容隨機的數據包
                            Random r = new Random();
                            char c = 1;
                            ByteBuf buffer = ctx.alloc().buffer();
                            for (int i = 0; i < 10; i++) {
                                byte[] bytes = new byte[8];
                                for (int j = 0; j < r.nextInt(8); j++) {
                                    bytes[j] = (byte) c;
                                }
                                c++;
                                buffer.writeBytes(bytes);
                            }
                            ctx.writeAndFlush(buffer);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
            //釋放連接
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            System.out.println("client error :" + e);
        } finally {
            //釋放EventLoopGroup
            worker.shutdownGracefully();
        }
    }
}

結果:

1 1 0 0 0 0 0 0 | 長度:8字節
2 2 2 2 0 0 0 0 | 長度:8字節
3 0 0 0 0 0 0 0 | 長度:8字節
4 4 4 0 0 0 0 0 | 長度:8字節
5 5 5 5 0 0 0 0 | 長度:8字節
6 0 0 0 0 0 0 0 | 長度:8字節
7 0 0 0 0 0 0 0 | 長度:8字節
8 8 0 0 0 0 0 0 | 長度:8字節
9 9 9 0 0 0 0 0 | 長度:8字節
10 10 10 10 10 0 0 0 | 長度:8字節

使用固定長度也有一定的缺點,消息長度不好確定:
1)過大,造成空間浪費。
2)過小,對於某些數據包可能不夠。

3.3 分隔符

1)Netty提供了LineBasedFrameDecoder(換行符幀解碼器)。

默認以 \n 或 \r\n 作爲分隔符,需要指定最大長度,如果超出指定長度仍未出現分隔符,則拋出異常。

2)Netty提供了DelimiterBasedFrameDecoder(自定義分隔符幀解碼器)。
需要自己指定一個ByteBuf類型的分隔符,且需要指定最大長度。

LineBasedFrameDecoder示例代碼:

服務端

public class LineBasedServer {

    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf byteBuf = (ByteBuf) msg;
                            System.out.println(byteBuf.toString(StandardCharsets.UTF_8));
                            super.channelRead(ctx, msg);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            //阻塞等待連接
            channelFuture.sync();
            //阻塞等待釋放連接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error:" + e);
        } finally {
            // 釋放EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

客戶端:

public class LineBasedClient {
    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        //建立連接成功後,會觸發active事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 發送帶有分隔符的數據包
                            ByteBuf buffer = ctx.alloc().buffer();
                            String str = "hello world\nhello world\n\rhello world\nhello world";
                            buffer.writeBytes(str.getBytes(StandardCharsets.UTF_8));
                            ctx.writeAndFlush(buffer);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
            //釋放連接
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            System.out.println("client error :" + e);
        } finally {
            //釋放EventLoopGroup
            worker.shutdownGracefully();
        }
    }
}

結果:

hello world
hello world
hello world

DelimiterBasedFrameDecoder代碼示例,大體與前一種相同,下面只給出不同位置的代碼:

服務端:

protected void initChannel(SocketChannel ch) throws Exception {
                    ByteBuf buffer = ch.alloc().buffer();
                    buffer.writeBytes("||".getBytes(StandardCharsets.UTF_8));
                    ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buffer));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf byteBuf = (ByteBuf) msg;
                            System.out.println(byteBuf.toString(StandardCharsets.UTF_8));
                            super.channelRead(ctx, msg);
                        }
                    });
                }

客戶端:

String str = "hello world||hello world||hello world||hello world";

使用分隔符的這種方式,也有其缺點:處理字符數據比較合適,但如果內容本身包含了分隔符,那麼就會解析錯誤。逐個字節去比較,相率也不是很好。

3.4 預設長度

LengthFieldBasedFrameDecoder長度字段解碼器,允許我們在發送的消息當中,指定消息長度,然後會根據這個長度取讀取響應的字節數。

public LengthFieldBasedFrameDecoder(
            int maxFrameLength,
            int lengthFieldOffset, 
            int lengthFieldLength,
            int lengthAdjustment, 
            int initialBytesToStrip) 

主要看這個有5個字段的構造器,下面我們分別看每個字段的含義是什麼。

字段含義我先列在這:

maxFrameLength:最大長度。
lengthFieldOffset:長度字段偏移量。
lengthFieldLength:長度字段長度。
lengthAdjustment:以長度字段爲基準,還有幾個字節是內容。
initialBytesToStrip:從頭剝離幾個字節。

通過Netty提供的幾個例子,解釋這幾個字段的意思。

例子1:

  • lengthFieldOffset = 0
  • lengthFieldLength = 2
  • lengthAdjustment = 0
  • initialBytesToStrip = 0

則發送前,總共14個字節,其中lengthFieldLength佔據兩個字節,0x000C表示消息長度是12:

Length Actual Content
0x000C "HELLO, WORLD"

接收解析後,仍然是14個字節:

Length Actual Content
0x000C "HELLO, WORLD"

例子2:

  • lengthFieldOffset = 0
  • lengthFieldLength = 2
  • lengthAdjustment = 0
  • initialBytesToStrip = 2

則發送前,總共14個字節,其中lengthFieldLength佔據兩個字節,0x000C表示消息長度是12,initialBytesToStrip表示從頭開始剝離2個字節,則解析後,將長度的字段剝離掉了:

Length Actual Content
0x000C "HELLO, WORLD"

接收解析後,只有12個字節:

Actual Content
"HELLO, WORLD"

例子3:

  • lengthFieldOffset = 2
  • lengthFieldLength = 3
  • lengthAdjustment = 0
  • initialBytesToStrip = 0

則發送前,總共17個字節,其中lengthFieldLength佔據3個字節,長度字段偏移量是2,偏移量用來存放魔數Header 1:

Header 1 Length Actual Content
0xCAFE 0x00000C "HELLO, WORLD"

接收解析後,只有17個字節:

Header 1 Length Actual Content
0xCAFE 0x00000C "HELLO, WORLD"

例子4:

  • lengthFieldOffset = 0
  • lengthFieldLength = 3
  • lengthAdjustment = 2
  • initialBytesToStrip = 0

則發送前,總共17個字節,其中lengthFieldLength佔據3個字節,lengthAdjustment 表示從長度字段開始,還有2個字節是內容:

Length Header 1 Actual Content
0x00000C 0xCAFE "HELLO, WORLD"

接收解析後,只有17個字節:

Length Header 1 Actual Content
0x00000C 0xCAFE "HELLO, WORLD"

例子5:

  • lengthFieldOffset = 1
  • lengthFieldLength = 2
  • lengthAdjustment = 1
  • initialBytesToStrip = 3

則發送前,總共17個字節,長度字段便宜1個字節,長度字段爲2個字節,從長度字段開始,還有一個字節後是內容,忽略前三個字節。

Header 1 Length Header 2 Actual Content
0xCA 0x000C 0xFE "HELLO, WORLD"

接收解析後,只有13個字節:

Header 2 Actual Content
0xFE "HELLO, WORLD"

模擬例子5,寫一段實例代碼,其中內容略有不同:

示例代碼:

服務端

/**
 * @description: TODO
 * @author:weirx
 * @date:2021/11/12 15:34
 * @version:3.0
 */
public class LineBasedServer {

    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,1,4,1,5));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf byteBuf = (ByteBuf) msg;
                            System.out.println(byteBuf.readByte() + "|" + byteBuf.toString(StandardCharsets.UTF_8));
                            super.channelRead(ctx, msg);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            //阻塞等待連接
            channelFuture.sync();
            //阻塞等待釋放連接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error:" + e);
        } finally {
            // 釋放EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

客戶端:

public class LineBasedClient {
    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        //建立連接成功後,會觸發active事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                           send(ctx,"hello, world");
                            send(ctx,"HI!");
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
            //釋放連接
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            System.out.println("client error :" + e);
        } finally {
            //釋放EventLoopGroup
            worker.shutdownGracefully();
        }
    }

    static void send(ChannelHandlerContext ctx,String msg){
        ByteBuf buffer = ctx.alloc().buffer();
        byte[] bytes = msg.getBytes();
        int length = bytes.length;
        //先寫Header 1
        buffer.writeByte(1);
        //再寫長度
        buffer.writeInt(length);
        //再寫Header 2
        buffer.writeByte(2);
        //最後寫內容
        buffer.writeBytes(bytes);
        ctx.writeAndFlush(buffer);
    }
}

結果:

2|hello, world
2|HI!

關於粘包和半包的介紹就這麼多了,有用的話,幫忙點個贊吧~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章