精华内容
下载资源
问答
  • TCP拆包和黏包的过程解决
    2019-01-25 17:27:01

    TCP拆包和黏包的过程和解决

    粘包、拆包解决办法

    通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:

     

    1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

     

    2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

     

    3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

    更多相关内容
  • 在进行Java NIO学习时,发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题。 我们都知道TCP属于传输层的协议,...

    在进行Java NIO学习时,发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题。

    我们都知道TCP属于传输层的协议,传输层除了有TCP协议外还有UDP协议。那么UDP是否会发生粘包或拆包的现象呢?答案是不会。UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。而TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段,基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。

    粘包、拆包表现形式

    现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:

    第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。normal

    第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。one

    第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。half_oneone_half

    粘包、拆包发生原因

    发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充,

    1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。

    2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。

    3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。

    4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

    等等。

    粘包、拆包解决办法

    通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:

    1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

    2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

    3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

    等等。

    样例程序

    我将在程序中使用两种方法来解决粘包和拆包问题,固定数据包长度和添加长度首部,这两种方法各有优劣。固定数据包长度传输效率一般,尤其是在要发送的数据长度长短差别很大的时候效率会比较低,但是编程实现比较简单;添加长度首部虽然可以获得较高的传输效率,冗余信息少且固定,但是编程实现较为复杂。下面给出的样例程序是基于之前的文章《Java中BIO,NIO和AIO使用样例》中提到的NIO实例的,如果对NIO的使用还不是很熟悉,可以先了解一下Java中NIO编程。

    固定数据包长度

    这种处理方式的思路很简单,发送端在发送实际数据前先把数据封装为固定长度,然后在发送出去,接收端接收到数据后按照这个固定长度进行拆分即可。发送端程序如下:

     
    1. // 发送端

    2. String msg = "hello world " + number++;

    3. socketChannel.write(ByteBuffer.wrap(new FixLengthWrapper(msg).getBytes()));

    4.  
    5. // 封装固定长度的工具类

    6. public class FixLengthWrapper {

    7.  
    8. public static final int MAX_LENGTH = 32;

    9. private byte[] data;

    10.  
    11. public FixLengthWrapper(String msg) {

    12. ByteBuffer byteBuffer = ByteBuffer.allocate(MAX_LENGTH);

    13. byteBuffer.put(msg.getBytes());

    14. byte[] fillData = new byte[MAX_LENGTH - msg.length()];

    15. byteBuffer.put(fillData);

    16. data = byteBuffer.array();

    17. }

    18.  
    19. public FixLengthWrapper(byte[] msg) {

    20. ByteBuffer byteBuffer = ByteBuffer.allocate(MAX_LENGTH);

    21. byteBuffer.put(msg);

    22. byte[] fillData = new byte[MAX_LENGTH - msg.length];

    23. byteBuffer.put(fillData);

    24. data = byteBuffer.array();

    25. }

    26.  
    27. public byte[] getBytes() {

    28. return data;

    29. }

    30.  
    31. public String toString() {

    32. StringBuilder sb = new StringBuilder();

    33. for (byte b : getBytes()) {

    34. sb.append(String.format("0x%02X ", b));

    35. }

    36. return sb.toString();

    37. }

    38. }

    可以看到客户端在发送数据前首先把数据封装为长度为32bytes的数据包,这个长度是根据目前实际数据包长度来规定的,这个长度必须要大于所有可能出现的数据包的长度,这样才不会出现把数据“截断”的情况。接收端程序如下:

     
    1. private static void processByFixLength(SocketChannel socketChannel) throws IOException {

    2. while (socketChannel.read(byteBuffer) > 0) {

    3.  
    4. byteBuffer.flip();

    5. while (byteBuffer.remaining() >= FixLengthWrapper.MAX_LENGTH) {

    6. byte[] data = new byte[FixLengthWrapper.MAX_LENGTH];

    7. byteBuffer.get(data, 0, FixLengthWrapper.MAX_LENGTH);

    8. System.out.println(new String(data) + " <---> " + number++);

    9. }

    10. byteBuffer.compact();

    11. }

    12. }

    可以看出接收端的处理很简单,只需要每次读取固定的长度即可区分出来不同的数据包。

    添加长度首部

    这种方式的处理较上面提到的方式稍微复杂一点。在发送端需要给待发送的数据添加固定的首部,然后再发送出去,然后在接收端需要根据这个首部的长度信息进行数据包的组合或拆分,发送端程序如下:

     
    1. // 发送端

    2. String msg = "hello world " + number++;

    3. // add the head represent the data length

    4. socketChannel.write(ByteBuffer.wrap(new PacketWrapper(msg).getBytes()));

    5.  
    6. // 添加长度首部的工具类

    7. public class PacketWrapper {

    8.  
    9. private int length;

    10. private byte[] payload;

    11.  
    12. public PacketWrapper(String payload) {

    13. this.payload = payload.getBytes();

    14. this.length = this.payload.length;

    15. }

    16.  
    17. public PacketWrapper(byte[] payload) {

    18. this.payload = payload;

    19. this.length = this.payload.length;

    20. }

    21.  
    22. public byte[] getBytes() {

    23. ByteBuffer byteBuffer = ByteBuffer.allocate(this.length + 4);

    24. byteBuffer.putInt(this.length);

    25. byteBuffer.put(payload);

    26. return byteBuffer.array();

    27. }

    28.  
    29. public String toString() {

    30. StringBuilder sb = new StringBuilder();

    31. for (byte b : getBytes()) {

    32. sb.append(String.format("0x%02X ", b));

    33. }

    34. return sb.toString();

    35. }

    36. }

    从程序可以看到,发送端在发送数据前首先给待发送数据添加了代表长度的首部,首部长为4bytes(即int型长度),这样接收端在收到这个数据之后,首先需要读取首部,拿到实际数据长度,然后再继续读取实际长度的数据,即实现了组包和拆包的操作。程序如下:

     
    1. private static void processByHead(SocketChannel socketChannel) throws IOException {

    2.  
    3. while (socketChannel.read(byteBuffer) > 0) {

    4. // 保存bytebuffer状态

    5. int position = byteBuffer.position();

    6. int limit = byteBuffer.limit();

    7. byteBuffer.flip();

    8. // 判断数据长度是否够首部长度

    9. if (byteBuffer.remaining() < 4) {

    10. byteBuffer.position(position);

    11. byteBuffer.limit(limit);

    12. continue;

    13. }

    14. // 判断bytebuffer中剩余数据是否足够一个包

    15. int length = byteBuffer.getInt();

    16. if (byteBuffer.remaining() < length) {

    17. byteBuffer.position(position);

    18. byteBuffer.limit(limit);

    19. continue;

    20. }

    21. // 拿到实际数据包

    22. byte[] data = new byte[length];

    23.  
    24. byteBuffer.get(data, 0, length);

    25. System.out.println(new String(data) + " <---> " + number++);

    26. byteBuffer.compact();

    27. }

    28. }

    关键信息已经在程序中做了注释,可以很明显的感觉到这种方法的处理难度相对于固定长度要大一些,不过这种方式可以获取更大的传输效率。

    这里需要提醒各位同学一个问题,由于我在测试的时候采用的是一台机器连续发送数据来模拟高并发的场景,所以在测试的时候会发现服务器端收到的数据包的个数经常会小于包的序号,好像发生了丢包。但经过仔细分析可以发现,这种情况是因为TCP发送缓存溢出导致的丢包,也就是这个数据包根本没有发出来。也就是说,发送端发送数据过快,导致接收端缓存很快被填满,这个时候接收端会把通知窗口设置为0从而控制发送端的流量,这样新到的数据只能暂存在发送端的发送缓存中,当发送缓存溢出后,就出现了我上面提到的丢包,这个问题可以通过增大发送端缓存来缓解这个问题,

    socketChannel.socket().setSendBufferSize(102400);  
    

    当然这个话题不在本文的讨论范围,如果有兴趣的同学可以参阅《TCP/IP详解卷一》中的拥塞窗口一章。

    关于源码说明,源码默认是把粘包和拆包处理这一部分注释掉了,分别位于NIOTcpServer和NIOTcpClient文件中,需要测试粘包和拆包处理程序的同学需要把这一段注释给去掉。

    最后给出源码下载地址

    参考

    Netty精粹之TCP粘包拆包问题

    展开全文
  • TCP粘包和拆包

    千次阅读 2019-07-15 11:04:27
    它会根据TCP缓冲区的实际情况进行的划分,所以在业务上认为,一个完整的可能会被TCP拆分成多个进行发送,也有可能把多个小的封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。 假设客户端分别...

    TCP粘包和拆包

           TCP是个“流”协议,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

    假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。

    (1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;

    (2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;

    (3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;

    (4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。

    如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

    1.1 粘包和拆包原因

    (1)要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;

    (2)接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;

    (3)要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;

    (4)待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。

    1.2 粘包和拆包解决策略

           由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:

    1. 消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
    2. 设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如FTP协议。
    3. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。
    4. 更复杂的应用层协议。
    展开全文
  • netty中TCP黏包/拆包解决之道

    千次阅读 2017-01-31 12:03:32
    1.TCP黏包/拆包的原理TCP 是一个“流”协议,所谓流就是没有界限的一串数据。TCP并不了解上层业务数据的具体定义,它只会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多...

    这篇博客的主要内容是:
    1. TCP黏包/拆包的基础知识
    2.没考虑TCP黏包/拆包 导致的异常案例
    3.netty中解决TCP黏包/拆包的方法

    1.TCP黏包/拆包的原理

    TCP 是一个“流”协议,所谓流就是没有界限的一串数据。TCP并不了解上层业务数据的具体定义,它只会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送(拆包);也有可能把多个小的包封装成一个大的数据包一起发送(黏包)。

    1)黏包/拆包问题说明

    通过一个图示来说明TCP的黏包/拆包的问题:
    这里写图片描述

    如上图所示:客户端分别发送两个数据包D1、D2给服务端,由于服务端一次读取到的字节数是不一定的,故可能存在以下4种情况:
    (1)服务端分两次读取到了两个独立的数据包,分别是D1、D2,没有黏包和拆包。这也是最理想的情况。

    (2)服务端一次读取到了两个数据包,D1、D2粘合在一起,被称为TCP黏包。

    (3)服务端分两次读取到了数据包:第一次读取到了完整的D1包和部分D2包D2_1;第二次读取到了剩余的D2包D2_2, 这称为TCP的拆包。

    (4)服务端分两次读取到了数据包:第一次读取到了部分的D1包D1_1;第二次读取到了剩余的D1包D1_2和完整的D2包, 这也称为TCP的拆包。

    (5)如果此时服务端TCP接收窗口比较小,而数据包D1和D2又比较大,可能会分多次服务端才能完全将D1和D2完全接收。

    2) 黏包拆包问题的常用解决思路:

    前面就说了底层的TCP协议(滑动窗口协议会依据网络情况决定每次发送数据包的大小)是无法理解上层应用封装协议大小也就无法避免会发生拆包和黏包,所以只能通过上层的应用协议栈来解决。常用解决思路有如下几个方面:
    (1)消息定长,例如每个报文的大小为固定长度200个字节,如果不够,空位补空格。
    (2)在包尾增加回车换行符进行分割,
    (3)将消息分为消息头和消息体,消息头中包含表示消息总长度的字段(或则是消息体总长度)。常用设计思路是消息头第一个字段使用int32表示消息总长度。
    (4)使用更加复杂的应用协议栈。

    2.未考虑TCP黏包/拆包 导致的异常案例

    本示例基于之前的一片博文:基于netty的时间服务器上:
    http://blog.csdn.net/u010853261/article/details/54799089

    在那个时间服务器上并没有考虑到读取半包的问题,这在功能测试时没有问题,但是一旦压力上来,发送的数据包比较大时候,就可能存在黏包和拆包问题,下面还是通过实例来说明:

    TimeServer 的改造

    这里我们对服务端的handler事件进行改造:

    package netty.quanwei.p4;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.Unpooled;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import utils.DateUtil;
    import utils.LogUtil;
    
    import java.util.Date;
    
    /**
     * Created by louyuting on 17/1/31.
     */
    public class TimeServerHandler  extends ChannelInboundHandlerAdapter{
    
        private int counter;
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
            LogUtil.log_debug("Server -> read");
    
            ByteBuf buf = (ByteBuf)msg;
            byte[] req = new byte[buf.readableBytes()];
            buf.readBytes(req);
    
            String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
    
            LogUtil.log_debug("timeServer received order: " + body + "the counter is:" + (++counter)) ;
    
            String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? DateUtil.fmtDateToMillisecond(new Date()) : "BAD ORDER";
            currentTime = currentTime + System.getProperty("line.separator");
    
            //response
            ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
            //异步发送应答消息给客户端: 这里并没有把消息直接写入SocketChannel,而是放入发送缓冲数组中
            ctx.writeAndFlush(resp);
        }
    
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            LogUtil.log_debug("Server -> read complete");
    
            //将发送缓冲区中数据全部写入SocketChannel
            //ctx.flush();
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            //释放资源
            ctx.close();
        }
    }

    主要做了如下改变:收到消息之后就计数一次, 然后发送应答消息给客户端。按照这种设计, 逻辑上应该是服务端收到的消息总数和客户端发送的消息总数相同; 而且请求消息删除回车换行符之后应该是 “QUERY TIME ORDER”

    TImeClient改造

    package netty.quanwei.p4;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.Unpooled;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import utils.LogUtil;
    
    /**
     * Created by louyuting on 17/1/31.
     */
    public class TimeCLientHandler extends ChannelInboundHandlerAdapter{
        private byte[] req;
    
        private int counter;
    
        public TimeCLientHandler() {
            this.req = ("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes();
        }
    
    
        /**
         * 链路建立成功时发送100条消息到服务端, 每发送一条就刷新一次数据到SocketChannel
         * @param ctx
         * @throws Exception
         */
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            LogUtil.log_debug("client -> active");
            ByteBuf message=null;
    
            for(int i=0; i<100; i++){
                message = Unpooled.buffer(req.length);
                message.writeBytes(req);
                ctx.writeAndFlush(message);
            }
    
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            LogUtil.log_debug("client -> read");
    
            ByteBuf buf = (ByteBuf)msg;
            byte[] req = new byte[buf.readableBytes()];
            buf.readBytes(req);
    
            String body = new String(req, "UTF-8");
    
            LogUtil.log_debug("NOW is: " + body + " the counter is:" + (++counter));
    
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            ctx.close();
        }
    }

    主要做了如下改变:
    1)链路建立成功时发送100条消息到服务端, 每发送一条就刷新一次数据到SocketChannel,保证每条消息都被及时写入到channel
    2)按逻辑服务端应该接收到100条查询时间的指令.
    3)此外,客户端每收到一次响应就打印一次响应并计数。

    运行结果:

    服务端:

    2017-01-31 13:30:51Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDERthe counter is:1
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDERthe counter is:2
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:3
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:4
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:5
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:6
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:7
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:8
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:9
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:10
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:11
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:12
    2017-01-31 13:30:57Server -> read complete
    2017-01-31 13:30:57Server -> read
    2017-01-31 13:30:57:timeServer received order: QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDER
    QUERY TIME ORDERthe counter is:13
    2017-01-31 13:30:57Server -> read complete

    这说明:counter值13,服务端只接收到了13条消息;本应该是100条消息的,说明这是发生了TCP的黏包。

    客户端:

    2017-01-31 13:30:57:client -> active
    2017-01-31 13:30:57:client -> read
    2017-01-31 13:30:57:NOW is: 2017-01-31 13:30:57-939
    2017-01-31 13:30:57-941
    BAD ORDER
    BAD ORDER
    BAD ORDER
    BAD ORDER
    BAD ORDER
    BAD ORDER
    BAD ORDER
    BAD ORDER
    BAD ORDER
    BAD ORDER
    BAD ORDER
    the counter is:1

    客户端理想下应该是收到100条响应的,但是根据counter值可知也只收到了1条,但是服务端确实收到了13条请求的消息的,所以返回的是一个时间和12个BAD ORDER字符。 这说明发生了黏包。

    这冲充分说明在没考虑黏包和拆包情况下,服务器很可能不能正常工作。

    3.netty中解决TCP黏包/拆包的方法

    下面通过netty自带的解码器LineBasedFrameDecoder和StringDecoder来解决TCP黏包的问题。

    还是基于上面的时间服务器来更正,解决TCP黏包和拆包问题:

    服务端

    更改服务端的ChildChannelInitializer类,增加LineBasedFrameDecoder和StringDecoder

    package netty.quanwei.p4_2;
    
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.handler.codec.LineBasedFrameDecoder;
    import io.netty.handler.codec.string.StringDecoder;
    
    /**
     * Created by louyuting on 17/1/31.
     */
    public class ChildChannelInitializer extends ChannelInitializer<SocketChannel> {
    
    
        @Override
        protected void initChannel(SocketChannel channel) throws Exception {
    
            channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
    
            channel.pipeline().addLast(new StringDecoder());
    
            channel.pipeline().addLast("timeServerHandler", new TimeServerHandler());
        }
    }

    然后更改timeServerHandler, 因为消息已经经过了StringDecoder解码,所以msg可以直接转换成string

    package netty.quanwei.p4_2;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.Unpooled;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import utils.DateUtil;
    import utils.LogUtil;
    
    import java.util.Date;
    
    /**
     * Created by louyuting on 17/1/31.
     *
     * 收到消息之后就计数一次, 然后发送应答消息给客户端.
     * 按照这种设计, 逻辑上应该是服务端收到的消息总数和客户端发送的消息总数相同;
     * 请求消息删除回车换行符之后应该是 "QUERY TIME ORDER"
     *
     */
    public class TimeServerHandler  extends ChannelInboundHandlerAdapter{
    
        private int counter;
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
            LogUtil.log_debug("Server -> read");
    
            String body = (String)msg;
    
            LogUtil.log_debug("timeServer received order: " + body + "the counter is:" + (++counter)) ;
    
            String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? DateUtil.fmtDateToMillisecond(new Date()) : "BAD ORDER";
            currentTime = currentTime + System.getProperty("line.separator");
    
            //response
            ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
            //异步发送应答消息给客户端: 这里并没有把消息直接写入SocketChannel,而是放入发送缓冲数组中
            ctx.writeAndFlush(resp);
        }
    
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            LogUtil.log_debug("Server -> read complete");
    
            //将发送缓冲区中数据全部写入SocketChannel
            //ctx.flush();
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            //释放资源
            ctx.close();
        }
    }

    客户端

    客户端的修改和服务端基本类似:

    package netty.quanwei.p4_2;
    
    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.LineBasedFrameDecoder;
    import io.netty.handler.codec.string.StringDecoder;
    
    /**
     * Created by louyuting on 17/1/31.
     * netty 时间服务器 客户端
     */
    public class TimeClient {
    
        public void connect(int port, String host) throws Exception{
            //配置客户端NIO 线程组
            EventLoopGroup group = new NioEventLoopGroup();
    
            Bootstrap client = new Bootstrap();
    
            try {
                client.group(group)
                        .channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel channel) throws Exception {
                                channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
    
                                channel.pipeline().addLast(new StringDecoder());
    
                                channel.pipeline().addLast(new TimeCLientHandler());
    
                            }
                        });
    
                //绑定端口, 异步连接操作
                ChannelFuture future = client.connect(host, port).sync();
    
                //等待客户端连接端口关闭
                future.channel().closeFuture().sync();
            } finally {
                //优雅关闭 线程组
                group.shutdownGracefully();
            }
        }
    
        /**
         * main 函数
         * @param args
         */
        public static void main(String[] args) {
            TimeClient client = new TimeClient();
            try {
                client.connect(18888, "127.0.0.1");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    handler 的修改:

    package netty.quanwei.p4_2;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.Unpooled;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import utils.LogUtil;
    
    /**
     * Created by louyuting on 17/1/31.
     * 链路建立成功时发送100条消息到服务端, 每发送一条就刷新一次数据到SocketChannel,保证每条消息都被及时写入到channel
     * 按逻辑服务端应该接收到100条查询时间的指令.
     *
     * 此外,客户端每收到一次响应就打印一次响应
     *
     */
    public class TimeCLientHandler extends ChannelInboundHandlerAdapter{
        private byte[] req;
    
        private int counter;
    
        public TimeCLientHandler() {
            this.req = ("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes();
        }
    
    
        /**
         * 链路建立成功时发送100条消息到服务端, 每发送一条就刷新一次数据到SocketChannel
         * @param ctx
         * @throws Exception
         */
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            LogUtil.log_debug("client -> active");
            ByteBuf message=null;
    
            for(int i=0; i<100; i++){
                message = Unpooled.buffer(req.length);
                message.writeBytes(req);
                ctx.writeAndFlush(message);
            }
    
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            LogUtil.log_debug("client -> read");
    
            String body = (String)msg;
    
            LogUtil.log_debug("NOW is: " + body + " the counter is:" + (++counter));
    
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            ctx.close();
        }
    }

    运行结果:

    加入了netty自带的解决TCP黏包拆包的解码器之后,便可以按照我们预期的运行了:
    服务端:

    2017-01-31 14:05:34Server -> read
    2017-01-31 14:05:34timeServer received order: QUERY TIME ORDERthe counter is:1
    2017-01-31 14:05:34Server -> read complete
    2017-01-31 14:05:34Server -> read
    2017-01-31 14:05:34timeServer received order: QUERY TIME ORDERthe counter is:2
    ..........
    ..........
    2017-01-31 14:06:21Server -> read
    2017-01-31 14:06:21timeServer received order: QUERY TIME ORDERthe counter is:99
    2017-01-31 14:06:21Server -> read
    2017-01-31 14:06:21timeServer received order: QUERY TIME ORDERthe counter is:100
    2017-01-31 14:06:21Server -> read complete

    客户端:

    2017-01-31 14:06:21client -> active
    2017-01-31 14:06:21client -> read
    2017-01-31 14:06:21NOW is: 2017-01-31 14:06:21-082 the counter is:1
    2017-01-31 14:06:21client -> read
    2017-01-31 14:06:21NOW is: 2017-01-31 14:06:21-083 the counter is:2
    ........
    ........
    2017-01-31 14:06:21client -> read
    2017-01-31 14:06:21NOW is: 2017-01-31 14:06:21-157 the counter is:98
    2017-01-31 14:06:21client -> read
    2017-01-31 14:06:21NOW is: 2017-01-31 14:06:21-158 the counter is:99
    2017-01-31 14:06:21client -> read
    2017-01-31 14:06:21NOW is: 2017-01-31 14:06:21-159 the counter is:100

    服务端和客户端都是发送并接收了100次请求。

    LineBasedFrameDecoder和StringDecoder原理分析

    LineBasedFrameDecoder的工作原理是:依次遍历ByteBuf中的可读字节,判断看其是否有”\n” 或则 “\r\n”, 如果有就以此位置为结束位置。 从可读索引到结束位置的区间的字节就组成了一行。 它是以换行符为结束标志的解码器,支持携带结束符和不带结束符两种解码方式,同时支持配置单行的最大长度,如果读到了最大长度之后仍然没有发现换行符,则抛出异常,同时忽略掉之前读到的异常码流。

    StringDecoder的功能就非常简单了,就是将之前接收到的对象转换成字符串。

    LineBasedFrameDecoder + StringDecoder 就是一个按行切换的文本解码器。

    这里有一个问题,如果消息不是以换行符结束的怎么办呢?不用担心,netty提供了多种TCP黏包拆包解码器,满足不同需求。

    本文所有源码github地址:
    1)没考虑TCP黏包/拆包 导致的异常案例:
    https://github.com/leetcode-hust/leetcode/tree/master/louyuting/src/netty/quanwei/p4

    2)netty中解决TCP黏包/拆包的方法:
    https://github.com/leetcode-hust/leetcode/tree/master/louyuting/src/netty/quanwei/p4_2

    展开全文
  • 于是通过查阅资料,发现这个就是传说中的TCP粘包问题。下面通过编写代码来重现这个问题: 服务端代码 server/main.go func main() { l, err := net.Listen(tcp, :4044) if err != nil { panic(err) } fmt....
  • 对于新手来说,TCP粘包是个脑阔疼的事儿,不记得啥时候给人写的例子,翻出来发一下。一共两种模式 如图为协议长度模式 将要发的消息,标志符,长度,内容告诉目标,让其拆开~ 一种为标志符处理,各有喜好,自行...
  • TCP粘包,拆包及解决方法

    万次阅读 多人点赞 2018-05-24 00:19:44
    在进行Java NIO学习时,发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题。我们都知道TCP属于传输层的协议,传输...
  • JavaTCP粘包、拆包

    2019-05-27 13:05:00
    import java.nio.ByteBuffer; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled;...import io.netty.channel.ChannelFuture;...import io.n...
  • TCP粘包和拆包问题

    2021-01-31 22:29:47
    1)产生TCP粘包和拆包问题的主要原因是,操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,...
  • 服务端接收消息解析老是出现消息不全的问题 解码编码器 @Component public class SimpleChatChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel...
  • Netty中,解决拆包和黏包中,解决方式有三种 1、在每个包尾部,定义分隔符,通过回车符号,...案例这个案例是通过第一种方式,通过回车符号的方式来解决拆包和黏包,通过在childHandler 中添加 指定的分隔符进行拆包
  • tcp黏包拆包

    2018-08-13 20:03:00
    1.黏包2.封包与拆包 1.黏包 ​ 1.为什么出现黏包 1.发送方原因 tcp默认会使用Nagle算法,而Nagle算法主要做两件事。1)只有上一个分组得到确认,才会发送下一个分组 2)收集多个小分组,在一个确认到来时一起...
  • **@tcp遇到黏包怎么解决? 这段时间复习下网络协议相关的知识, 在网上偶然看到tcp黏包的问题,就好奇地自己就实验了下,总结了下面几点经验,写的不对的还请指正: 一: 什么情况下tcp会发生黏包现象? 发送数据方...
  • 通过socket通讯实现服务器与客户端的连接。首先服务器利用udp广播发送自己的ip地址,客户端在收到广播后通过此ip以tcp连接的方式连接服务器来通讯。
  • TCP 是一个面向字节流的协议,它是性质是流式的,所以它并没有分段。...因此TCP的socket编程,收发两端(客户端服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的,更有效的发到对方,使用了优...
  • netty搭建tcp服务,并以相应的编码解决粘包,拆包问题
  • TCP粘包:指发送方发送的若干数据包在接收方接收时粘成一团,从接收缓冲区看,后一数据的头紧接着前一数据的尾 产生的原因: 1.发送方的原因:TCP默认使用Nagle算法,而Nagle算法主要做两件事情:只有上一个...
  • TCP黏包拆包

    2019-08-18 00:28:28
    之前以为这是多么高大上的问题,后来才发现这个问题实在是是简单得不得了,其实在大三的课程设计中就有...但是其实是有更好的方法处理的,TCP是保证送到、有序的。 我在想如果发生这两张异常,有没有好的办法解决?
  • 计算机网络之TCP粘包、拆包
  • TCP黏包/拆包原理 TCP是一个流的协议。一个完整的包可能被TCP拆分为多个包进行发送;也有可能把多个小的包封装成一个大的数据包一起发送 例如 客户端发送两个包给服务器,可能产生的情况: 1 a b单独发送 2 a b...
  • 1.TCP粘包是指发送方发送的若干数据到接收方接收时粘成一,从接收缓冲区看,后一数据的头紧接着前一数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。 2.发送方引起的...
  • 转自https://blog.csdn.net/u010853261/article/details/547988981.TCP黏包/拆包的原理TCP是一个“流”协议,所谓流就是没有界限的一串数据.TCP并不了解上层业务数据的具体定义,它只会根据TCP缓冲区的实际情况进行...
  • 它会根据TCP缓冲区的实际情况进行的划分,所以在业务上认为,一个完整的可能会被TCP拆分成多个进行发送,也有可能把多个小包封装成一个大的数据包进行发送,这就是TCP的粘包和拆包问题。 TCP 粘包与拆包问题...
  • Qt自身封装的readyRead作为接收网络数据接口,可以关联一个槽函数,每次接收到网络数据就会响应此槽函数,对数据进行拆包在这个槽函数中进行; connect(Socket, &QTcpSocket::readyRead, this, &TcpClient...
  • 原文博客地址:http://blog.csdn.net/zhangxinrun/article/details/6721495TCP粘包分析这两天看csdn有一些关于socket粘包...长连接 Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送接...
  • qt中TCP客户端接收的所有数据都需要拆包与并包吗?如果不是请问我想要同时接收有黏包和不黏包的数据应该怎么做?能具体到用代码写出来吗,谢谢各位大神了
  • TCP协议】(3)---TCP粘包黏包 有关TCP协议之前写过两篇博客: 1、【TCP协议】(1)---TCP协议详解 2、【TCP协议】(2)---TCP三次握手四次挥手 一、TCP粘包、拆包图解 假设客户端分别发送了两个数据包D1...
  • go语言解决TCP黏包

    2021-08-24 16:50:13
    简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。 2.接收端接收不及时造成的接收端粘包:TCP会把...
  • 所以在业务上认为,一个完整的有可能被tcp拆分成多个进行发送,也有可能把多个小包封装成一个大的数据包发送,这就是所谓的tcp拆包、粘包问题。 2、如何处理粘包/半问题? 处理粘包/半的思路就是找出数据...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 761
精华内容 304
关键字:

TCP拆包和黏包