最近不小心被一些编码相关的东西困扰了。这些从Web诞生便产生的一些编码问题,在2012年的今天仍然可以让一个程序员抓狂。项目的大致需求是这样的,客户端需要将一个字符串进行如下处理:
- 将字符串进行Deflate压缩
- 将压缩后的结果再进行Base64编码
那么服务器端,需要进行上述流程的反过程。问题是,由于这是个开放接口,客户端可以使用各种不同的平台进行开发,如JS、Java、Ruby、PHP等。好在,大多数语言都是基于zlib包装了自己的相关API,那么也算是实现标准了。窘迫的事情时,NodeJS里面zlib的实现是在0.6以后加入的,我们使用的技术仍然是0.4版本的。
迫于时间有限,切换到0.6或者实现zlib的binding都是不靠谱的,于是我打开了万能的Github。果然功夫不负有心人,找到了Deflate的纯JS实现——RawDeflate。顿时对作者的崇拜之心油然而生。殊不知,悲剧就是从这里开始的。
Base64算法
为了测试有效性,我用JS做了客户端编码工作,然后用Ruby实现了服务器端解码工作。代码如下:
- 被我修改过的deflate.js
- JS编码脚本,v1
- Ruby解码脚本,v1
这里只把编码脚本和解码脚本贴出来,其他的已经在上面给出了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,可以发现几个现象:
- 仅仅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;
}
}
}
总结
编码问题总是一个挥之不去的阴影!









