tcp的粘包和拆包示例以及使用LengthFieldFrameDecoder來解決的方法

粘包和拆包是什麼?

TCP協議是一種字節流協議,沒有記錄邊界,我們在接收消息的時候,不能人爲接收到的數據包就是一個整包消息

當客戶端向服務器端發送多個消息數據的時候,TCP協議可能將多個消息數據合併成一個數據包進行發送,這就是粘包

當客戶端向服務器端發送的消息過大的時候,tcp協議可能將一個數據包拆成多個數據包來進行發送,這就是拆包

以下一netty爲例,展示一下tcp粘包和拆包的例子:

ServerBusinessHanler:

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.nio.charset.Charset;
import java.util.UUID;

public class ServerBusinessHanler extends SimpleChannelInboundHandler<ByteBuf> {

    int count = 0;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        byte[] bytes = new byte[msg.readableBytes()];
        msg.readBytes(bytes);
        String message = new String(bytes, Charset.forName("UTF-8"));
        System.out.println("從客戶端收到的字符串:" + message);
        System.out.println("服務端接收到的請求數:" + (++count));
        ctx.writeAndFlush(Unpooled.copiedBuffer(UUID.randomUUID().toString(),Charset.forName("UTF-8")) );

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println(cause.getStackTrace());
        ctx.channel().close();
    }

}

Server:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;


public class Server {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                        
                            ch.pipeline().addLast(new ServerBusinessHanler());
                        }
                    });
            serverBootstrap.bind(new InetSocketAddress(8899)).sync().channel().closeFuture().sync();

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}    

ClientBusinessHandler:

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;    
import java.nio.charset.Charset;

public class ClientBusinessHandler extends SimpleChannelInboundHandler<ByteBuf> {
    int count = 1;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        byte[] bytes = new byte[msg.readableBytes()];
        msg.readBytes(bytes);
        System.out.println("從服務器端接收到的信息: " + new String(bytes, Charset.forName("UTF-8")));
        System.out.println("從服務器端讀到的請求的個數: " + count++);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 10; i++) {
            ctx.writeAndFlush(Unpooled.copiedBuffer("中國移動".getBytes()));
        }
        ctx.writeAndFlush(Unpooled.copiedBuffer("jiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyipingjiaoyiping".getBytes()));
    }
}

Client:

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;


public class Client {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup eventExecutors = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(eventExecutors)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new ClientBusinessHandler());
                    }
                });
        Channel channel = bootstrap.connect("127.0.0.1", 8899).channel();
        channel.closeFuture().sync();

    }
}    

分別運行服務器端和客戶端,我們看到服務器端的輸出:

相應的,服務器端也只給客戶端了兩個返回,而不是我們期待的11個

我們從客戶端傳入的十個 "中國移動"和第二次發送的長的字符串的前一部分發生了粘包,而第二個長字符串的後一部分則被拆分到第二個數據包裏了

粘包和拆包的情況都出現了

那怎麼解決粘包和拆包的問題呢? 一種方法是在字符串中使用特定的分隔符來分隔,另外一種方法是,我們在發送數據包的時候在前邊附加上數據包的長度

netty爲我們提供了多種的解碼器,比如定長解碼器和基於分隔符的解碼器等,這裏,我麼使用 LengthFieldBasedFrameDecoder這個解碼器來解決粘包和拆包的問題,關於這個解碼器的詳細信息,可以參考這個類的java doc文檔,上邊講述的很詳細,簡單來說,就是我們需要在請求中添加一些關於數據字段長度的信息(以下簡稱length),LengthFieldBasedFrameDecoder的構造方法中,設置一下關於length所佔的字節數和要跳過的字節數等信息(我們只需要讀取數據內容的話,可以跳過length的信息) 這樣的話,我們從客戶端發送的每個數據包,都可以正確地解析

首先,我們增加一個編碼器,爲數據包附件上lenght:

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;


//本類作用,加上四個字節的消息頭(int類型),表示數據包長度
public class LengthEncoder extends MessageToByteEncoder<ByteBuf> {
    @Override
    protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
        int length = msg.readableBytes();
        byte[] bytes = new byte[length];
        msg.readBytes(bytes);
        out.writeInt(length);
        out.writeBytes(bytes);
    }
}

然後,將我們的編碼器和對應的 LengthFieldBasedFrameDecoder添加到服務器端和客戶端的ChannelInitializer裏邊去

修改之後的服務器端代碼:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import java.net.InetSocketAddress;


public class Server {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(4096,0,4,0,4));
                            ch.pipeline().addLast(new LengthEncoder());
                            ch.pipeline().addLast(new ServerBusinessHanler());
                        }
                    });
            serverBootstrap.bind(new InetSocketAddress(8899)).sync().channel().closeFuture().sync();

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}   

new LengthFieldBasedFrameDecoder(4096,0,4,0,4));的意思是:使用LengthFieldBasedFrameDecoder的最大的數據包大小是4096個,其中length佔四個字節,讀取的時候跳過4個字節(也就是最終解碼的時候,忽略掉length字段,只返回真正的數據內容)

修改之後的客戶端代碼:

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;


public class Client {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup eventExecutors = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(eventExecutors)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(4096,0,4,0,4));
                        ch.pipeline().addLast(new LengthEncoder());
                        ch.pipeline().addLast(new ClientBusinessHandler());
                    }
                });
        Channel channel = bootstrap.connect("127.0.0.1", 8899).channel();
        channel.closeFuture().sync();

    }
}  

分別運行服務器端和客戶端之後我們得到的結果:
服務器端輸出:

客戶端輸出:

這樣就解決了粘包和拆包導致的問題

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