这是一个非常常见的需求,尤其是在与 C/C++ 等语言编写的服务端或客户端进行交互时,因为 C/C++ 的 struct 在网络传输时通常会被序列化成一串连续的字节。
核心思想是:将 Java 中的“结构体”(通常是一个普通类或 record)对象,转换成可以在网络传输的字节数组(序列化),然后在接收端将这个字节数组还原回对象(反序列化)。
Java 中的“结构体”
Java 没有像 C/C++ 那样的原生 struct 关键字,但我们可以使用 普通类(POJO - Plain Old Java Object) 或者 record(Java 14+ 引入的不可变数据类) 来达到完全相同的效果。
我们推荐使用 record,因为它更简洁、更安全(不可变),并且非常适合用来表示数据结构。
示例:定义一个“结构体”
假设我们要表示一个用户信息,包含 ID、用户名和年龄。
// 使用 record (Java 14+) - 推荐
public record UserInfo(int id, String username, int age) {}
// 或者使用传统的 class
// public class UserInfo {
// public int id;
// public String username;
// public int age;
//
// // 需要一个无参构造函数,方便某些序列化库
// public UserInfo() {}
//
// public UserInfo(int id, String username, int age) {
// this.id = id;
// this.username = username;
// this.age = age;
// }
// }
序列化与反序列化
这是最关键的一步,我们需要将 UserInfo 对象转换成字节数组,以及将字节数组转换回 UserInfo 对象。
有几种主流的方法:
手动序列化(最底层,最灵活)
这种方法完全由我们自己控制字节流的格式,适用于与特定协议的 C/C++ 程序交互。
步骤:
- 确定字节顺序(Endianness):网络通信通常使用 大端序,即高位字节在前。
- 确定数据长度:对于
String等变长数据,通常需要先发送其字节数组的长度。 - 按顺序写入/读取数据。
序列化工具类:
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class StructSerializer {
// 将 UserInfo 对象序列化为字节数组
public static byte[] serialize(UserInfo userInfo) throws IOException {
// ByteArrayOutputStream 是一个在内存中写入字节数组的流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// DataOutputStream 可以方便地写入基本数据类型
try (DataOutputStream dos = new DataOutputStream(baos)) {
// 1. 写入 ID (int, 4字节)
dos.writeInt(userInfo.id());
// 2. 写入 username (String)
// a. 先写入字符串的字节长度 (int, 4字节)
// b. 再写入字符串的字节内容
byte[] usernameBytes = userInfo.username().getBytes(StandardCharsets.UTF_8);
dos.writeInt(usernameBytes.length);
dos.write(usernameBytes);
// 3. 写入 age (int, 4字节)
dos.writeInt(userInfo.age());
}
return baos.toByteArray();
}
// 从字节数组反序列化为 UserInfo 对象
public static UserInfo deserialize(byte[] data) throws IOException {
// ByteArrayInputStream 是一个从字节数组读取的流
// ByteBuffer 提供了更方便的缓冲区操作
ByteBuffer buffer = ByteBuffer.wrap(data);
// 1. 读取 ID
int id = buffer.getInt();
// 2. 读取 username
int usernameLength = buffer.getInt();
byte[] usernameBytes = new byte[usernameLength];
buffer.get(usernameBytes);
String username = new String(usernameBytes, StandardCharsets.UTF_8);
// 3. 读取 age
int age = buffer.getInt();
return new UserInfo(id, username, age);
}
}
使用 JSON/XML(最通用,跨语言友好)
这是目前最流行的方法,因为它可读性强,并且几乎所有编程语言都有成熟的 JSON 库。
添加依赖(以 Jackson 为例):
如果你使用 Maven,在 pom.xml 中添加:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version> <!-- 使用最新版本 -->
</dependency>
序列化/反序列化代码:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonSerializer {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static byte[] serializeToJson(UserInfo userInfo) throws JsonProcessingException {
// 将对象转换为 JSON 字符串,再转为字节数组
String json = objectMapper.writeValueAsString(userInfo);
return json.getBytes(StandardCharsets.UTF_8);
}
public static UserInfo deserializeFromJson(byte[] data) throws JsonProcessingException {
// 从字节数组创建 JSON 字符串,再转换为对象
String json = new String(data, StandardCharsets.UTF_8);
return objectMapper.readValue(json, UserInfo.class);
}
}
使用 Java 序列化(不推荐用于网络)
Java 内置的序列化机制非常简单,但存在严重的安全风险和版本兼容性问题,不推荐用于跨语言或生产环境的网络通信。
import java.io.*;
public class JavaSerializer {
public static byte[] serializeJava(UserInfo userInfo) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(userInfo);
}
return baos.toByteArray();
}
public static UserInfo deserializeJava(byte[] data) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
return (UserInfo) ois.readObject();
}
}
}
结合 Socket 进行通信
现在我们将序列化和反序列化应用到 Socket 编程中。
服务端
服务端会接收来自客户端的字节数组,然后反序列化成 UserInfo 对象。
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
int port = 8080;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器启动,监听端口 " + port + "...");
// 等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress());
// 获取输入流
try (InputStream inputStream = clientSocket.getInputStream()) {
// 1. 先读取消息长度(假设我们用一个 int 来表示长度)
// 这是为了解决粘包问题,确保我们读取了完整的数据包
byte[] lengthBytes = new byte[4];
inputStream.read(lengthBytes);
int messageLength = ByteBuffer.wrap(lengthBytes).getInt();
// 2. 根据长度读取消息体
byte[] messageBytes = new byte[messageLength];
inputStream.read(messageBytes);
// 3. 反序列化
UserInfo userInfo = StructSerializer.deserialize(messageBytes); // 使用手动序列化
// UserInfo userInfo = JsonSerializer.deserializeFromJson(messageBytes); // 使用JSON序列化
// 4. 处理数据
System.out.println("收到用户信息: " + userInfo);
System.out.println("ID: " + userInfo.id());
System.out.println("用户名: " + userInfo.username());
System.out.println("年龄: " + userInfo.age());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
客户端会创建一个 UserInfo 对象,序列化成字节数组,然后通过 Socket 发送给服务端。
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
public class Client {
public static void main(String[] args) {
String host = "localhost";
int port = 8080;
try (Socket socket = new Socket(host, port);
OutputStream outputStream = socket.getOutputStream()) {
// 1. 创建一个"结构体"对象
UserInfo userInfo = new UserInfo(101, "张三", 30);
// 2. 序列化对象
byte[] messageBytes;
// messageBytes = StructSerializer.serialize(userInfo); // 使用手动序列化
messageBytes = JsonSerializer.serializeToJson(userInfo); // 使用JSON序列化
// 3. 发送数据前,先发送数据的长度(4字节)
// 这是非常重要的步骤,可以让服务端知道一次要读多少字节
byte[] lengthBytes = ByteBuffer.allocate(4).putInt(messageBytes.length).array();
outputStream.write(lengthBytes);
// 4. 发送实际的数据
outputStream.write(messageBytes);
System.out.println("用户信息已发送: " + userInfo);
} catch (IOException e) {
e.printStackTrace();
}
}
}
总结与最佳实践
| 特性 | 手动序列化 | JSON/XML 序列化 | Java 内置序列化 |
|---|---|---|---|
| 性能 | 高,直接操作二进制 | 中等,需要字符串转换 | 低,开销大 |
| 可读性 | 差 | 高 | 差 |
| 跨语言 | 好,只要协议一致 | 极好,是事实标准 | 差,仅限 Java |
| 安全性 | 高,无已知漏洞 | 高,库经过广泛测试 | 低,存在反序列化漏洞 |
| 维护性 | 差,协议变更需改代码 | 高,格式直观 | 中,依赖 serialVersionUID |
| 适用场景 | 性能极致要求的系统、与特定C/C++协议交互 | Web服务、API、大多数现代应用 | Java应用内部通信、RMI |
推荐方案:
对于绝大多数新的网络项目,强烈推荐使用 JSON 序列化,它在性能、可读性、跨语言支持和安全性之间取得了最佳平衡。
只有在以下情况才考虑手动序列化:
- 你需要极致的性能优化。
- 你需要与一个使用特定二进制协议的现有 C/C++ 系统进行交互,并且无法修改该协议。
关键点回顾:
- Java "结构体" ->
record或class。 - 核心 -> 序列化 (Object -> byte[]) 和 反序列化 (byte[] -> Object)。
- Socket 通信 -> 序列化后的
byte[]是通过Socket的InputStream和OutputStream传输的。 - 解决粘包 -> 在发送实际数据前,先发送数据的长度(一个
int),让接收方能准确读取完整的数据包。
