2012 Jan 19

Wrangling with Deflate, Base64 and GZip

最近不小心被一些编码相关的东西困扰了。这些从Web诞生便产生的一些编码问题,在2012年的今天仍然可以让一个程序员抓狂。项目的大致需求是这样的,客户端需要将一个字符串进行如下处理:

  1. 将字符串进行Deflate压缩
  2. 将压缩后的结果再进行Base64编码

那么服务器端,需要进行上述流程的反过程。问题是,由于这是个开放接口,客户端可以使用各种不同的平台进行开发,如JS、Java、Ruby、PHP等。好在,大多数语言都是基于zlib包装了自己的相关API,那么也算是实现标准了。窘迫的事情时,NodeJS里面zlib的实现是在0.6以后加入的,我们使用的技术仍然是0.4版本的。

迫于时间有限,切换到0.6或者实现zlib的binding都是不靠谱的,于是我打开了万能的Github。果然功夫不负有心人,找到了Deflate的纯JS实现——RawDeflate。顿时对作者的崇拜之心油然而生。殊不知,悲剧就是从这里开始的。

Base64算法

为了测试有效性,我用JS做了客户端编码工作,然后用Ruby实现了服务器端解码工作。代码如下:

这里只把编码脚本和解码脚本贴出来,其他的已经在上面给出了Gist链接。

编码脚本, v1

var RawDeflate = require("./deflate"),
    Buffer = require("buffer").Buffer,
    fs = require("fs");

function encode(str) {
  return new Buffer(RawDeflate.deflate(str)).toString("base64");
}

console.log(encode("hello, world!"));
fs.writeFileSync("data.txt", encode("hello, world!"));

解码脚本,v1

require 'base64'
require 'zlib'

def decode(str)
  Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(Base64.decode64(str))
end

str = File.open("data.txt", "r") do |data|
  data.readlines().join()
end

p decode(str)

运行的结果是空串哦,亲!也就是说Ruby竟然不能解出来。Ruby是直接在Zlib上进行包装的,那么其他的语言PHP、Python什么的也应该是同样的结果。

好吧,难道我JS的算法有误差?那么和Ruby的算法进行对比以下吧⋯⋯

Ruby的测试脚本,算出的每个步骤都进行md5:

require 'base64'
require 'zlib'
require 'digest/md5'

def deflate(str)
  Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(str, Zlib::FINISH)
end

def base64(str)
  Base64.encode64(str)
end

def md5(str)
  s = Digest::MD5.hexdigest(str)
end

def encode(str)
  base64(deflate(str))
end

def decode(str)
  Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(Base64.decode64(str))
end

str = 'hello, world'

p "Test Base64"
p base64(str)
p md5(base64(str))

p "Test Deflate"
p deflate(str)
p md5(deflate(str))

p "Test Encode Defalte and Base64"
p encode(str)
p md5(encode(str))

p "Test Decode Deflate and Base64"
p decode(encode(str))

运行发现,Ruby自己处理自己压缩出来的东西毫无问题。那么JS呢?

var Base64 = require("./b64"),
    RawDeflate = require("./deflate"),
    Buffer = require("buffer").Buffer,
    crypto = require("crypto");

function decode (str) {
  return RawDeflate.inflate(new Buffer(str, "base64").toString());
}

function md5 (str) {
  return crypto.createHash("md5").update(str).digest("hex");
}

function base64 (str) {
  return new Buffer(str).toString("base64");
}

function deflate (str) {
  return RawDeflate.deflate(str);
}

function encode (str) {
  return base64(deflate(str));
}

var str = "hello, world";
console.log("Testing string: " + str);

console.log("Base64 Test");
console.log(base64(str));
console.log(md5(base64(str)));

console.log("Defalte Test");
console.log(deflate(str));
console.log(md5(deflate(str)));

console.log("Test Encode");
console.log(encode(str));
console.log(md5(encode(str)));

console.log("Test Decode");
console.log(decode(encode(str)));

好吧,JS的算法自己处理自己的结果也是正确的。那么比较以下两边各个步骤的md5,可以发现几个现象:

Screen Shot 2012-01-19 at 下午6.43.35

  • 仅仅Base64后MD5便不一致
  • 仅仅Deflate后MD5一致
  • Deflate后再Base64,结果MD5不一致

那么就是NodeJS的Base64这么算可能不对了,这时我已经凌乱了,算了,干脆找个JS的Base64实现吧。我的大神啊,原来RawDeflate的作者自己也写了个Base64的实现,我觉得这里面有阴谋⋯⋯

用上这位大神的Base64,同时我也发现了RawDeflate里面有一个使用实例。例子里面在Deflate之前,把字符串进行了Base64.utob操作。但是我们用的测试字符是“hello,world”貌似没有非ACSII字符。

那么我们新的编码脚本如下:

var RawDeflate = require("./deflate"),
    Buffer = require("buffer").Buffer,
    fs = require("fs"),
    Base64 = require("./base64");

function encode(str) {
  return Base64.toBase64(RawDeflate.deflate(Base64.utob(str)));
}

console.log(encode("hello, world!"));
fs.writeFileSync("data.txt", encode("hello, world!"));

