package com.google.common.base;
今天在找base包下的源码阅读时,看到了Charsets,肯定是定义字符集的类,本来就想简单的看一下。(部分内容摘抄自:http://blog.csdn.net/sundaysunshine/article/details/53954813)
随后想到在web工程里一直会遇到编码问题,于是想总结一下编码的问题。
先看看Charsets的定义的一些常量吧。
public static final Charset US_ASCII = Charset.forName("US-ASCII");
public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
public static final Charset UTF_8 = Charset.forName("UTF-8");
public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
public static final Charset UTF_16 = Charset.forName("UTF-16");
从源码中,我们可以看到它定义的一些字符集。
我们在编写web工程时,肯定会遇到:1、页面乱码,2、servlet获取数据乱码,3、数据库乱码
那么我们是怎么解决的呢,大约就是这些转码方式:
1、在jsp页面头设置编码方式:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
2、纯粹html乱码: 最好是在<head></head>之间添加<meta charset="utf-8">
3、<%request.setCharacterEncoding("utf-8");%>
4、response.setContentType("text/hmtl;charset=UTF-8");
5、使用getBytes()重新构造接收的String: new String(username.getBytes("ISO-8859-1"), "UTF-8");
6、另外就是编码与解码
比如存储cookie信息时:
Cookie userNameCookie = new Cookie("userName", URLEncoder.encode(user.getUserName(), "utf-8")); //存入信息时编码
userName = URLDecoder.decode(cookies[i].getValue(), "utf-8"); //取信息时,进行解码
那么疑问来了,为什么会出现乱码的情况呢?
答案就是只要有I/O操作的地方都会可能有乱码发生。
那么再看看为什么会出现乱码?(下面部分内容摘抄自:http://blog.csdn.net/sundaysunshine/article/details/53954813)
我们知道I/O有四大家族InputStream, OutputStream,Writer,Reader前两个是基于字节的操作,后两个是基于字符的操作。由于我们平时是使用字符的方式进行记录信息,但由于计算机只认识0和1,所以I/O作为人机交互的手段,它的底层一定是以字节的方式进行传送的,编码问题其实就是发生在字符和字节之间相互转换的时候。
读操作:
写操作:
也就是说如果我们用错了字典,那么编码或解码出来的数据就会可能出现问题。
下面介绍一下各个编码:
- ASCII 码
学过计算机的人都知道 ASCII 码,总共有 128 个,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126 是打印字符,可以通过键盘输入并且能够显示出来。
- ISO-8859-1
128 个字符显然是不够用的,于是 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,所有应用的最广泛。ISO-8859-1 仍然是单字节编码,它总共能表示 256 个字符。
- GB2312
它的全称是《信息交换用汉字编码字符集 基本集》,它是双字节编码,总的编码范围是 A1-F7,其中从 A1-A9 是符号区,总共包含 682 个符号,从 B0-F7 是汉字区,包含 6763 个汉字。
- GBK
全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。
- GB18030
全称是《信息交换用汉字编码字符集》,是我国的强制标准,它可能是单字节、双字节或者四字节编码,它的编码与 GB2312 编码兼容,这个虽然是国家标准,但是实际应用系统中使用的并不广泛。
- UTF-16
说到 UTF 必须要提到 Unicode(Universal Code 统一码),ISO 试图想创建一个全新的超语言字典,世界上所有的语言都可以通过这本字典来相互翻译。可想而知这个字典是多么的复杂,关于 Unicode 的详细规范可以参考相应文档。Unicode 是 Java 和 XML 的基础,下面详细介绍 Unicode 在计算机中的存储形式。
UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。
- UTF-8
UTF-16 统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成。
UTF-8 有以下编码规则:
- 如果一个字节,最高位(第 8 位)为 0,表示这是一个 ASCII 字符(00 - 7F)。可见,所有 ASCII 编码已经是 UTF-8 了。
- 如果一个字节,以 11 开头,连续的 1 的个数暗示这个字符的字节数,例如:110xxxxx 代表它是双字节 UTF-8 字符的首字节。
- 如果一个字节,以 10 开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节
0xxxxxxx
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
网络端的I/O主要就拿以https作为基点说,因为https(安全为目标的HTTP通道,简单讲是HTTP的安全版,数据传输时用SSL加锁)是未来的一种趋势。HTTPS的I/O场景如下:
网络端的https请求实质是通过URL链接的。所以网络I/O的更多的情况下依赖于URL的实现的。
一个简单的URL包括如下图:
https是协议名,www.baidu.com是域名,紫色部分就是请求时携带的信息。可以看到路径中出现了中文,所以如果服务器端解码时使用了错误的编码格式,将会导致请求失败,同时当服务器端发送返回数据时如果浏览器端采用了错误的编码方式也会导致乱码。具体来说包括(这里忽略https里面的SSL加密和解密的过程):
HTTPS Header (报头)的编解码
通常被分为4个部分:general header, request header, response header, entity header。
当客户端发起一个 HTTPS请求除了上面的 URL 外还可能会在 Header 中传递其它参数如 Cookie、redirectPath 等,这些用户设置的值很可能也会存在编码问题,服务器(以Tomcat为例) 对它们又是怎么解码的呢?
对 Header 中的项进行解码也是在调用 request.getHeader 是进行的,如果请求的 Header 项没有解码则调用 MessageBytes 的 toString 方法,这个方法将从 byte 到 char 的转化使用的默认编码也是 ISO-8859-1,而我们也不能设置 Header 的其它解码格式,所以如果你设置 Header 中有非 ASCII 字符解码肯定会有乱码。
我们在添加 Header 时也是同样的道理,不要在 Header 中传递非 ASCII 字符,如果一定要传递的话,我们可以先将这些字符用 org.apache.catalina.util.URLEncoder 编码然后再添加到 Header 中,这样在浏览器到服务器的传递过程中就不会丢失信息了,如果我们要访问这些项时再按照相应的字符集解码就好了。
POST 表单的编解码
在前面提到了 POST 表单提交的参数的解码是在第一次调用 request.getParameter 发生的,POST 表单参数传递方式与 QueryString 不同,它是通过 HTTPS 的 BODY 传递到服务端的。当我们在页面上点击 submit 按钮时浏览器首先将根据 ContentType 的 Charset 编码格式对表单填的参数进行编码然后提交到服务器端,在服务器端同样也是用 ContentType 中字符集进行解码。所以通过 POST 表单提交的参数一般不会出现问题,而且这个字符集编码是我们自己设置的,可以通过 request.setCharacterEncoding(charset) 来设置。
另外针对 multipart/form-data 类型的参数,也就是上传的文件编码同样也是使用 ContentType 定义的字符集编码,值得注意的地方是上传文件是用字节流的方式传输到服务器的本地临时目录,这个过程并没有涉及到字符编码,而真正编码是在将文件内容添加到 parameters 中,如果用这个编码不能编码时将会用默认编码 ISO-8859-1 来编码。
HTTP BODY 的编解码
当用户请求的资源已经成功获取后,这些内容将通过 Response 返回给客户端浏览器,这个过程先要经过编码再到浏览器进行解码。这个过程的编解码字符集可以通过 response.setCharacterEncoding 来设置,它将会覆盖 request.getCharacterEncoding 的值,并且通过 Header 的 Content-Type 返回客户端,浏览器接受到返回的 socket 流时将通过 Content-Type 的 charset 来解码,如果返回的 HTTP Header 中 Content-Type 没有设置 charset,那么浏览器将根据 Html 的 <meta HTTP-equiv="Content-Type" content="text/html; charset=GBK" /> 中的 charset 来解码。如果也没有定义的话,那么浏览器将使用默认的编码来解码。
所以服务器端和客户端必须协调好编码格式,通常将其编码设置为utf-8,同时浏览器也最好将编码格式改为utf-8。
这里也提出I/O乱码问题的解决思路就是:先找出存在读和写操作的地方,然后逐一进行编码格式的排查。
为了支持国际化以及防止乱码建议使用utf-8编码。如果你不指定解析时的编码格式,那么编码器和解码器会使用系统默认的编码格式,这就相当于将你的数据与你的系统强行绑定,不利于跨平台操作。