【简记】Java Web 内幕——编码问题


为什么要编码

总的来说,编码的原因可以总结为:

  • 计算机中存储信息的最小单元是一个字节即 8 个 bit,所以能表示的字符范围是 0~255 个
  • 人类要表示的符号太多,无法用一个字节来完全表示

要解决这个矛盾必须需要一个新的数据结构 char,从 char 到 byte 必须编码(因为计算机中最小的标准存储单位是byte)


如何编码

ASCII 码

  • 学过计算机的人都知道 ASCII 码,总共有 128 个,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126
    是打印字符,可以通过键盘输入并且能够显示出来。

ISO-8859-1(扩展ASCII编码)

  • 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(扩展GB2312)

  • 全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。

Unicode编码集

  • ISO 试图想创建一个全新的超语言字典,世界上所有的语言都可以通过这本字典来相互翻译。可想而知这个字典是多么的复杂,关于 Unicode的详细规范可以参考相应文档。Unicode 是 Java 和 XML 的基础,下面详细介绍 Unicode 在计算机中的存储形式。

UTF-16(固定两字节)

  • UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16作为内存的字符存储格式的一个很重要的原因。

UTF-8(并不是指1字节,采用的是变长技术,长度范围1-6字节)

  • UTF-16统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成。

UTF-8 有以下编码规则:

  • 如果一个字节,最高位(第 8 位)为 0,表示这是一个 ASCII 字符(00 - 7F)。可见,所有 ASCII 编码已经是 UTF-8了。 如果一个字节,以 11 开头,连续的 1 的个数暗示这个字符的字节数,例如:110xxxxx 代表它是双字节 UTF-8字符的首字节。 如果一个字节,以 10 开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节

在I/O操作中存在的编码

下图是 Java 中处理 I/O 问题的接口:
这里写图片描述

Reader 类是 Java 的 I/O 中读字符的父类,而 InputStream 类是读字节的父类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中处理读取字节到字符的转换,而具体字节到字符的解码实现它由 StreamDecoder 去实现,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。值得注意的是如果你没有指定 Charset,将使用本地环境中的默认字符集,例如在中文环境中将使用 GBK 编码。

写的情况也是类似,字符的父类是 Writer,字节的父类是 OutputStream,通过 OutputStreamWriter 转换字符到字节。

    public static void main(String[] args) throws Exception {
        File file = new File("e:/stream.txt");
        String charset = "UTF-8";
        // 写字符转换成字节流
        FileOutputStream outputStream = new FileOutputStream(file); //FileOutputStream继承自OutputStream,用于向文件中写入数据;这一步生成具体文件
        OutputStreamWriter writer = new OutputStreamWriter(
                outputStream, charset); // 变成字节流,并指定了编码方式
        try {
            writer.write("这是要保存的中文字符");
        } finally {
            writer.close();
        }
        // 读取字节转换成字符
        FileInputStream inputStream = new FileInputStream(file);
        InputStreamReader reader = new InputStreamReader(inputStream, charset); // 字节转字符,并确定解码方式
        StringBuffer buffer = new StringBuffer();
        char[] buf = new char[64];
        int count = 0;
        try {
            while ((count = reader.read(buf)) != -1) {
                buffer.append(buffer, 0, count);
            }
        } finally {
            reader.close();
        }
    }
}

强烈的不建议使用操作系统的默认编码,因为这样,你的应用程序的编码格式就和运行环境绑定起来了,在跨环境下很可能出现乱码问题。


内存操作中的编码
String s = "这是一段中文字符串"; 
byte[] b = s.getBytes("UTF-8"); 
String n = new String(b,"UTF-8"); // UTF-8是解码方式

这两个类已经被 Charset 类取代,Charset 提供 encode 与 decode 分别对应 char[] 到 byte[] 的编码和 byte[] 到 char[] 的解码。如下代码所示:

Charset charset = Charset.forName("UTF-8"); 
ByteBuffer byteBuffer = charset.encode(string); 
CharBuffer charBuffer = charset.decode(byteBuffer); 

String. getBytes(charsetName) 编码过程的时序图
这里写图片描述

根据 charsetName 找到 Charset 类,然后根据这个字符集编码生成 CharsetEncoder,这个类是所有字符编码的父类,针对不同的字符编码集在其子类中定义了如何实现编码,有了 CharsetEncoder 对象后就可以调用 encode 方法去实现编码了。


几种编码的案例与对比

