TCP粘包,拆包及解决方法

来源:互联网 发布:小布老师linux 编辑:程序博客网 时间:2024/06/07 13:35

1.首先知道一点:UDP是不会发生粘包或拆包现象的,UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。

TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;在TCP的首部也没有表示数据长度的字段,基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。

2.粘包,拆包表现形式

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

⑴正常接收两个包,没发生粘包或者拆包

normal

⑵只接收了一个包,由于TCP不会丢包,这个包含了两个包的信息,即发生了粘包,由于接收端不知道两个包界限所以不好解析。

one

⑶两个包不完整或多出一块,即发生了粘包和拆包



half_one

one_half

3.粘包拆包发生的常见原因

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

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

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

⑷接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
等等。
4.粘包,拆包解决办法

关键在于添加边界信息,常用办法如下:

⑴发送端给每个数据包添加包含数据包长度的包首部。效率高,实现复杂。

⑵将数据包封装为固定长度(不够的用0填充),接收端每次从缓冲区读取固定长度。效率低,实现简单。

⑶数据包之间添加特殊字符。

5.样例程序

NIO编程样例:
https://blog.insanecoder.top/javazhong-bio-niohe-aioshi-yong-yang-li/

⑴固定数据包长度

发送端:

// 发送端String msg = "hello world " + number++;  socketChannel.write(ByteBuffer.wrap(new FixLengthWrapper(msg).getBytes()));// 封装固定长度的工具类public class FixLengthWrapper {    public static final int MAX_LENGTH = 32;    private byte[] data;    public FixLengthWrapper(String msg) {        ByteBuffer byteBuffer = ByteBuffer.allocate(MAX_LENGTH);        byteBuffer.put(msg.getBytes());        byte[] fillData = new byte[MAX_LENGTH - msg.length()];        byteBuffer.put(fillData);        data = byteBuffer.array();    }    public FixLengthWrapper(byte[] msg) {        ByteBuffer byteBuffer = ByteBuffer.allocate(MAX_LENGTH);        byteBuffer.put(msg);        byte[] fillData = new byte[MAX_LENGTH - msg.length];        byteBuffer.put(fillData);        data = byteBuffer.array();    }    public byte[] getBytes() {        return data;    }    public String toString() {        StringBuilder sb = new StringBuilder();        for (byte b : getBytes()) {            sb.append(String.format("0x%02X ", b));        }        return sb.toString();    }}

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

接收端:

private static void processByFixLength(SocketChannel socketChannel) throws IOException {      while (socketChannel.read(byteBuffer) > 0) {        byteBuffer.flip();        while (byteBuffer.remaining() >= FixLengthWrapper.MAX_LENGTH) {            byte[] data = new byte[FixLengthWrapper.MAX_LENGTH];            byteBuffer.get(data, 0, FixLengthWrapper.MAX_LENGTH);            System.out.println(new String(data) + " <---> " + number++);        }        byteBuffer.compact();    }}
⑵添加首部

发送端:

// 发送端String msg = "hello world " + number++;  // add the head represent the data lengthsocketChannel.write(ByteBuffer.wrap(new PacketWrapper(msg).getBytes()));// 添加长度首部的工具类public class PacketWrapper {    private int length;    private byte[] payload;    public PacketWrapper(String payload) {        this.payload = payload.getBytes();        this.length = this.payload.length;    }    public PacketWrapper(byte[] payload) {        this.payload = payload;        this.length = this.payload.length;    }    public byte[] getBytes() {        ByteBuffer byteBuffer = ByteBuffer.allocate(this.length + 4);        byteBuffer.putInt(this.length);        byteBuffer.put(payload);        return byteBuffer.array();    }    public String toString() {        StringBuilder sb = new StringBuilder();        for (byte b : getBytes()) {            sb.append(String.format("0x%02X ", b));        }        return sb.toString();    }}

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

private static void processByHead(SocketChannel socketChannel) throws IOException {    while (socketChannel.read(byteBuffer) > 0) {        // 保存bytebuffer状态        int position = byteBuffer.position();        int limit = byteBuffer.limit();        byteBuffer.flip();        // 判断数据长度是否够首部长度        if (byteBuffer.remaining() < 4) {            byteBuffer.position(position);            byteBuffer.limit(limit);            continue;        }        // 判断bytebuffer中剩余数据是否足够一个包        int length = byteBuffer.getInt();        if (byteBuffer.remaining() < length) {            byteBuffer.position(position);            byteBuffer.limit(limit);            continue;        }        // 拿到实际数据包        byte[] data = new byte[length];        byteBuffer.get(data, 0, length);        System.out.println(new String(data) + " <---> " + number++);        byteBuffer.compact();    }}
测试的时候采用的是一台机器连续发送数据来模拟高并发的场景,所以在测试的时候会发现服务器端收到的数据包的个数经常会小于包的序号,好像发生了丢包。但经过仔细分析可以发现,这种情况是因为TCP发送缓存溢出导致的丢包,也就是这个数据包根本没有发出来。也就是说,发送端发送数据过快,导致接收端缓存很快被填满,这个时候接收端会把通知窗口设置为0从而控制发送端的流量,这样新到的数据只能暂存在发送端的发送缓存中,当发送缓存溢出后,就出现了我上面提到的丢包,这个问题可以通过增大发送端缓存来缓解这个问题。
socketChannel.socket().setSendBufferSize(102400);  





原创粉丝点击