您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
关于编码问题的深度解析
 
作者:SundaySunshine  来源:blog.csdn.net  发布于:2017-1-10
   次浏览      
 

编码问题的产生其实都是I/O操作导致的问题,也就是说所有I/O存在的地方就有可能会出现编码问题。

所以要想深度解析编码问题,我们就必须回到源码的实现机制,找到I/O操作中可能导致乱码的操作(行为)。

我们知道I/O有四大家族InputStream, OutputStream,Writer,Reader前两个是基于字节的操作,后两个是基于字符的操作。由于我们平时是使用字符的方式进行记录信息,但由于计算机只认识0和1,所以I/O作为人机交互的手段,它的底层一定是以字节的方式进行传送的,编码问题其实就是发生在字符和字节之间相互转换的时候。

看I/O(以Java 的I/O为例)的底层图:

读操作:

写操作:

也就是说如果我们用错了字典,那么编码或解码出来的数据就会可能出现问题。为什么说可能,因为有可能不同字典之间有相同的东西,或者存在兼容(但是这种情况出现的可能性很小)。

好了。说到这里你也许要问了,什么是在计算机I/O里面什么字典?

好吧,说直白点这里讲的字典就是编码方式如GB2312,GBK,UTF-8,UTF-16,就好比我们说的中文和中文字典一样,编码器和解码器就是一个不认识字的文盲,我们写了一句“你很帅”但它不认识,所以就通过查中文字典查看我们这个字到底什么意思,乱码的出现就是在它们拿错了字典(拿了本英文字典去查)结果肯定查到些乱七八糟的东西。

下面具体拿出这些编码格式来说一说:

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 开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节

嗯嗯,说多了,来个实例:

import java.io.UnsupportedEncodingException;  

/**
* I/O乱码模拟
*
* @author GG
*
*/
public class first {

public static void main(String[] args) throws UnsupportedEncodingException {
String message = "你很帅";

toHex("utf-8", message.getBytes("utf-8"));
toHex("gb2312", message.getBytes("gb2312"));
toHex("gbk", message.getBytes("gbk"));
toHex("ISO-8859-1", message.getBytes("ISO-8859-1"));

}

// 将字节流转换为16进制
public static void toHex(String charsetName, byte[] b) {
System.out.print(charsetName + "编码:");
for (byte c : b) {
System.out.print(Integer.toHexString(c & 0xFF) + " ");
}
System.out.println();
}
}

运行结果:

经过不同的编码转换为不同16进制数存在于计算机,解码时一旦用错了编码方式就会可能导致信息变成其他的意思,毕竟计算机只关心是0还是1,它根本就不知道自己有没有拿错字典,只有用户知道信息是否乱码。乱码异常发生的情况有无数种,但它绝对是发生在编码或解码阶段,也就是说它其实是I/O引发的问题。随着大数据和云时代的降临,I/O也面临很多瓶颈问题,如何正确并快速的编码解码是解决I/O瓶颈中简单而又极具挑战的难题,在这里由衷感谢那些

奋斗在科技领域的工作者们,是他(她)们开启了这个崭新的时代。

嗯嗯,又多扯了。既然现在是大数据时代,那么再聊聊网络I/O编码异常吧。

网络端的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编码。如果你不指定解析时的编码格式,那么编码器和解码器会使用系统默认的编码格式,这就相当于将你的数据与你的系统强行绑定,不利于跨平台操作。

 

   
次浏览       
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

WEB应用程序UI模版代码编写
C# 编码规范和编程好习惯
什么是防御性编程
善于防守-健壮代码的防御性
Visual C++编程命名规则
JavaScript程序编码规范
更多...   


设计模式原理与应用
从需求过渡到设计
软件设计原理与实践
如何编写高质量代码
单元测试、重构及持续集成
软件开发过程指南


某全球知名通信公司 代码整洁
横河电机 如何编写高质量代码
某知名金融软件服务商 代码评审
东软集团 代码重构
某金融软件服务商 技术文档
中达电通 设计模式原理与实践
法国电信 技术文档编写与管理
更多...