杰瑞科技汇

Java结构体如何通过Socket传输?

这是一个非常常见的需求,尤其是在与 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++ 程序交互。

步骤:

  1. 确定字节顺序(Endianness):网络通信通常使用 大端序,即高位字节在前。
  2. 确定数据长度:对于 String 等变长数据,通常需要先发送其字节数组的长度。
  3. 按顺序写入/读取数据

序列化工具类:

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 序列化,它在性能、可读性、跨语言支持和安全性之间取得了最佳平衡。

只有在以下情况才考虑手动序列化:

  1. 你需要极致的性能优化。
  2. 你需要与一个使用特定二进制协议的现有 C/C++ 系统进行交互,并且无法修改该协议。

关键点回顾:

  1. Java "结构体" -> recordclass
  2. 核心 -> 序列化 (Object -> byte[]) 和 反序列化 (byte[] -> Object)。
  3. Socket 通信 -> 序列化后的 byte[] 是通过 SocketInputStreamOutputStream 传输的。
  4. 解决粘包 -> 在发送实际数据前,先发送数据的长度(一个 int),让接收方能准确读取完整的数据包。
分享:
扫描分享到社交APP
上一篇
下一篇