一、计算机编码概述
1、ASCII码
大家都知道美国人发明了计算机,发明计算机过程中面临的一个问题就是如何将美国生活中的字符存储到计算机。这些字符包括可见字符(数组、英文字母、标点符号)和不可见字符(控也称为控制字符,如空格、换行、回车)总共128个
将这些字符分别和数字一一对应起来,如下表,从0开始到127总共128个,0、1、2...127称之为码位,其中0-31以及127为控制字符共33个,32-126码位对应可见字符共95个,美国人称这个表为ASCII字符集。
有了这个字符集,下面问题就是如何将这个字符表存入计算机呢?美国人采用将码位转成对应的二进制,然后把二进制存储到计算机中。
但是最大的码位127对应的二进制就是1111111 占7比特,计算机处理信息的最小单位之字节,一个字节8比特,所以为了凑整,都在前面加个0,就有了如下码位对应的二进制列表,这个二进制列表称为ASCII码
字符 | 码位 | 二进制 | 存储在计算机内容(ASCII码) |
空字符 | 0 | 0 | 0000 0000 |
标题开始 | 1 | 1 | 0000 0001 |
... | ... | ... | ... |
0 | 48 | 110000 | 0011 0000 |
... | ... | ... | ... |
删除 | 127 | 11111111 | 1111 1111 |
什么是编码:上面这个将 字符转成映射计算机存储内容 过程就是编码
上面ASCII码从0000 0000 到0111 1111总共128个,美国人已经基本使用没啥问题了,但是当欧洲其他国家在使用计算机过程中,发现自己国家好多字符没有在这个ASCII字符集里,于是对ASCII码表进行扩展,新增了1000 0000 到1111 1111共128新字符,ASCII字符集达到256个,新增的字符称为”ISO-8859-1字符集“,对应的二进制称为”ISO-8859-1码”,ISO-8859-1编码是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
英语虽然没有重音字母,但仍会标明为ISO/IEC 8859-1编码。除此之外,欧洲以外的部分语言,如南非荷兰语、斯瓦希里语、印尼语及马来语、菲律宾他加洛语等也可使用ISO/IEC 8859-1编码。
法语及芬兰语本来也使用ISO/IEC 8859-1来表示。但因它没有法语使用的 œ、Œ、Ÿ 三个字母及芬兰语使用的 Š、š、Ž、ž ,故于1998年被ISO/IEC 8859-15所取代。(ISO 8859-15同时加入了欧元符号)
ISO-8859-1标准中0x80-0xFF为控制字符。ISO-8895-15去除了0x80-0xFF中的控制字符,在0x80-0xFF加入了œ、Œ、Ÿ 、Š、š、Ž、ž等字母和欧元(€)、单引号(‘’)、双引号(“”)、斜体f(ƒ)、省略号(…)、商标(™)、千分号(‰)等常用符号
Latin1是ISO-8859-1的别名,有些环境下写作Latin-1
ISO-8859-1编码参照表参考地址
3、GB2312
当中国人开始使用计算机,对于扩展的ISO-8859-1码也只能表示256个字符,而中国汉字远远大于256个字符,那就的设计自己的编码方式,所谓的字符集编码就是对字符集编码,编码之前首先要设计出字符集,就像ASCII码一样,先设计罗列出ASCII字符集再设计出ASCII码,
设计字符集过程是对所收录字符进行了“分区”处理,共94个区,每区含有94个位,共8836个码位。这种表示方式也称为区位码。
01-09区收录除汉字外的682个字符。
10-15区为空白区,没有使用。
16-55区收录3755个一级汉字,按拼音排序。
56-87区收录3008个二级汉字,按部首/笔画排序。
88-94区为空白区,没有使用。
举例来说,“啊”字是GB2312编码中的第一个汉字,它位于16区的01位,所以它的区位码就是1601。
字符集列表参考地址
1981年5月1日发布的简体中文汉字编码国家标准,GB2312规定对收录的每个字符采用两个字节表示,第一个字节为“高字节”,对应94个区;第二个字节为“低字节”,对应94个位。所以它的区位码范围是:0101-9494。区号和位号分别加上0xA0就是GB2312编码。例如最后一个码位是9494,区号和位号分别转换成十六进制是5E5E,0x5E+0xA0=0xFE,所以该码位的GB2312编码是FEFE。
GB2312编码范围:A1A1-FEFE,其中汉字的编码范围为B0A1-F7FE,第一字节0xB0-0xF7(对应区号:16-87),第二个字节0xA1-0xFE(对应位号:01-94)。
4、BIG5编码
台湾地区繁体中文标准字符集,采用双字节编码,共收录13053个中文字,1984年实施。
5、GBK
1995年12月发布的汉字编码国家标准,是对GB2312编码的扩充,对汉字采用双字节编码。GBK字符集共收录21003个汉字,包含国家标准GB13000-1中的全部中日韩汉字,和BIG5编码中的所有汉字。
6、GB18030
GBK不包括少数名族字符,于是对GBK扩展新增,有了GB18030,2000年3月17日发布的汉字编码国家标准,是对GBK编码的扩充,覆盖中文、日文、朝鲜语和中国少数民族文字,其中收录27484个汉字。GB18030字符集采用单字节、双字节和四字节三种方式对字符编码。兼容GBK和GB2312字符集。
7、Unicode编码
中国编码就有GB2312、GBK、GB18030好几种,其他国家也就会有许多,为了统一ISO组织设计出了一个规则,叫Unicode规则(它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。,有的人称为Unicode编码也有的称为Unicode规则,因为包括字符集和编码),最初Unicode规则字符集是UCS-2字符集,编码和ASCII字符集一样,也是将码位和编码一一对应,从0x0000到0xFFFF总共65536个字符,但是后续65536个字符还是不够,后续又有了UCS-4字符集,采用四个字节为每个字符编码,从0x00000000到0xFFFFFFFF可以表示43亿个字符
8、UTF-8和UTF-16编码
上面的Unicode编码每个字符需要占用四个字节表示,比较浪费空间,所以很长一段时间,Unicode编码很被接受,随着互联网发展,需要交换的信息越来越多,出现了UTF-8和UTF-16编码,UTF-8是每次传输8位数据,是一种可变长编码方式
UTF-8将UCS-4划分成四个区间,每个区间对应的UTF-8编码样式如下:
1区间 0x0000 0000至0x0000 007F : 0xxxxxxx 单字节编码,以0开始
2区间 0x0000 0080至0x0000 07FF :110xxxxx 10xxxxxx:双字节编码形式
3区间 0x0000 0800至0x0000 FFFF :1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式
4区间 0x0001 0000至0x0010 FFFF :11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式
比如 UCS-4中的 “王” 码位是0x0000 738B 二进制是0000 0000 0000 0000 01111 0011 1000 1011
总共32位
并且 “王” 码位属于第三区间,根据第三区间编码样式和上面二进制就可以得出"王"对应的UTF-8编码
只要将二进制从后往前以此插入到编码样式即可,如下得到"王"对应的UTF-8编码是
11100111 10001110 10001011 转成16进制就是 0xe7 0x8e 0x8b
二、锟斤拷
上面学习了字符编码之后,可以认识到字符编码重要性,许多文本编译器基本都可以指定编码进行读写,比如指定UTF-8读写,html也会通过head里面的meta指定网页编码,大部分默认都是UTF-8
锟斤拷是一个常见的乱码,一般发生在UTF-8和中文编码(比如GBK)转换过程中发生,unicode字符集有一个特殊的符号 ,当把字符转成utf-8时,会把无法识别的或展示的字符自动替换为替换符号 ,用于提示用户,经过utf8编码后对应的3字节二进制是
11101111 10111111 101111101
对应的十六进制就是EF BF BD,如果有两个无法识别的字符,那就是EF BF BD EF BF BD,此时把文件用GBK读取查看,所以两个 变成了锟斤拷,因为GBK中每个汉字用两个字节读取,如下,EF BF 对应汉字锟,BD EF 对应汉字斤,BF BD 对应汉字拷,因此锟斤拷是从UTF-8两个特殊符号而来
三、java中的编码
1、概述
java.nio.charset包中提供了Charset类,它继承了Comparable接口;还有CharsetDecoder、CharsetEncoder编码和解码的类,它们都是继承Object类。
当java程序在非Unicode字符集的操作系统中与其他程序进行数据交换时可能会出现乱码的情况,这时就需要对交换的数据进行编码和解码。向ByteBuffer中存放数据的时候需要考虑字符的编码,从中读取的时候也需要考虑字符的编码方式,也就是编码和解码。
编码和解码类如下:Charset、CharsetDecoder、CharsetEncoder类
2、方法
1、静态方法获取JDK所支持的所有字符集集合
SortedMap<String,Charset> sortedmap = Charset.availableCharset();
System.out.println("****************当前JDK所支持的字符集********************");
for (var name : sortedmap.keySet()){
System.out.println(name + ": " +sortedmap.get(name));
}
2、获取字符集有如下两种方式
//返回指定的字符集CharSet
Charset charset = Charset.forName("utf8");
//返回虚拟机默认的字符集CharSet
Charset charset = Charset.defaultCharset();
3、接下来我们使用字符集CharSet创建一个编码器和一个解码器
//编码器
CharsetEncoder encoder = charset.newEncoder();
//解码器
CharsetDecoder decoder = charset.newDecoder();
4、使用编码器和解码器解析数据
//编码,传入CharBuffer
ByteBuffer bytebuffer = encoder.encode(in);
//解码,传入ByteBuffer
CharBuffer charbuffer = decoder.decode(in);
String str = "大家好";
//把字符串按照 GBK 编码回字节数组,在以 ISO-8859-1 进行解码,得到字符串
String strISO=new String(str.getBytes("GBK"), StandardCharsets.ISO_8859_1);
System.out.println(" 使用 ISO-8859-1 字符编码输出 "+strISO);
//把字符串按照 ISO-8859-1 编码回字节数组,在以 GBK 进行解码,得到字符串
String strGBK=new String(strISO.getBytes(StandardCharsets.ISO_8859_1),"GBK");
System.out.println(" 将 ISO-8859-1 转 GBK 字符编码输出 "+strGBK);
3、字符串和字节数组转换
String str = "大家好";
//转换
byte[] bytes = str.getBytes();
String s = new String(bytes);
//指定字符集转换
byte[] bytes1 = str.getBytes(StandardCharsets.UTF_8);
String s1 = new String(bytes,StandardCharsets.UTF_8);
4、应用
常见的网络请求中的乱码处理
public void service(HttpServletResponse response, HttpServletRequest request) throws IOException { System.out.println( Charset.defaultCharset());//java 默认编码 UTF-8 //request 默认是UTF-8 String characterEncoding1 = response.getCharacterEncoding(); //response 默认是ISO-8859-1 String characterEncoding = request.getCharacterEncoding(); /** * 方式一 * 1、先设置服务器字符集为UTF-8 * 2、再通过响应头告诉浏览器也使用UTF-8 */ response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Type","text/html;charset=UTF-8"); /** * 方式二 * 直接设置ContentType */ response.setContentType("text/html;charset=UTF-8"); response.getWriter().write("测试"); }
四、编码算法
1、URL编码
URL编码是浏览器发送数据给服务器时使用的编码,它通常附加在URL的参数部分,例如:
之所以需要URL编码,是因为出于兼容性考虑,很多服务器只识别ASCII字符。但如果URL中包含中文、日文这些非ASCII字符怎么办?不要紧,URL编码有一套规则:
- 如果字符是
A
~Z
,a
~z
,0
~9
以及-
、_
、.
、*
,则保持不变; - 如果是其他字符,先转换为UTF-8编码,然后对每个字节以
%XX
表示。
例如:字符 中
UTF-8编码是0xe4b8ad
,因此,它的URL编码是%E4%B8%AD
。URL编码总是大写。
Java标准库提供了一个URLEncoder
类来对任意字符串进行URL编码:
import java.net.URLEncoder; import java.nio.charset.StandardCharsets; public class Main { public static void main(String[] args) { String encoded = URLEncoder.encode("中文!", StandardCharsets.UTF_8); System.out.println(encoded); } }
上述代码的运行结果是%E4%B8%AD%E6%96%87%21
,中
的URL编码是%E4%B8%AD
,文
的URL编码是%E6%96%87
,!
虽然是ASCII字符,也要对其编码为%21
。
和标准的URL编码稍有不同,URLEncoder把空格字符编码成+
,而现在的URL编码标准要求空格被编码为%20
,不过,服务器都可以处理这两种情况。
如果服务器收到URL编码的字符串,就可以对其进行解码,还原成原始字符串。Java标准库的URLDecoder
就可以解码:
public class Main { public static void main(String[] args) { String decoded = URLDecoder.decode("%E4%B8%AD%E6%96%87%21", StandardCharsets.UTF_8); System.out.println(decoded); } }
要特别注意:URL编码是编码算法,不是加密算法。URL编码的目的是把任意文本数据编码为%
前缀表示的文本,编码后的文本仅包含A
~Z
,a
~z
,0
~9
,-
,_
,.
,*
和%
,便于浏览器和服务器处理。
2、Base64编码
URL编码是对字符进行编码,表示成%xx
的形式,而Base64编码是对二进制数据进行编码,表示成文本格式。
Base64编码可以把任意长度的二进制数据变为纯文本,且只包含A
~Z
、a
~z
、0
~9
、+
、/
、=
这些字符。它的原理是把3字节的二进制数据按6bit一组,用4个int整数表示,然后查表,把int整数用索引对应到字符,得到编码后的字符串。
举个例子:3个byte数据分别是e4
、b8
、ad
,按6bit分组得到39
、0b
、22
和2d
:
┌───────────────┬───────────────┬───────────────┐ │ e4 │ b8 │ ad │ └───────────────┴───────────────┴───────────────┘ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ │1│1│1│0│0│1│0│0│1│0│1│1│1│0│0│0│1│0│1│0│1│1│0│1│ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘ ┌───────────┬───────────┬───────────┬───────────┐ │ 39 │ 0b │ 22 │ 2d │ └───────────┴───────────┴───────────┴───────────┘
因为6位整数的范围总是0
~63
,所以,能用64个字符表示:字符A
~Z
对应索引0
~25
,字符a
~z
对应索引26
~51
,字符0
~9
对应索引52
~61
,最后两个索引62
、63
分别用字符+
和/
表示。
在Java中,二进制数据就是byte[]
数组。Java标准库提供了Base64
来对byte[]
数组进行编解码:
public class Main { public static void main(String[] args) { byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad }; String b64encoded = Base64.getEncoder().encodeToString(input); System.out.println(b64encoded); } }
编码后得到5Lit
4个字符。要对Base64
解码,仍然用Base64
这个类:
public class Main { public static void main(String[] args) { byte[] output = Base64.getDecoder().decode("5Lit"); System.out.println(Arrays.toString(output)); // [-28, -72, -83] } }
有的童鞋会问:如果输入的byte[]
数组长度不是3的整数倍肿么办?这种情况下,需要对输入的末尾补一个或两个0x00
,编码后,在结尾加一个=
表示补充了1个0x00
,加两个=
表示补充了2个0x00
,解码的时候,去掉末尾补充的一个或两个0x00
即可。
实际上,因为编码后的长度加上=
总是4的倍数,所以即使不加=
也可以计算出原始输入的byte[]
。Base64编码的时候可以用withoutPadding()
去掉=
,解码出来的结果是一样的:
public class Main { public static void main(String[] args) { byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad, 0x21 }; String b64encoded = Base64.getEncoder().encodeToString(input); String b64encoded2 = Base64.getEncoder().withoutPadding().encodeToString(input); System.out.println(b64encoded); System.out.println(b64encoded2); byte[] output = Base64.getDecoder().decode(b64encoded2); System.out.println(Arrays.toString(output)); } }
因为标准的Base64编码会出现+
、/
和=
,所以不适合把Base64编码后的字符串放到URL中。一种针对URL的Base64编码可以在URL中使用的Base64编码,它仅仅是把+
变成-
,/
变成_
:
public class Main { public static void main(String[] args) { byte[] input = new byte[] { 0x01, 0x02, 0x7f, 0x00 }; String b64encoded = Base64.getUrlEncoder().encodeToString(input); System.out.println(b64encoded); byte[] output = Base64.getUrlDecoder().decode(b64encoded); System.out.println(Arrays.toString(output)); } }
Base64编码的目的是把二进制数据变成文本格式,这样在很多文本中就可以处理二进制数据。例如,电子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用Base64编码,然后以文本的形式传送。
Base64编码的缺点是传输效率会降低,因为它把原始数据的长度增加了1/3。
和URL编码一样,Base64编码是一种编码算法,不是加密算法。
如果把Base64的64个字符编码表换成32个、48个或者58个,就可以使用Base32编码,Base48编码和Base58编码。字符越少,编码的效率就会越低。
3、小结
URL编码和Base64编码都是编码算法,它们不是加密算法;
URL编码的目的是把任意文本数据编码为%前缀表示的文本,便于浏览器和服务器处理;
Base64编码的目的是把任意二进制数据编码为文本,但编码后数据量会增加1/3。
今日头条系列之:java中的base64编码器 - 今日头条