最近做部分老接口的升级工作,这些老接口是采用php开发的,调用端读取文件发送到服务端;
新的服务端采用Java开发,在联调过程中发现,文件上传之后被损坏了,检查发现上传前后的文件大小发生了变化;通过调试发现,客户端上传的Content-Type为application/x-www-form-urlencoded,这意味着Web容器会将上传的文件二进制转换成字符串,而我们默认的编码为UTF-8;这意味着文件在编码转换过程中出现了问题,尝试将容器的编码改为ISO-8859-1,发现OK了;之前在工作中也遇到很多编码问题,但并未深入探讨,准备借这个机会详细了解下;
计算机是基于二进制0和1的,为了便于人类交流,产生了各种字符编码,将二进制转换为字符,可以简单的理解为一种映射关系,常见字符编码有以下几种:
US-ASCII:
可以表示128个字符,对应的二进制为0~127;ISO-8859-1:
也叫做Latin1,单字节编码,编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号;
因为ISO-8859-1编码范围使用了单字节内的所有空间,在支持ISO-8859-1的系统中传输和存储其他任何编码的字节流都不会被抛弃。换言之,把其他任何编码的字节流当作ISO-8859-1编码看待都没有问题。这是个很重要的特性,MySQL数据库默认编码是Latin1就是利用了这个特性。
GB2312:
一个小于127的字节表示的字符与ASCII相同,但两个大于127的字节连在一起时,就表示一个汉字,前面的一个字节(称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们组合出大约7000多个简体汉字,每个汉字占用2个字节的脑容量;UTF-8:
对于单字节字符,字节的第一位为0,后7位为这个符号的Unicode码,所以对于拉丁字母,UTF-8与ASCII码是一致的。
对于n字节(n>1)的字符,第一个字节前n位都设为1,第n+1位为0,后面字节的前两位一律设为10,剩下没有提及的位,全部为这个符号的Unicode编码。
再回到之前的问题,当采用new String(byte bytes[])将二进制转换为字符时,默认采用file.encoding指定的编码,如果未指定,则使用UTF-8;查看源码会发现,内部是使用StringDecoder完成转换的:
private StringDecoder(Charset cs, String rcn) {
this.requestedCharsetName = rcn;
this.cs = cs;
this.cd = cs.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
this.isTrusted = (cs.getClass().getClassLoader0() == null);
}
但比较坑爹的出现在这,可以看到当发生输入格式错误或发现无法映射的字符时,默认的行为是REPLACE,对于UTF-8是采用"\uFFFD"进行替换;
可以看到由于-1(0xFF)和-2(0xFE)无法被映射为UTF-8字符,因此默认采用"\uFFFD"(10进值65533)替换,导致转码前后的二进制发生了变化;因此对于二进制文件,最安全的方式是采用ISO-8859-1,由于它是单字节编码,不会发生数据的丢失;
其实上面问题的根源在于客户端未遵守HTTP协议规范,如果采用multipart/form-data方式上传文件,则不会将二进制转换为字符串,也就不会导致后面一系列的问题了;