在 C/C++ 中,struct 是一种复合数据类型,可以将不同类型的数据(如 int, char, float 等)组合在一起,形成一个逻辑上的整体,它非常适合于在网络编程中定义协议消息,因为它可以将多个字段打包,方便进行序列化和反序列化。

Java 语言本身没有 struct 关键字,但它有功能更强大的等价物:类 和 记录,下面我将分步讲解如何在 Java Socket 编程中使用它们来模拟和超越 C/C++ 的结构体功能。
为什么需要“结构体”在 Socket 中?
当两个程序通过网络通信时,它们交换的是字节流,为了让接收方能正确理解这些字节,发送方和接收方必须约定一个“协议”,这个协议规定了数据的格式,也就是我们常说的“消息结构”。
一个简单的登录请求消息可能包含:
- 用户名 (字符串)
- 密码 (字符串)
- 登录类型 (整数)
在 C 中,你会这样定义:

struct LoginRequest {
char username[32];
char password[32];
int loginType;
};
然后你可以直接将这个 struct 的内存地址发送出去(在特定架构下),或者手动将其转换为字节流。
在 Java 中,我们需要用类来达到同样的目的。
方法一:使用普通类
这是最传统、最灵活的方式,你可以创建一个类来封装所有相关的数据字段,并提供方法来将这些对象转换为字节数组(序列化)以及从字节数组重建对象(反序列化)。
步骤 1: 定义消息类
假设我们要定义一个简单的 Student 结构体,包含 id (int), name (String), 和 score (float)。
import java.nio.charset.StandardCharsets;
// 1. 定义一个类来模拟结构体
public class Student {
private int id;
private String name;
private float score;
// 构造函数
public Student(int id, String name, float score) {
this.id = id;
this.name = name;
this.score = score;
}
// Getters 和 Setters
public int getId() { return id; }
public String getName() { return name; }
public float getScore() { return score; }
// 2. 序列化:将对象转换为字节数组
public byte[] toByteArray() {
// 注意:这是一个简化的手动序列化方法,实际项目中更推荐使用序列化协议
// 如 Protocol Buffers, JSON, 或者 Java 自带的序列化(不推荐用于跨语言)。
// 1. 将 name 转换为字节数组
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
// 2. 创建一个足够大的字节数组
// id(4) + nameLength(4) + nameBytes + score(4)
byte[] result = new byte[4 + 4 + nameBytes.length + 4];
// 3. 使用 ByteBuffer 来方便地写入基本数据类型
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap(result);
buffer.putInt(this.id); // 写入 id
buffer.putInt(nameBytes.length); // 写入 name 的长度
buffer.put(nameBytes); // 写入 name 的内容
buffer.putFloat(this.score); // 写入 score
return result;
}
// 3. 反序列化:从字节数组重建对象
public static Student fromByteArray(byte[] data) {
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap(data);
int id = buffer.getInt();
int nameLength = buffer.getInt();
byte[] nameBytes = new byte[nameLength];
buffer.get(nameBytes);
String name = new String(nameBytes, StandardCharsets.UTF_8);
float score = buffer.getFloat();
return new Student(id, name, score);
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", score=" + score +
'}';
}
}
步骤 2: 在 Socket 编程中使用
现在我们可以在客户端和服务器之间发送和接收 Student 对象了。
服务器端代码:
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) throws IOException {
// 1. 创建服务器套接字,监听 8888 端口
try (ServerSocket serverSocket = new ServerSocket(8888)) {
System.out.println("服务器已启动,等待客户端连接...");
// 2. 阻塞,等待客户端连接
try (Socket clientSocket = serverSocket.accept();
InputStream in = clientSocket.getInputStream()) {
System.out.println("客户端已连接: " + clientSocket.getInetAddress());
// 3. 读取数据
// 先读取一个长度头(如果协议需要),这里我们假设客户端会一次性发送所有数据
// 注意:这个简单的例子没有处理粘包问题,实际应用中需要更复杂的协议
byte[] receivedData = in.readAllBytes(); // 读取所有可用字节
// 4. 反序列化
Student receivedStudent = Student.fromByteArray(receivedData);
// 5. 打印接收到的数据
System.out.println("收到学生信息: " + receivedStudent);
}
}
}
}
客户端代码:
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
// 1. 创建客户端套接字,连接到服务器的 8888 端口
try (Socket socket = new Socket("localhost", 8888);
OutputStream out = socket.getOutputStream()) {
System.out.println("已连接到服务器");
// 2. 创建一个 Student 对象
Student studentToSend = new Student(101, "张三", 95.5f);
// 3. 序列化对象为字节数组
byte[] dataToSend = studentToSend.toByteArray();
// 4. 发送字节数组
out.write(dataToSend);
System.out.println("已发送学生信息: " + studentToSend);
}
}
}
方法二:使用 Java 14+ 的 record (推荐)
从 Java 14 开始,引入了 record(记录)类型。record 是一种不可变的数据载体,专门用来创建简单的数据类,它极大地简化了 struct 的实现,自动生成了构造函数、getter 方法、equals(), hashCode() 和 toString() 方法。
步骤 1: 定义 record
使用 record 重写 Student 结构体。
import java.nio.charset.StandardCharsets;
// 使用 record 定义一个不可变的数据结构
public record StudentRecord(int id, String name, float score) {
// 序列化方法逻辑与之前类似
public byte[] toByteArray() {
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
byte[] result = new byte[4 + 4 + nameBytes.length + 4];
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap(result);
buffer.putInt(this.id);
buffer.putInt(nameBytes.length);
buffer.put(nameBytes);
buffer.putFloat(this.score);
return result;
}
// 反序列化方法,由于 record 的构造函数是自动生成的,我们直接用它
public static StudentRecord fromByteArray(byte[] data) {
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap(data);
int id = buffer.getInt();
int nameLength = buffer.getInt();
byte[] nameBytes = new byte[nameLength];
buffer.get(nameBytes);
String name = new String(nameBytes, StandardCharsets.UTF_8);
float score = buffer.getFloat();
return new StudentRecord(id, name, score);
}
}
record 的代码更简洁,意图更明确,非常适合定义网络协议消息。
更高级的方案:使用序列化框架
手动序列化(如上面的例子)虽然直观,但有几个缺点:
- 脆弱:如果修改了类的结构(比如增加一个字段),所有相关的序列化/反序列化代码都要改,否则会出错。
- 效率低:需要手动处理字节顺序、数据对齐等问题。
- 不跨语言:Java 的
ByteBuffer写入的字节顺序是平台相关的(大端/小端),除非你手动指定,否则在不同架构的机器上可能会出问题。
在实际企业级开发中,通常会使用成熟的序列化框架来定义和传输结构化数据,这些框架会自动处理上述所有问题。
方案 A: JSON
JSON 是最通用的数据交换格式,人类可读,几乎所有语言都支持。
添加依赖 (Maven)
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
修改 Student 类
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
// 使用注解可以控制 JSON 的生成格式
@JsonInclude(JsonInclude.Include.NON_NULL)
public class StudentJson {
private int id;
private String name;
private float score;
// Jackson 要求有无参构造函数
public StudentJson() {}
public StudentJson(int id, String name, float score) {
this.id = id;
this.name = name;
this.score = score;
}
// Getters and Setters...
// 序列化为 JSON 字符串,再转为字节数组
public byte[] toByteArray() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(this);
return jsonString.getBytes(StandardCharsets.UTF_8);
}
// 从 JSON 字节数组反序列化
public static StudentJson fromByteArray(byte[] data) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
String jsonString = new String(data, StandardCharsets.UTF_8);
return mapper.readValue(jsonString, StudentJson.class);
}
@Override
public String toString() {
return "StudentJson{" + "id=" + id + ", name='" + name + '\'' + ", score=" + score + '}';
}
}
优点:跨语言、易于调试、社区支持强大。 缺点:JSON 文本格式比二进制格式占用更多空间,解析速度相对较慢。
方案 B: Protocol Buffers (Protobuf)
这是 Google 开发的高效、跨语言的二进制序列化库,性能极高,体积小。
定义 .proto 文件
student.proto
syntax = "proto3";
// 指定 Java 包名
option java_package = "com.example.proto";
option java_outer_classname = "StudentProto";
// 定义消息结构体
message Student {
int32 id = 1;
string name = 2;
float score = 3;
}
使用 Protobuf 编译器生成 Java 代码
protoc --java_out=. student.proto
这会生成 com/example/proto/StudentProto.java 文件,其中包含一个 Student 类,该类已经内置了高效的 toByteArray() 和 parseFrom(byte[]) 方法。
在 Socket 中使用
// 发送方
StudentProto.Student student = StudentProto.Student.newBuilder()
.setId(101)
.setName("李四")
.setScore(88.0f)
.build();
byte[] data = student.toByteArray();
out.write(data);
// 接收方
byte[] receivedData = in.readAllBytes();
StudentProto.Student receivedStudent = StudentProto.Student.parseFrom(receivedData);
System.out.println("收到: " + receivedStudent);
优点:性能极高、序列化后体积小、强类型、跨语言。 缺点:需要额外的编译步骤,二进制格式不易于人工调试。
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 普通类 | 灵活,面向对象,易于理解 | 代码冗长,需要手动实现序列化,易出错 | 学习、简单项目、快速原型 |
record (Java 14+) |
代码极简,不可变,线程安全,自动生成方法 | 不可变(在某些场景下是限制) | Java 新项目,定义简单数据载体,强烈推荐 |
| JSON | 跨语言,可读性好,调试方便,生态成熟 | 占用空间大,解析速度相对慢 | Web API,配置文件,需要与多种语言交互的场景 |
| Protocol Buffers | 性能最高,体积小,强类型,跨语言 | 需要编译步骤,二进制格式不易读 | 对性能和体积要求极高的分布式系统、微服务 |
对于现代 Java Socket 编程,我的建议是:
- 如果项目使用 Java 14+,优先使用
record来定义你的消息结构。 - 如果只是简单的内部通信或学习,手动序列化的
class或record就足够了。 - 如果是生产环境,特别是需要跨语言或对性能/体积有要求,强烈推荐使用 Protocol Buffers 或其他成熟的二进制序列化框架(如 FlatBuffers, Avro)。
