写在前面
编解码的问题是个蛮大的概念,网上也搜过一些资料,讲的也都蛮详细的,写此一篇也是给自己做个纪要,以免日后再遇见编解码问题无法解决
遇见问题
在项目中使用的dubbo来做服务间通信的方式,有一个需求是将一个图片流显示给前端,由于项目依赖层级的原因,这个图片流只能在服务的最底层生成,然后向上传递,最后交给调用者。一开始就简单的将输出流inputStream作为返回值传出去了,结果报错,大意是不识别某个字符,刚开始没想到是编码格式的问题,以为就是不能传数据流,就想着要不把流转成string传出去,结果试了之后dubbo倒是没报错,生成的图片无法打开,debug发现,底层生成的inputStream的数据长度跟最后string转成的inputStream的长度不相同,才想到可能是编码格式的问题。
寻求解答
其实想到是编码格式的问题,第一反应是设置一个编码格式好了,于是给转码和解码都加了UTF-8格式的要求,结果一看仍然是不对的,这就有点懵逼了,因为本身对这块不是很熟,就想着那要不就稍微研究一下是为什么好了。
首先是原理
从原理上来说,计算机都是以二进制来储存所有的数据的,那要表达全世界各种各样的字符,就需要有相应的编码格式,告诉计算机要以什么样的规则来解析一段指定的二进制数据。而计算机在读取二进制数据的时候又规定了以八个二进制位为一个字节,即1byte,由于每位二进制可以有0和1两种形式,所以1byte可以表示256种不同含义。那用不同的编码格式来读取同样的二进制,可以得到不同的结果,比如同样的1000100011110001,这是十六位二进制数据,以ISO-8859-1解析和以UTF-8来解析,结果就是完全不一样的意思(当然我不知道这具体是啥意思,随手写的,保证结果不一样而已)。这也就是为什么,同样的一份txt文件,可以是一堆乱码,也可以是正常显示的文字的原因。
然后是原因
上面讲了一些计算机读取文件时显示具体内容的原理,提到了编码格式,现在为人熟知且常用的就那几种,ASCII,ISO-8859-1,UTF-8,UTF-16,GBK。这里面有两组是很相似的,一个是ASCII和ISO-8859-1,一个是UTF-8和UTF-16。首先是ASCII和ISO-8859-1,同样都是采用单字节读取,即用一个字节表示一个字母,不同的是ASCII表只能表示128个字符,因为最高位是0不变的;而ISO-8859-1会用到最高位的值,所以能够表示256种不同的字符。而UTF-8和UTF-16则是Unicode编码的实现,这里就不得不讲一下Unicode编码和UTF-8,UTF-16的区别。后两者的区别比较简单,UTF-8采用的是1~4个字节不定长的存储格式,而UTF-16则是两个字节或四个字节来存储字符。Unicode是我们熟知的编码字符集,而其实也只是字符集,只规定了符号的二级制代码,并没有规定如何存储。什么意思,比如一个“汉”字,Unicode编码为6C49,转化成二进制是1101100 01001001,这并不意味着计算机存储这个“汉”字就是这些二进制,还取决于具体的编码格式。所以通俗点说可以将Unicode理解为接口,而UTF-8和UTF-16理解为是接口的具体实现。
而上面这个“汉”字是如何存储的呢,比如我们采用UTF-8的方式来存储,UTF-8定义了两个简单的编码规则:
- 对于单字节的符号,字节第一位设0,后面的是这个符号的Unicode编码。所以对于英文字母,UTF-8编码和ASCII码是一样的。
- 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的Unicode码。
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
----------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
上面是具体的Unicode转UTF-8的编码规则,根据这张表,可以很轻松的定位不同的Unicode编码对应的UTF-8的编码格式,由于“汉”的Unicode编码是6C49,落在了上面的第三个区间,所以UTF-8编码格式则是111001101011000110001001,即用三个字节来储存。
接着是实践
了解了上面的理论,就随手实践一下,看看结果。首先是想一下情景,上面问题中提到的是从流转成字符,然后从字符再转成流,所以模拟一下这个过程
byte[] bytes = new byte[2];
bytes[0] = (byte) 130;
bytes[1] = 88;
String test = new String(bytes);
System.out.print(test);
上面这段代码模式的是字节流转成字符流的过程,注意byte[0]需要强转而1不需要,是因为Java默认一个byte的大小是0~127,最高位是符号位,而130由于大于127,则按照规定,是取互补的那个负数,即-126(如果说设置的是大于256,则一个字节无法表示,需要多个字节,这里面就需要更多的运算了,Java则是取最低的那8位)。然后new一个string出来,这里没有设置编码格式,Java默认使用的是UTF-8(可以通过System.out.println(Charset.defaultCharset());
来打印默认的编码格式),打印出来的结果是�X
,可以看见有两个字符,第一个是乱码,第二个识别出来是X,乱码是因为这个二进制流按照UTF-8的对照表找不到对应的字符,所以就无法显示。
这个时候再将这个字符流转成字节流试试呢
String test = new String(bytes);
byte[] bytes1 = test.getBytes();
debug一下看看结果,会发现byte1的数组长度不再是2了,而是4位
可以发现最后一位仍然是88,但是前三位是由130代表的字符流转变而来的,原因则也是因为130代表的Unicode字符大小落在了上面总结的编码表的第三区间,所以需要三个字节来存储,于是乎变成了三位,感兴趣的可以将这三个字节转成二进制看看是什么。
最后是结论
有了上面的一个小实践,这里明白了两个问题
- 我在最开始那个流转成字符串,再由字符串转成流的问题中遇见了一种情况,我使用ASCII码编码的时候,图片无法显示出来,但是字节数转化前后是一样的;但是当我使用ISO_8859_1编码格式的时候,就可以显示出图片了。于是可以得到的结论是这个图片流里面的字符肯定都是那0-256范围中的字符,由于ASCII码表无法表示128-256,所以才会有字节数一样,但是结果不一样的情况。
- 当我用UTF-8转变的时候,发现转换后的字节数组会比一开始的多很多,这可能是因为图片流中存在UTF-8无法识别的字符,比如,举个极端的例子,所有的8位二进制全部是10xxxxxx形式的,对应上面的UTF-8与Unicode转码表,就会发现无法对应,自然是得不到想要的结果的
再次象征性总结
这次码的也挺乱的,其实对Java中字节流与字符流的了解确实不够透彻,也源于对计算机原理的基础了解还不足,这次也算是个引子,后面还是要再多研究强化一下这方面的知识。
还有,这里参考了很多阮一峰大神的一篇关于编解码的文章,大神就是大神,讲的就是通俗易懂,虽然是十几年前的文章,但是仍然有很强的可读性
长路漫漫~~