http://www.cnblogs.com/gdayq/p/5817367.html

重点摘录:

  • UTF-8 对单字节范围内字符仍然用一个字节表示,对汉字采用三个字节表示。
  • UTF-8 编码与 GBK 和 GB2312 不同,不用查码表,所以在编码效率上 UTF-8 的效率会更好,所以在存储中文字符时 UTF-8 编码比较理想。
  • GB2312 与 GBK 编码规则类似,但是 GBK 范围更大
  • 相对来说 UTF-16 编码效率最高,字符到字节相互转换更简单,进行字符串操作也更好。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间快速切换,如 Java 的内存编码就是采用 UTF-16 编码。
  • UTF-16不适合在网络之间传输,因为网络传输容易损坏字节流,一旦字节流损坏将很难恢复
  • UTF-8 更适合网络传输,对 ASCII 字符采用单字节存储,另外单个字符损坏也不会影响后面其它字符,在编码效率上介于 GBK 和 UTF-16 之间,所以 UTF-8 在编码效率上和编码安全性上做了平衡,是理想的中文编码方式。

Java Web涉及到的编码

这里写图片描述


URL的编译码

这里写图片描述
(上面的URL和URI是针对servlet进行描述的)

注意这里是在浏览器里直接输入 URL 所以是通过 Get 方法请求的,它本身是通过HTTP的header传到服务端的。如果是 POST 方法请求的话,QueryString 将通过表单方式提交到服务器端。

自测发现pageEncoding项影响了post(Form Data,但不是传到服务端的编码方式)和get(URI中)提交的参数编码

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

GET 方式 HTTP 请求的 QueryString 与 POST 方式 HTTP 请求的表单参数都是作为 Parameters 保存,都是通过 request.getParameter 获取参数值。对它们的解码是在 request.getParameter 方法第一次被调用时进行的。
(所以之前在filter中修改编码,用代理模式的时候就是在getParameter时触发)
request.getParameter 方法被调用时将会调用 org.apache.catalina.connector.Request 的 parseParameters 方法。这个方法将会对 GET 和 POST 方式传递的参数进行解码,但是它们的解码字符集有可能不一样。

QueryString 的解码字符集要么是 Header 中 ContentType 中定义的 Charset 要么就是默认的 ISO-8859-1,要使用 ContentType 中定义的编码就要设置 connector 的 <Connector URIEncoding=”UTF-8” useBodyEncodingForURI=”true”/> 中的 useBodyEncodingForURI 设置为 true。这个配置项的名字有点让人产生混淆,它并不是对整个 URI 都采用 BodyEncoding 进行解码而仅仅是对 QueryString 使用 BodyEncoding 解码,这一点还要特别注意。


HTTP Header 的编解码

对 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 表单参数传递方式与 QueryString 不同,它是通过 HTTP 的 BODY 传递到服务端的。当我们在页面上点击 submit 按钮时浏览器首先将根据 ContentType 的 Charset 编码格式对表单填的参数进行编码然后提交到服务器端,在服务器端同样也是用 ContentType 中字符集进行解码。所以通过 POST 表单提交的参数一般不会出现问题,而且这个字符集编码是我们自己设置的,可以通过 request.setCharacterEncoding(charset) 来设置。

Filter中:

request.setCharacterEncoding("UTF-8");                // POST提交有效

HTTP BODY 的编解码

当用户请求的资源已经成功获取后,这些内容将通过 Response 返回给客户端浏览器,这个过程先要经过编码再到浏览器进行解码。这个过程的编解码字符集可以通过 response.setCharacterEncoding(response.setContentType("text/html;charset=UTF-8");) 来设置,它将会覆盖 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 来解码。如果也没有定义的话,那么浏览器将使用默认的编码来解码。


其它需要编码的地方

xml 文件可以通过设置头来制定编码

<?xml version="1.0" encoding="UTF-8"?>  

Velocity 模版设置编码services.VelocityService.input.encoding=UTF-8

JSP 设置编码格式:
<%@page contentType="text/html; charset=UTF-8"%>

数据库都是通过客户端 JDBC 驱动来完成,用 JDBC 来存取数据要和数据的内置编码保持一致,可以通过设置 JDBC URL 来制定如

MySQL:url="jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK"

常见问题分析

中文变看不懂的字符:汉字用gbk编码,iso-8859-1解码

一个汉字变成一个问号:汉字用iso-8859-1编码,iso-8859-1解码


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值