[原]php处理base64编码和Unicode客户端交互的问题

最近才对编码问题真正深入研究。因为我碰到了这个问题:客户端传过来的是对UTF-16的base64编码,而php没有办法正确解码。
有关编码,可以先看看这篇文章:字符,字节和编码
关于UTF-8和UTF-16,可以参考:谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词
有关URL的base64处理,可以参考这篇:通过 URL 传递 base64 编码参数的问题
摘录如下:

一般情况下,URL 中的参数应使用 url 编码规则,即把参数字符串中除了 -_. 之外的所有非字母数字字符都将被替换成百分号(%)后跟两位十六进制数,空格则编码为加号(+)。但是对于带有中文的参数来说,这种编码会使编码后的字符串变得很长。如果希望有短一点的方式对参数编码,可以采用 base64 编码方式对字符串进行编码,但是 base64 编码方式不能处理 JavaScript 中的中文,因为 JavaScript 中的中文都是以 UTF-16 方式保存的。而 base64 只能处理单字节字符,所以不能直接用 base64 对带有中文的 JavaScript 字符串进行编码。但是可以通过 utf.js 这个程序中提供的 utf16to8 来将 UTF-16 编码的中文先转化为 UTF-8 方式,然后再进行 base64 编码。这样编码后的字符串,在传递到服务器端后可以直接通过 base64_decode 解码成 UTF-8 的中文字符串。但是还有个问题需要注意。base64 编码中使用了加号(+),而 + 在 URL 传递时会被当成空格,因此必须要将 base64 编码后的字符串中的加号替换成 %2B 才能当作 URL 参数进行传递。否则在服务器端解码后就会出错。

不过,我对上述的“而 base64 只能处理单字节字符”有疑问,这是base64的标准规定,还是php的缺陷?
如果是php的缺陷,我想应该能写一个针对UTF-16的base64解码函数。这样就不需要客户端来进行UTF16转成UTF8了。

补充:
按照RFC2045的定义,Base64被定义为:Base64内容传送编码被设计用来把任意序列的8位字节描述为一种不易被人直接识别的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)
上面第二篇摘录如下:

UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?

Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一个有点小聪明的想法:

在UCS编码中有一个叫做”ZERO WIDTH NO-BREAK SPACE”的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符”ZERO WIDTH NO-BREAK SPACE”。

这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符”ZERO WIDTH NO-BREAK SPACE”又被称作BOM。

UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符”ZERO WIDTH NO-BREAK SPACE”的UTF-8编码是EF BB BF(读者可以用我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。

Windows就是使用BOM来标记文本文件的编码方式的。

也就是说,base64编码必须针对单字节。从这个角度说,是不能对UTF-16的字串进行base64编码操作的。难怪php解码后得到的结果不一致。原来问题不是出在php上。

我又看了客户端base64编码部分的代码,确实是客户端的问题。客户端用了“(const BYTE*)sSrcBuf ”把UTF-16的双字节强制转为BYTE流之后进行base64编码,结果是错误的。难怪我解码以后英文字符之间都有莫名奇妙的“空格”(实际是“00”,但是记事本看网页源代码上的“00”显示成空格)。
实际上是这样的:比如“ABC”,UTF-8字节流是“41 42 43”。而UTF-16则是“41 00 42 00 43 00”,所以“(const BYTE*)sSrcBuf ”之后,“00”就被作为单独的一个字节进行了转码,所以解码以后就还原为UTF-8的字节流“41 00 42 00 43 00”,这时候跟初始的“41 42 43”已经不一致了(print的时候只能打出“A”)。难怪我的SQL会报出“ERROR: unterminated quoted string at or near “‘A” at character”的错,因为数据库读取SQL读到“00”就认为结束了。
这时候我冒出一个想法,要是服务器端解码以后再强制把字节流当做UTF-16转为UTF-8,结果不就一样了吗?
事实证明我的想法是正确的:

最后还有一个问题,UTF-16的BOM不一定是FF FE,也可能是FE FF。这怎么办?要是有ASCII字符还好办,两个两个的找,看“00”是在前还是在后。要是双字节都有内容呢?举上面文章的例子:“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?
这个问题单纯改服务器端php没有办法解决。最终的解决办法只有两个:
1)如上面文章所说,客户端UTF-16先转成UTF-8再base64编码,再提交;显示的时候转回UTF-16
2)客户端可以基于UTF-16的字节流base64编码,但是提交的时候要带上“BOM”,即指明是“FFFE”,还是“FEFF”

当然,如果能确定客户端的Unicode的BOM方式,上面的2)也可以不用。
直接在服务器端加上BOM标记进行转码即可。
好像Windows下都是FFFE的,所以可以直接用这个:

[原]php处理base64编码和Unicode客户端交互的问题》有2个想法

  1. 平客

    呵呵,不错,研究的很透彻,不过有一点不太准确,就是base64只针对byte,其实base64的本质用意是什么吧,就是比如你有一个二进制文件,如果你想“打印”出来怎么办?里面什么样的字符都有,有些根本就不是可打印字符。而通过base64编码,就可以将任何二进制流转成人眼或打印机可识别的字符。这其实最早用于邮件传输附件,因为邮箱正文肯定是可打印字符,但是附件可以是任意二进制文件。而以前的邮箱服务器无法传输任意的二进制流。
    换句话说,比如我们有一个很重要的可执行文件,保存在磁盘上怕坏了,放网站上怕丢了。呵呵,这时,我们可以将它“打印”在纸上保存起来。
    也就是先把*.exe二进制流转成base64编码,当然转化后都是可打印字符。然后打印在纸上。我再把纸给你,你原样一个一个字符转成文件文件,再把文本文件通过base64解码,还原成二进制流,这样,就相当于我的程序保存在我们平常所用的“纸上”了。哈哈。当然,你肯定想到了,保存一个几十K的小程序就得很厚的一叠纸。没办法,base64编码后要比二进制多好多的字节。
    在我们的项目中,我是把unicode字符串进行base64编码了,这并不是说base64只能对ansi编码,其实只要是二进制流,都可以的,更不用说unicode字符串了,它不就是一段二进制流吗?关键是解码后你要知道原来的是unicode?还是ansi,更或者是一幅图片的数据呢,还是一个程序的二进制代码。

发表评论