粘包和拆包问题产生的原因
- 应用层原因:
- 应用程序写入数据的字节大小大于套接字发送缓冲区的大小,就会发生拆包。例如,假设发送缓冲区大小为1024字节,应用程序要发送2048字节的数据,这2048字节的数据就会被拆分到两个或多个TCP数据包中发送。
- 应用程序写入数据的频率过快,而接收方处理数据的速度相对较慢,发送缓冲区还未被清空,新的数据又写入,多个数据包就可能被合并在一起发送,从而产生粘包。
- TCP协议本身特性:
- TCP是面向流的协议,它没有明确的报文边界。在发送端,TCP会根据网络状况和发送缓冲区的情况,将多个应用层的数据包合并成一个TCP数据包发送(Nagle算法会优化这种合并,减少网络流量,但也可能导致粘包)。在接收端,TCP将接收到的数据放入接收缓冲区,应用层从接收缓冲区读取数据时,无法确定原始数据包的边界,所以可能会将多个数据包的数据当作一个数据包处理,即粘包;也可能只读取到部分数据包的数据,即拆包。
解决方案及代码示例
- 定长包:
- 思路:每个数据包都固定长度,如果数据包长度不足,通过填充特定字符(如空格)达到定长。接收方每次按固定长度读取数据,就可以避免粘包和拆包问题。
- 代码示例:
// 发送端
import java.io.OutputStream;
import java.net.Socket;
public class Sender {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream os = socket.getOutputStream();
String message = "Hello World";
// 假设定长为20
String fixedLengthMessage = String.format("%-20s", message);
os.write(fixedLengthMessage.getBytes());
os.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 接收端
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Receiver {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
byte[] buffer = new byte[20];
is.read(buffer);
String message = new String(buffer).trim();
System.out.println("Received: " + message);
is.close();
socket.close();
serverSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 包头 + 包体:
- 思路:数据包由包头和包体组成,包头中包含包体的长度等信息。接收方先读取包头,获取包体长度,再根据包体长度读取包体数据。
- 代码示例:
// 发送端
import java.io.OutputStream;
import java.net.Socket;
public class Sender {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream os = socket.getOutputStream();
String message = "Hello World";
byte[] body = message.getBytes();
int bodyLength = body.length;
// 先发送包头,这里包头简单用4字节表示包体长度
os.write(intToByteArray(bodyLength));
// 再发送包体
os.write(body);
os.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static byte[] intToByteArray(int value) {
return new byte[]{
(byte) (value >>> 24),
(byte) (value >>> 16),
(byte) (value >>> 8),
(byte) value
};
}
}
// 接收端
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Receiver {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
// 先读取包头,获取包体长度
byte[] lengthBuffer = new byte[4];
is.read(lengthBuffer);
int bodyLength = byteArrayToInt(lengthBuffer);
byte[] bodyBuffer = new byte[bodyLength];
is.read(bodyBuffer);
String message = new String(bodyBuffer);
System.out.println("Received: " + message);
is.close();
socket.close();
serverSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static int byteArrayToInt(byte[] bytes) {
return ((bytes[0] & 0xFF) << 24) |
((bytes[1] & 0xFF) << 16) |
((bytes[2] & 0xFF) << 8) |
(bytes[3] & 0xFF);
}
}
- 特殊分隔符:
- 思路:在每个数据包的末尾添加特殊的分隔符,接收方通过识别分隔符来分割数据包。
- 代码示例:
// 发送端
import java.io.OutputStream;
import java.net.Socket;
public class Sender {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream os = socket.getOutputStream();
String message = "Hello World";
String messageWithSeparator = message + "|";
os.write(messageWithSeparator.getBytes());
os.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 接收端
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Receiver {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
if (line.endsWith("|")) {
String message = line.substring(0, line.length() - 1);
System.out.println("Received: " + message);
}
}
br.close();
socket.close();
serverSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}