Java 和 C# 之间的 Base64 和二进制流

Base64 and binary streams between Java and C#

我觉得答案很明显,但假设我在 C# 中有以下内容

using (MemoryStream ms = new MemoryStream())
{
    using (BinaryWriter bw = new BinaryWriter(ms))
    {
        // Write some floats, bytes, and uints
        // Convert.ToBase64String this stuff from ms.ToArray
    }
}

和 Java 中的以下内容(好吧,它是 Scala,但使用 Java 库):

val byteStream = new ByteArrayOutputStream()
val outStream = new DataOutputStream(byteStream)
// Write some floats, bytes, and longs where the uints were using 
// writeFloat, writeByte, and writeLong. .NET has an overloaded 
// function that takes whatever. 
// Base64.getEncoder.encodeToString byteStream.toByteArray

我得到了完全不同的 base 64 字符串。他们在这里做什么不同?我需要 Java 输出来匹配 .NET 输出。我假设它是某种字节顺序问题,但我没有运气使用 ByteBuffer 来纠正这个问题。

Java:

PczMzT3MzM0/gAAAPczMzQAAAAAAAAAAAAAAAD3MzM0/gAAAAQAAAABRn8XzAAAAAAAAAAEAAAAAAAAAAQ==

C#(带有未知的 = 符号,因为我们出于某些原因将它们砍掉):

zczMPc3MzD0AAIA/zczMPQAAAAAAAAAAAAAAAM3MzD0AAIA/AfPFn1EBAAAAAQAAAA

我真的觉得它好像是字节排序,这就是为什么我尝试在 Java 代码中使用 ByteBuffer 排序方法来更改排序但我没有成功。

为进一步说明,Java 代码在 x86_64 CentOS Java 7 上为 运行,而 .NET 在 x86_64 Windows 上Server 2008 .NET 4.These 值来自 Protobuf 对象,因此我认为它们应该是跨平台的。在数字上,无论我至少在编写这三种数据类型时输入什么,数据都是相同且一致的。唯一显着的区别是 Java 中缺少无符号类型,也许存在二进制表示差异,这是我最初试图解决的问题,但我似乎无法弄清楚。

正如我所说。使用另一种格式不是一种选择。我需要从 java 写入的二进制数据,然后进行 base 64 编码以产生与 .NET 相同的结果。序列化选项不是一个选项。这必须是它。我需要一个资源来帮助将它们组合在一起,无论这是否意味着字节数据的二进制操作。我需要对数据类型进行一些解释,因为我进行了大量搜索,但没有找到解释如何执行此操作或真正差异的资源,因此我可以实施我决定在这里提出的解决方案。

不同的平台有不同的二进制表示。如果你想匹配 base64 字符串,你应该使用 json 或 xml 序列化。 Json 或 xml 提供跨平台。

编辑:不要误会我的意思:Base64 是标准的编码算法。它为相同的数据提供相同的输出。我的意思是字节数组可能不同。

如何实现跨平台二进制通​​信:

  • 定义准确的字节格式
  • 在每个平台实施

通常,您可以使用最接近您的需求(例如 https://en.wikipedia.org/wiki/BSON)并在您感兴趣的一个或所有平台上受支持的现成协议来简化这两个步骤。

请注意,给定 language/framework 中的基本二进制序列化类型通常严格针对 language/framework(通常是特定版本),因为它经常提供 speed/size 好处并且没有很好"binary object representation".

上接受的标准

替代方法是使用定义明确的文本格式,如其他答案中建议的 JSON/XML。

二进制格式之间的一些可能的技术差异:

  • 整数类型的序列化可能因字节而异order/possible 替代表示(如 .Net 中的压缩 int)
  • 布尔值和枚举类型的大小可能不同
  • arrays/strings可以用不同的类型来表示长度
  • 可以通过一些二进制表示添加填充
  • 字符串可以是 Utf8、Utf-16 或任何其他 specified/unspecified 带或不带尾随 0 的编码。

主要问题是 C# 的 BinaryWriter 先写入数据类型的低字节,而 Java 的 DataOutputStream 先写入高字节。

此外,当您写入 .NET 无符号整数时,将写入 4 个字节。但是当你写一个Javalong的时候,它写了8个字节。所以这是另一个区别。

但是一旦您理解了差异,修复它们以使其匹配实际上并不难。这里有 2 个代码片段,一个在 C# 中,另一个在 Java 中,它们对相同的信息进行编码并输出相同的 Base64 编码的字符串。就我而言,我选择覆盖 Java 编写 floatlong 的方式。

.NET 代码示例

static void Main(string[] args)
{
    using (MemoryStream ms = new MemoryStream())
    {
        using (BinaryWriter bw = new BinaryWriter(ms))
        {
            // floats
            bw.Write(-456.678f);
            bw.Write(0f);
            bw.Write(float.MaxValue);

            // bytes
            bw.Write((byte)0);
            bw.Write((byte)120);
            bw.Write((byte)255);

            // uints
            bw.Write(0U);
            bw.Write(65000U);
            bw.Write(4294967295U);
        }

        var base64String = Convert.ToBase64String(ms.ToArray());
        Console.WriteLine(base64String);
    }
}

Java 代码示例

public static void main(String[] args) throws Exception {
    try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) {

        try (DataOutputStream outStream = new DataOutputStream(byteStream)) {
            // floats
            writeFloat(-456.678f, outStream);
            writeFloat(0f, outStream);
            writeFloat(Float.MAX_VALUE, outStream);

            // bytes
            outStream.writeByte(0);
            outStream.writeByte(120);
            outStream.writeByte(255);

            // longs (uints)
            writeUint(0L, outStream);
            writeUint(65000L, outStream);
            writeUint(4294967295L, outStream);
        }

        String base64String = Base64.getEncoder().encodeToString(byteStream.toByteArray());
        System.out.println(base64String);
    }
}

private static void writeFloat(float f, DataOutputStream stream) throws Exception {
    int val = Float.floatToIntBits(f);
    stream.writeByte(val & 0xFF);
    stream.writeByte((val >>> 8) & 0xFF);
    stream.writeByte((val >>> 16) & 0xFF);
    stream.writeByte((val >>> 24) & 0xFF);
}

private static void writeUint(long val, DataOutputStream stream) throws Exception {
    stream.writeByte((int) (val & 0xFF));
    stream.writeByte((int) ((val >>> 8) & 0xFF));
    stream.writeByte((int) ((val >>> 16) & 0xFF));
    stream.writeByte((int) ((val >>> 24) & 0xFF));
}

两个样本的输出

yVbkwwAAAAD//39/AHj/AAAAAOj9AAD/////

确保使用 float 类型测试边缘情况,并在必要时进行调整。如果这对您很重要,我希望像 NaN 这样的有趣值会导致差异,但也许您并不关心这一点。否则,我希望它能正常工作。