我们仍然使用之前的解码脚本,结果竟然通过了!ruby得到了“hello,world“!

其中原理不解,有待高人解答,但是灾难还没有结束哦。

RFC1951、RFC1950和RFC1951

因为对方服务器目前使用的是JAVA,那么我必须用JAVA测试才能OK。

JAVA的代码如下,只贴出核心代码吧:

InputStream in = new InflaterInputStream(new Base64InputStream(new FileInputStream(compressed)));

第一次试验的结果是,无法解压的,想哭啊,得到“unknown compression method”的错误。

我开始质疑JAVA的实现了。虽然也是zlib的前缀,但是InflaterInputStream这个类的JavaDoc里却没有写清楚它对应的算法。

我们知道,HTTP传输里面经常会用到gzip、deflate来压缩内容,只用在header里面写明“Content-encoding”。Web服务器和浏览器其实都是相当默契的。好吧,我必须承认,浏览器不愧是Web里面最复杂的软件。因为它handle了太多的东西。

RFC1950是zlib算法,RFC1951是deflate算法,RFC1952是Gzip算法。那么他们是三个独立的算法么?

我以为是,其实不是!

在HTTP 1.1的定义里面,也就是RFC2616是这么定义delfate传输压缩的:

deflate – The “zlib” format defined in RFC 1950 [31] in combination with the “deflate” compression mechanism described in RFC 1951 [29].

原来zlib和deflate是同时使用的!zlib仅仅作为一种容器格式,来承载通过deflate算法压缩的内容。就像AVI其实是个视频容器格式,里面的压缩算法可以是XVID也可以H.262!

回来想想RawDeflate的主页上也仅仅说明是RFC1951的实现,而且人家是”Raw“Deflate,Raw!看来是我自己不懂啊⋯⋯

跟我一样苦逼的人还是不少的,看这个帖子,有一个回复已经给出了JAVA里面的解决方案。

如果要解决单纯使用Deflate压缩的文件,必须传入一个自定义Inflater。

InputStream in = new InflaterInputStream(conn.getInputStream()), new Inflater(true));

更新了JAVA脚本之后,的确问题解决了。顺手把其他语言解码方案也试验出来了。基本上,除了JAVA需要,其他实现都不需要进行额外的判断,所以JAVA实在不是一个敏捷语言啊⋯⋯

Ruby解码实现

require 'base64'
require 'zlib'

def decode(str)
  Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(Base64.decode64(str))
end

File.open("data.txt", "r") do |data|
  str = data.readlines
  p decode str.join
end

Python解码实现

Python在压缩的时候,想得到纯粹的Deflate流也是需要额外的加工的,详见此处:http://stackoverflow.com/questions/1089662/python-inflate-and-deflate-implementations

Python的解码的实现也很直白:

import zlib
import base64

str = ""
with open("data.txt") as data:
    for line in data:
        print line
        str += line

def decode_base64_and_inflate(str):
    decoded_data = base64.b64decode( str )
    return zlib.decompress( decoded_data , -zlib.MAX_WBITS)

print decode_base64_and_inflate(str)

PHP解码实现

PHP一定要使用gzinflate才能解压纯Deflate流,gzuncompress是用来解压HTTP采用“deflate”方式压缩的数据的!

$data = file_get_contents("data.txt");

echo $data;
echo gzinflate(base64_decode($data));

// this command will throw an exception: data error
// echo gzuncompress(base64_decode($data));

JAVA实现

JAVA实现好累啊,这么罗嗦。还需要一个额外的一个JAR进行Base64操作。这里用的是Apache的Common Codec

//http://stackoverflow.com/questions/3932117/handling-http-contentencoding-deflate

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipException;

import org.apache.commons.codec.binary.Base64InputStream;

public class Inflate {

    public static void inflate(File compressed, OutputStream out) throws IOException {
        InputStream in = new InflaterInputStream(new Base64InputStream(new FileInputStream(compressed)), new Inflater(true));
        shovelInToOut(in, out);
        in.close();
        out.close();
    }

    /**
     * Shovels all data from an input stream to an output stream.
     */
    private static void shovelInToOut(InputStream in, OutputStream out) throws IOException {
//    The following code should be working but it produces strange suffix like "[B@13fcf0ce" now and then
//        byte[] buffer = new byte[1000];
//        int len;
//        while((len = in.read(buffer)) > 0) {
//            out.write(buffer, 0, len);
//            System.out.println(buffer);
//        }

        String line;
        InputStreamReader isr = new InputStreamReader(in);
        BufferedReader br = new BufferedReader(isr);
        while((line = br.readLine()) != null) {
            out.write(line.getBytes());
        }
    }

    /**
     * Main method to test it all.
     */
    public static void main(String[] args) throws IOException, DataFormatException {
        File compressed = new File("data.txt");
        try {
            inflate(compressed, System.out);
        } catch(ZipException e) {
            System.out.println(e.getMessage());
            throw e;
        }
    }

}

总结

编码问题总是一个挥之不去的阴影!

Post a Comment

Your email is never shared. Required fields are marked *

*
*