Netty5入門學習筆記002-TCP粘包/拆包問題的解決之道(上)

TCP網絡通信時候會發生粘包/拆包的問題,接下來探討其解決之道。

什麼是粘包/拆包

一般所謂的TCP粘包是在一次接收數據不能完全地體現一個完整的消息數據。TCP通訊爲何存在粘包呢?主要原因是TCP是以流的方式來處理數據,再加上網絡上MTU(最大傳輸單元)的往往小於在應用處理的消息數據,所以就會引發一次接收的數據無法滿足消息的需要,導致粘包的存在。處理粘包的唯一方法就是制定應用層的數據通訊協議,通過協議來規範現有接收的數據是否滿足消息數據的需要。

情況分析

TCP粘包通常在流傳輸中出現,UDP則不會出現粘包,因爲UDP有消息邊界,發送數據段需要等待緩衝區滿了纔將數據發送出去,當滿的時候有可能不是一條消息而是幾條消息合併在換中去內,在成粘包;另外接收數據端沒能及時接收緩衝區的包,造成了緩衝區多包合併接收,也是粘包。

解決辦法

1、消息定長,報文大小固定長度,不夠空格補全,發送和接收方遵循相同的約定,這樣即使粘包了通過接收方編程實現獲取定長報文也能區分。

2、包尾添加特殊分隔符,例如每條報文結束都添加回車換行符(例如FTP協議)或者指定特殊字符作爲報文分隔符,接收方通過特殊分隔符切分報文區分。

3、將消息分爲消息頭和消息體,消息頭中包含表示信息的總長度(或者消息體長度)的字段

4、更復雜的自定義應用層協議

代碼例子

1、Netty中提供了FixedLengthFrameDecoder定長解碼器可以幫助我們輕鬆實現第一種解決方案,定長解碼報文。

服務器端:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package im;
 
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
 * 定長解碼  服務器端
 * @author xwalker
 */
public class Server {
     
    public void bind(int port) throws Exception{
        //接收客戶端連接用
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        //處理網絡讀寫事件
        EventLoopGroup workerGroup=new NioEventLoopGroup();
        try{
        //配置服務器啓動類 
        ServerBootstrap b=new ServerBootstrap();
        b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100)
        .handler(new LoggingHandler(LogLevel.INFO))//配置日誌輸出
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(new FixedLengthFrameDecoder(30));//設置定長解碼器 長度設置爲30
                ch.pipeline().addLast(new StringDecoder());//設置字符串解碼器 自動將報文轉爲字符串
                ch.pipeline().addLast(new Serverhandler());//處理網絡IO 處理器
            }
        });
        //綁定端口 等待綁定成功
        ChannelFuture f=b.bind(port).sync();
        //等待服務器退出
        f.channel().closeFuture().sync();
        }finally{
            //釋放線程資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    public static void main(String[] args) throws Exception {
        int port=8000;
        new Server().bind(port);
    }
 
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package im;
 
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
 * 服務器handler
 * @author xwalker
 */
public class Serverhandler extends ChannelHandlerAdapter {
    int counter=0;
    private static final String MESSAGE="It greatly simplifies and streamlines network programming such as TCP and UDP socket server.";
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        System.out.println("接收客戶端msg:["+msg+"]");
        ByteBuf echo=Unpooled.copiedBuffer(MESSAGE.getBytes());
        ctx.writeAndFlush(echo);
    }
     
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
 
}

客戶端:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package im;
 
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
/**
 * 客戶端 
 * @author xwalker
 *
 */
public class Client {
    /**
     * 鏈接服務器
     * @param port
     * @param host
     * @throws Exception
     */
    public void connect(int port,String host)throws Exception{
        //網絡事件處理線程組
        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
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(new FixedLengthFrameDecoder(30));//設置定長解碼器
                ch.pipeline().addLast(new StringDecoder());//設置字符串解碼器
                ch.pipeline().addLast(new ClientHandler());//設置客戶端網絡IO處理器
            }
        });
        //連接服務器 同步等待成功
        ChannelFuture f=b.connect(host,port).sync();
        //同步等待客戶端通道關閉
        f.channel().closeFuture().sync();
        }finally{
            //釋放線程組資源
            group.shutdownGracefully();
        }
    }
    public static void main(String[] args) throws Exception {
        int port=8000;
        new Client().connect(port, "127.0.0.1");
 
    }
 
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package im;
 
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
 * 客戶端處理器
 * @author xwalker
 *
 */
public class ClientHandler extends ChannelHandlerAdapter {
    private static final String MESSAGE="Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients.";
    public ClientHandler(){}
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
            ctx.writeAndFlush(Unpooled.copiedBuffer(MESSAGE.getBytes()));
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        System.out.println("接收服務器響應msg:["+msg+"]");
    }
     
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

服務器和客戶端分別設置了定長解碼器 長度爲30字節,也就是規定發送和接收一次報文定長爲30字節。

運行結果:

客戶端接收到服務器的響應報文 一段文字被定長分成若干段接收。

服務器端接收客戶端發送的報文 一段話也是分成了等長的若干段。

上述是一個簡單長字符串傳輸例子,將一個長字符串分割成若干段。我們也可以自定義一系列定長的指令發送出去

例如指令長度都是30個字節,批量發出N條指令,這樣客戶端粘包後發出一個比較大的數據指令集,服務器接收到的數據在緩衝區內,只需要按照定長一個個指令取出來執行即可。

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