编码的本质

编码的本质含义是对一组事物进行标号。例:

  • 二进制编码: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对乱码字符的编码。同一套编码,两套字符之间有种神秘而又规律的联系。