编码的本质
编码的本质含义是对一组事物进行标号。例:
- 二进制编码:1是对高电平的标号,0是对低电平标号
- ASCII编码:61是对字符图像
a
的标号,41是对字符图像A
的标号 - UTF8编码:113是字符图像
国
的标号 - base64编码:对数字
0
,标记为A; 对数字63
,标记为 /
由上可见,标号的对象可以是任何事物,如电平、图像、甚至是数字。而标记的记号,也不一定是数字,如base64反直觉的用图像反过来标记数字。这取决于编码的实际用途,ASCII、UTF8这类字符集编码,目的是为了把图像转换成二进制; 而二进制编码,其中一个用途就是把电信号抽象成0、1两个数字,实现硬件层面的信息传输; 此时,二进制就成为了虚拟图形和现实电信号的桥梁,真正意义上的实现了底层的数据传输。而对于某些二进制数据,没有相应的字符集可以把它们转换为图像,为了便于人为操作这类数据(阅读、复制粘贴),base64出现了。
总结一下,上述各编码的用途:
- 二进制编码:底层信息传输的基础
- 字符集编码:为了使文字图形能够在硬件层面传输
- base64编码:为了方便人类操作二进制数据,主要还是复制粘贴
到此为止,我们可以说,对照着某种规则,把目标转为编号,称之为编码;把编号转为目标对象,称之为解码。
JVM 参数:file.encoding
jvm 运行时采用unicode编码,所以java内部不会存在乱码问题。但是跟外部交互的文本,还是需要关注编码。
如果设置了file.encoding参数,System.in、System.out、string.getBytes()等都会使用这个设置的编码解析数据,否则使用平台的默认编码。
JDBC编码
因为JAVA系统运行时采用unicode编码,如果数据库采用GBK或者utf8编码,则可能出现乱码问题。
Oracle数据库采用唯一编码,oracle的jdbc会自动将unicode转换为目标Oracle数据库的编码。
而Mysql可以同时使用多种编码,所以需要在Mysql JDBC的url中指定目标数据库的编码。
servlet服务请求、响应编码
ContentType
可在请求、响应的ContentType处指定body的编码,如下:Content-Type:application/x-www-form-urlencoded;charset=utf-8
servletFilter
也可以统一在
无论是Spring的CharacterEncodingFilter还是request的setCharacterEncoding,都是为byte[]指定解码的规则,而不是转换。
注意:CharacterEncodingFilter要设置在其他filter之前,以免parameter被提前解析
urlEncode
urlEncode就是对url特殊字符 = 、&、? 等进行转义的编码方式,需要指定字符集编码,流程是:先编码 -> 转成HexString -> 每个HexString前加%
经测试,Edge、postMan默认中文编码都是utf8
tomcat 连接器编码
在post请求中,url和body是可以分开编码的(参考[[#urlEncode]]),而body可以相对灵活地指定编码。useBodyEncodingForURI、UriEncoding 都是用于指定 url 解码用的。当未指定时,tomcat默认使用utf8解码。
关于body、url 编码的更详细研究,可以参考该文章
1、UriEncoding
解析参数时默认指定的url解码字符集,映射于tomcat源码中的:queryStringCharset
2、useBodyEncodingForURI
此时把该配置设为true
,可以在解析url时使用body指定的编码(参考[[#ContentType]]),相当于强行覆盖body编码给url。它只是针对如”author=君山”的查询参数(QueryString)有效,他的设置对于URL和URI无效。
编码优先级
body:contentType > filter > 默认
url:useBodyEncodingForURI > UriEncoding
C2 C3疑案
在服务器接收中文参数(“捕鱼”)后,用iSO-8859-1解码,字符串变乱码,对乱码用不同的字符集编码,得到了两套HexString
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
客户端utf8编码 | E6 | 8D | 95 | E9 | B1 | BC |
8859 解码得到乱码字符 | æ | | | é | ± | ¼ |
8859对字符再编码 | E6 | 8D | 95 | E9 | B1 | BC |
utf8对字符再编码 | C3 A6 | C2 8D | C2 95 | C3 A9 | C2 B1 | C2 BC |
可以发现,①8859对乱码字符的编码,②utf8对乱码字符的编码。同一套字符,两套编码之间有种神秘而又规律的联系。
通过对比UTF8、ISO-8859-1两套字符集的对比,可以还原这里面的真相
utf8编码规则
UTF-8是一种变长字节编码方式。对于某一个字符的UTF-8编码,如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的位数,其余各字节均以10开头。UTF-8最多可用到6个字节。
如表:
1字节 0xxxxxxx
2字节 110xxxxx 10xxxxxx
3字节 1110xxxx 10xxxxxx 10xxxxxx
4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
结论
西欧码(8859)的编码方式是纯序列。而UTF8的编码方式是 Unicode序列+特殊前缀,是为了达到可变长的目的。
ISO-8859-1字符集,可以算作ASCII的拓展,共256个字符,用一个Byte可以装完。所以当使用 8859 解码,得到的是与字节数一一对应的字符。而在UTF8字符集中,前256个字符的序列和 8859 是一致的。只是在编码时,还会对字符序列加上特定规则([[#utf8编码规则]])。此时UTF8再编码的序列中,频繁出现的C2、C3,实际上都是因为utf8的这种特殊的前缀规则导致的。
这个问题极易误导人地方在于,它可以让你解读为:①utf8对原字符的编码,②utf8对乱码字符的编码。同一套编码,两套字符之间有种神秘而又规律的联系。