初識Netty 二 (粘包拆包的簡單處理DelimiterBasedFrameDecode自定義分隔符)

上一節我們基於netty簡單構建了一個客戶端服務器,相互發送消息,但是前提是不考慮粘包和拆包的情況,今天我們來分析下粘包和拆包

源碼

什麼是粘包,什麼是拆包

熟悉TCP的都知道,TCP是一個"流協議",所謂的流,就是沒有界限的一串數據,連成一片的,比如我們客戶端發送2個數據包A、B給服務器,而每次發送包的多少具體會根據TCP緩衝區的實際情況進行包的劃分,所以A、B發送到服務器就會出現如下問題

UDP就像郵寄包裹一樣,雖然一次也是運輸多個,但是每個包裹都有界限,一個一個簽收,所以不會出現粘包、半包問題

  1. 服務端分兩次讀取到兩個獨立的數據包,分別是A和B,沒有粘包和拆包
    在這裏插入圖片描述
  2. 服務端一次接收到兩個數據包,A和B粘和在一起,發生了TCP粘包
    在這裏插入圖片描述
  3. 服務端分兩次讀取到了兩個數據包,第一次讀取到了A包的全部和B部分包,第二次讀取到了B包的剩下包內容,發生了TCP拆包
    在這裏插入圖片描述
  4. 服務端分兩次讀取到了兩個數據包,第一次讀取到了A包的部分,第二次讀取到了A包剩下的部分+B包的全部
    在這裏插入圖片描述
  5. 服務器TCP接受滑窗非常小,A和B數據包比較大,需要分很多次才能將A和B包完全接受,期間發生多次拆包

解決此類問題的根本手段:找出消息的邊界

方法 尋找消息邊界的方式 優點 缺點 推薦度
TCP連接改爲短連接,一個請求一個短連接 建立連接釋放連接之間的信息即爲傳輸的信息 簡單 效率低下 不推薦
固定長度 消息統一滿足固定長度,不足補零或者其他 簡單 浪費空間 不推薦
分隔符 分隔符之間 空間不浪費,也比較簡單 內容本身出現分隔符需要轉義,需要掃描全部內容 推薦
固定長度字段存內容的長度信息 先解析固定長度的字段獲取長度,然後讀取後續內容 精確定位數據,內容不用轉義 長度理論上有限制,需提前預支可能的最大長度從而定義長度佔用字節 推薦
其他方式 例如JSON以{}是否成對出現

代碼實現

這裏是基於按行分隔符切換的文本解析碼: LineBasedFrameDecoder + StringDecoder

代碼實現:

服務器端

NettyServer

@Slf4j
public class NettyServer {

    public static void main(String[] args) {
        int port = 8080;
        // 創建鏈各個Reactor線程租,一個用於服務端接受客戶端連接
        // 一個用於SocketChannel的網絡讀寫
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // netty 用於啓動NIO服務端的輔助啓動類,不低降低服務端開發難度
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)//配置NioServerSocketChannel 的TCP 參數
                    .childHandler(new ChildChannelHeandler());// 綁定I/O事件處理類 ChildChannelHeandler
            log.info("服務器啓動" + "端口 {}", port);
            // 綁定端口,調用同步阻塞等待成功
            ChannelFuture f = b.bind(port).sync();
            //等待服務端監聽端口關閉
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();

        }
    }

}

class ChildChannelHeandler extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
        socketChannel.pipeline().addLast(new StringDecoder());
        socketChannel.pipeline().addLast(new TimeServerHandler());
        //添加解碼器 StringDecoder 和 LineBasedFrameDecoder

    }
}

相比之前添加了 LineBasedFrameDecoder 和 StringDecoder解析器

TimeServerHandler

/**
 * @author WH
 * @version 1.0
 * @date 2020/5/24 21:48
 * @Description 繼承ChannelHandlerAdapter 對網絡事件進行讀寫
 */
@Slf4j
public class TimeServerHandler extends ChannelHandlerAdapter {

    private int count;

    //客戶端返回結果調用
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{
        System.out.println("返回消息");
        String body = (String) msg;
        //請求信息
        log.info("客戶端請求參數boyd: {}", body);
        // 以空格爲消息分割符號來進行包的拆分
        String message = "服務端收到了你的消息:" + body + System.getProperty("line.separator");
        log.info("服務端收到的消息總數count: {}", ++count);
        ByteBuf resp = Unpooled.copiedBuffer(message.getBytes());
        ctx.writeAndFlush(resp);

    }


    // 客戶端斷開連接監聽
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.info("客戶端斷開了連接");
        ctx.close();
    }

}

消息直接獲取 返回消息添加空格 System.getProperty(“line.separator”)

客戶端

NettyClient

public class NettyClient {

    public static void main(String[] args) {
        //配置客戶端NIO線程租
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY, true)
            .handler(new ChannelInitializer<SocketChannel>() {

                @Override
                public void initChannel(SocketChannel ch) throws Exception{
                    ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new TimeClinetHandler());
                    //添加解碼器



                }

            });
            //發起異步連接操作
            ChannelFuture f = b.connect("127.0.0.1", 8080).sync();
            //等待客戶端鏈路關閉
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //優雅退出,釋放NIO線程租
            group.shutdownGracefully();
        }

    }
}

相比之前添加了 LineBasedFrameDecoder 和 StringDecoder解析器

TimeClinetHandler

@Slf4j
public class TimeClinetHandler extends ChannelHandlerAdapter {

    private byte[] req;
    private int count;

    public TimeClinetHandler() {
        // 以空格爲消息分割符號來進行包的拆分
        String meg = "你好服務器" + System.getProperty("line.separator");
        req = meg.getBytes();
        log.info("發送消息 req: {}", new String(req));
    }

    //當服務器TCP鏈路建立成功後,調用 channelActive 方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ByteBuf firstMessage = null;
        log.info("開始發送消息");
        for (int i = 0; i < 100; i++) {
            firstMessage = Unpooled.buffer(req.length);
            firstMessage.writeBytes(req);
            ctx.writeAndFlush(firstMessage);
        }

    }

    //當服務器返回應答消息時,調用 channelRead 方法
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{
        //直接獲取解碼的字符串
        String body = (String) msg;
        log.info("服務器返回消息爲:{},消息總數爲{}",body,++count);
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.warn("發生異常" + cause.getMessage());
        ctx.close();
    }

}

消息直接獲取 返回消息添加空格 System.getProperty(“line.separator”)

運行

在這裏插入圖片描述

在這裏插入圖片描述

ps 如果不使用解析器會發現服務器接受到的消息就只要兩條,這裏是正常的

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