本文来说下什么是粘包拆包
概述
有过tcp编程的伙伴可能都知道,无论是服务端还是客户端,发送或读取消息的时候,都需要考虑粘包/拆包问题。本篇文章笔者就介绍下,tcp粘包拆包的一些基础知识,以及解决方案和示例。
什么是粘包/拆包
TCP是个“流”式的协议,所谓流,就像河里的水,中间没有边界。TCP传输的数据,在网络上就是一连串的数据,没有分界线。TCP协议的底层,并不了解上层业务的具体定义,它会根据TCP缓冲区的实际情况进行包的划分。在业务层面认为一个完整的包,可能会被TCP拆分成多个小包进行发送,也可能把多个小的包封装成一个大的数据包进行发送,这就是所谓的TCP粘包拆包问题。
粘包/拆包可能发生的情况
TCP粘包/拆包,可能发生4种情况,如图1所示:
客户端发送了两个数据包P1和P2给服务端,服务端一次读取到的字节数是不确定的,可能存在以下4种情况:
(1)服务端分两次读取到了两个独立的数据包P1和P2,没有发送粘包和拆包;
(2)服务端一次读到了两个数据包,P1和P2粘在一起,这就是TCP粘包情况;
(3)服务端分两次读取到了两个数据包,第一次读取了完整的P1包和P2包的一部分,第二次读取到了P2包的剩余部分,这被称为TCP拆包;
(4)服务端分两次读取了两个数据包,第一次读取了P1包的一部分,第二次读取到了P1包的剩余部分,这也是TCP拆包;
TCP粘包/拆包发生的原因
TCP数据流最终发到目的地,需要通过以太网协议封装成一个个的以太网帧发送出去,以太网数据帧大小最小64字节,最大1518字节,除去header部分,其数据payload为46到1500字节。所以如果以太网帧的payload大于MTU(默认1500字节)就需要进行拆包。
粘包拆包问题解决方法
由于TCP协议底层无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,所以,这个问题只能通过上层的应用层协议设计来解决,常见方案如下:
(1)消息定长,发送方和接收方规定固定大小的消息长度,例如每个报文大小固定为200字节,如果不够,空位补空格;
(2)在包围增加特殊字符进行分割,例如FTP协议;
(3)自定义协议,将消息分为消息头和消息体,消息头中包含消息总长度,这样服务端就可以知道每个数据包的具体长度了,知道了发送数据包的具体边界后,就可以解决粘包和拆包问题了;
为什么UDP不会发生粘包/拆包问题
UDP是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法(TCP使用了Nagle算法)。UDP支持的是一对多的模式,所以接收端的缓冲区采用了链式结构来缓存每一个到达的数据包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。即面向消息的通信是有消息边界的。所以udp根本不会粘包,但是会丢数据,不可靠。
笔者使用jdk的nio,写了个简单的发送文件的客户端,每次发送文件协议格式:数据包总长度|文件名长度|文件名|文件内容长度|文件内容,时间有限,服务端程序还未写完,稍后会继续完成的。
public static void sendFile(String hostname, int nioPort, byte[] file, String filename, long fileSize) {
SocketChannel channel = null;
Selector selector = null;
try {
channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(hostname, nioPort));
selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
selector.select();
Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {
SelectionKey key = keysIterator.next();
keysIterator.remove();
if (key.isConnectable()) {
channel = (SocketChannel) key.channel();
if (channel.isConnectionPending()) {
channel.finishConnect();
byte[] filenameBytes = filename.getBytes();
long totalLen = 4 + filenameBytes.length + 8 + fileSize;
ByteBuffer buffer = ByteBuffer.allocate((int) fileSize * 2 + filenameBytes.length);
buffer.putLong(totalLen);
buffer.putInt(filenameBytes.length);
buffer.put(filenameBytes);
buffer.putLong(fileSize);
buffer.put(file);
buffer.flip();
int sentData = channel.write(buffer);
System.out.println("已经发送了" + sentData + "字节的数据到" + hostname);
channel.register(selector, SelectionKey.OP_READ);
}
}
else if (key.isReadable()) {
channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
if (len > 0) {
System.out.println("收到服务端的响应:" + new String(buffer.array(), 0, len));
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
netty是如何解决粘包拆包问题的
基于jdk原生的socket或者nio编程,解决粘包拆包问题毕竟麻烦,作为一款非常强大的网络通信框架,netty提供了多种编码器用于解决粘包拆包问题,只要掌握这些类库的使用,你就不用关心如何解决粘包拆包问题了。
常见编码器:
- LineBasedFrameDecoder,基于行的解码器,遇到 “\n”、"\r\n"会被作为行分隔符;
- FixedLengthFrameDecoder,基于固定长度的解码器;
- DelimiterBasedFrameDecoder,基于分隔符的振解码器;
本文小结
本文详细介绍了什么是粘包拆包以及粘包拆包产生的原因。