字符从浏览器到数据库过程的转换

尽管开发些web程序,但也没有系统地理解字符到底如何在浏览器、web服务器、数据库服务器三者之间如何来回转换,也经常看到初学者或者有些“高手”亦有类似困惑,偶然间,看到SUN网站上这篇文章,系统通俗地讲述了该问题,看后受益匪浅。翻译过来,与大家分享。

翻译自:[url]http://java.sun.com/developer/technicalArticles/Intl/HTTPCharset/[/url]

字符在3W中的最终存储目的地路线中,穿过了不同层次的编程接口,并且可以跨越软件和硬件的边界。本文提供了准确的从浏览器到数据库传输字符数据...,然后再返回过程中的一些有用提示和最佳做法。
内容:
[list]
[*]浏览器显示和表单提交
[*] Web设置
[*] 数据库设置
[*] 总结
[/list]
为国际化,许多操作系统,应用开发语言,以及平台都走过了漫长的道路。有些事情很容易,比如在Swing 文本框中输入你的姓名。通过键盘,输入方法,和主机软件协作来创建正确的字符,无论你的名字是John,José, or 。不幸的是,虽然在浏览器中输入非ASCII文本与你在Swing组件中一样容易,但通过web精确传送它却是复杂的。因为没有工业标准来控制应用程序如何在GET或者POST方法中对数据进行编码,在多个编程接口传输过程中数据可能被转换成无意义的乱码。而且,web服务器和数据库管理者通常对字符编码都了解甚少,这样就影响了文本高保真的从浏览器到数据库的传输。

[b]浏览器显示和表单提交(Browser Display and Form Submission)[/b]

只要HTML页面为浏览器解析字符提供了足够的选择和使用合适的字体及编码的提示,现代浏览器就能够正确显示大多数文本。下面的图显示了当你没有为浏览器提供任何字符编码信息时,浏览器的一个可能的显示。这种情况下,通过HTML页面接受的字符虽然没有数据丢失但导致了错误的解析。

[align=center][img]http://dl.iteye.com/upload/attachment/474611/074eb3a0-fef9-3866-a3e0-0742ef32454f.png[/img]

图1 没有为浏览器提供字符编码信息[/align]
在下图中,浏览器(火狐1.07)错误地按照ISO 8859-1编码解析文件内容。尽管大多数浏览器都允许用户针对某个特定文件修改或者覆盖对应的编码设置,但是这种期望对普通用户来讲是不合理的。

[align=center][img]http://dl.iteye.com/upload/attachment/474613/91902f9a-3dd3-318f-902a-797bf1bcc308.png[/img]

图2 不正确的字符编码信息[/align]
这个文本实际上是UTF-8(一种Unicode编码)编码的文本。当在HTML中利用<mata>标签设置该信息后,浏览器就能正确显示日语中的“Hello World!”。

[align=center][img]http://dl.iteye.com/upload/attachment/474615/0f183345-6343-373a-b5a1-8bb5fa308297.png[/img]

图3 给浏览器正确的字符编码信息[/align]

能正确显示的HTML文件如下所示。注意其中的<meta>标签包含一个content属性,它告诉浏览器该文件有个使用charset=UTF-8的text/html类型。content属性中Charset关键字表示HTML文件的字符编码。另外,要尽可能使用language标签,这样浏览器就能够发现和利用合适的具体语言字形。比如,中文和日语都使用许多许多相同的字符。因为这些语言中和字形和其它语言相比有很大不同,所以,language标签可以帮助浏览器发现最合适的字体。
[align=center]
[img]http://dl.iteye.com/upload/attachment/474617/48e537d6-b953-3c4e-a052-574b65f808b7.png[/img]

Japanese Language Tag[/align]

如果你用JSP技术生成页面,仍然必须指示生成的HTML页面的字符编码。你可以使用JSP的page指令或HMTL的meta标签。page指令应是你的JSP文件的第一个元素且应该包含一个带charset设置的contenType属性。JSP编译器使用pageEncoding属性来编译JSP页面本身的。
<%@page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%>


现在你的页面能够正确编码了,并且它们可以与浏览器交流信息了。非常棒,但是仍然没有从浏览器传过来任何东西。正如已经展示的那样,浏览器从保存在HTTP header中charset信息或者HTML中<meta>标签知道了页面的编码信息。当通过GET或者POST向服务器返回信息时,它们使用相同的信息对form表单中的数据进行编码。如果在HTML的form表单标签中有一个accpet-charset属性,浏览器将使用该设置。也就是说,浏览器使用页面或者form表单本身编码方式对form表单中的数据进行编码。

下面的JSP展示了一个页面,它指示了页面内容使用ISO-8859-1编码,一种在西欧和美洲国家用来处理语言和脚本的常用字符编码。这个页面生成一个询问用户name的HTML表单。在提交时,同一个页面处理GET命令并且使用NAME属性打印出欢迎用户的信息。

[align=center][img]http://dl.iteye.com/upload/attachment/474619/68cc8bff-db70-36c5-99d4-692a2e3fb171.png[/img][/align]

当输入John后点击提交按钮,浏览器创建并且使用下面的URL向服务器发送信息。
http://localhost/sayhello.jsp?NAME=John
到目前为止一切正常,现在,输入一个José后点击提交按钮。由此产生的URL如下所示:
http://localhost/charconv/sayhello.jsp?NAME=Jos%E9
尽管产生的NAME参数看见有些不同,但是它是正确的。%E9字符串是一个URL-Encoding字符:即字符在ISO-8859-1字符集中代码点(codepoint)所对应的十六进制整数值。服务器期望GET和POST数据使用8859-1编码,因此它能够正确对该URL编码实体进行解码。

简单讲,URL编码是一个URL方面的web标准。它要求把所有非ASCII字符和特殊的ASCII字符按照%HH形式编码为十六进制字符串。不幸的是,标准中并没有规定对数据编码时使用何种字符集。

当表单中输入的字符没有包含在页面中的字符集时将会发生什么呢?想象一个日本,韩国或者中国的用户在相同的页面中输入他们的NAME。这些字符很可能没有出现在ISO-8859-1编码中。浏览器使用8859-1对它们编码将会出现一个重大问题,而且每个浏览器使用不同的方式来处理这个问题。
比如日本名字田中,FF浏览器产生下面的GET URL:
http://localhost/charconv/sayhello.jsp?NAME=%26%2330000%3B%26%2320013%3B


这个URL第一眼看上去有些奇怪,但是很快你能解密它的意思。首先,FF浏览器知道它不能用8859-1字符集来表示田中....它甚至都没有尝试。相反,它使用数字字符引用(NCR)来对字符编码。其它浏览器可能使用不同的方式,选择性来生成问号或者甚至完全阻止他们的输入。然而在这个情况下,每个字符都被编码成&#D;形式,这里D表示了字符在Unicode字符集中的十进制代码点值。其中田字符的值为30000,中的值为20013。根据创建的数字字符引用(NCR),我们可以产生出下面的参数字符串:
NAME=田中


现在URL-Encoding出现了,把特殊的&,#和; 字符转换成为了它们%HH相等的值。
NAME=%26%2330000%3B%26%2320013%3B


如果你的JSP页面检索这个参数并简单地给浏览器返回该值,将不会丢失数据。尽管页面的字符编码仍然是8859-1,浏览器也理解字符引用的数字,并且它将尽自己最大努力显示日文。然而,这里仍然有个潜在的问题。尽管处理GET和POST命令的服务器端的代码理解URL-Encoding字符串,它也很难能理解数字字符引用NCR。大多数读取NAME参数的servlet代码根据田中来解码字符串,除了字面意思并不知道这代表任何东西。换句话说,真实的田中字符将不会被发现除非你已经有意识的计划处理这种情况。因此发送数据时,不用实际的代码点或编码值而用数字字符引用(NCR)编码将会造成潜在的数据丢失。

为了解决这个问题,始终选择一个HTML网页编码,它可以处理你打算处理的所有字符。如果你期望有多语言用户并希望处理多种脚本,使用一种能表示所有脚本的字符编码。UTF-8是一种统一字符编码,并且它能正确表示世界上所有字符。若在页面中使用UTF-8而不是8859-1,浏览器将会产生一个不同的URL:
http://localhost/charconv/sayhello.jsp?NAME=%E7%94%B0%E4%B8%AD


这是一个正确的NAME参数的编码,它使用了UTF-8字符集。UTF-8使用3个字节对田中编码。因为每个字节有个超出ASCII范围的值,浏览器对其进行URL编码,这样每个字节就产生了%HH形式。期望UTF-8形式数据的服务器将能够正确处理该参数。

[b]Web服务器处理过程[/b]

尽管当前的浏览器能根据页面或者form表单中提示把form中数据使用相同的编码返回给服务器,但大多数服务器仍认为浏览器不知道如何选择字符。它们典型假设浏览器编码是ISO 8859-1。即使一个应用经历了用UTF-8 来对GET参数进行URL编码的麻烦,服务器仍将假设使用8859-1。这就导致了字符通过多个web应用层时出现了乱码。

当你使用UTF-8字符集来修改以前的JSP代码时,新的代码如下所示:

[align=center][img]http://dl.iteye.com/upload/attachment/474621/e33c95e7-ebc9-3ae7-b295-6b296b065712.png[/img][/align]

现在输入“José”并点击提交按钮,GET的URL如下:
http://localhost/sayhello.jsp?NAME=Jos%C3%A9
%C3%A9 是José 使用UTF-8对其进行URL编码的结果。没问题,浏览器根据contenType中UTF-8提示对字符进行编码。浏览器显示下面的文本。

[align=center][img]http://dl.iteye.com/upload/attachment/474623/52057627-1fac-38e3-8a77-af946201f9a2.bmp[/img]

Figure 4. UTF-8 Character Encoding[/align]

注意其中的展现的文本Hello, José!,这是不对的。发生了什么?尽管浏览器正确地发送了GET数据,但web服务器从URL中读取NAME参数Jos%C3%A9时,不正确地假设字符编码是8859-1。从这个角度讲,%C3代表了 ISO-8859-1中0xC3 (Ã) 字符的编码,%A9 代表了ISO-8859-1中 0xA9 (©) 。虽然contentType已经明确写在了JSP标签中了!

现在还没有公布的标准规定(浏览器和服务器)如何沟通关于GET和POST数据的字符集选择。如何解决呢?一些服务器尝试着解决这些问题。Tomcat中通过在server.xml文件里面的connector(连接器)中设置URIEncoding="UTF-8"来告诉web服务器如何选择字符编码,这样Tomcat就能正确读取URL中的GET参数了。它然后就发出了期望的"Hello, José!" 或者田中。在Sun Java System Application Server ,你可以在sun-web.xml文件中包含<parameter-encoding default-charset="UTF-8"/>。URL正确解释如下图所示:


[align=center][img]http://dl.iteye.com/upload/attachment/474625/f0814541-c0e4-3c42-ac0f-90b9cea3dad3.png[/img]

Figure 5. URL Interpretation
[/align]

假如你想POST非ASCII数据将会如何呢?一切都正常,因为你设置了URIEncoding标签,对吗?错了,Tomcat不使用URIEncoding标签来处理POST过来的form表单数据。那么,它将使用什么编码?ISO-8859-1.

所以现在,你又回到了你的起点,应用仅简单地返回Mr. ç°ä,而不是Mr. 田中。而SUN应用服务器在设置了parameter-encoding标签后,能够正确处理GET和POST数据,所以这些代码在该系统上能工作很好。

不幸的是,这些解决方法都完全依赖于服务器,并且你不能总控制你的应用在哪里部署。幸运的是,存在着与服务器独立的方法。

可能,最基本的服务器独立的方案是设置一个context参数来指示应用中的所有表单如何选择字符编码。然后你的应用就能在读取任何请求参数之前,通过读取context参数并能设置请求字符编码。你能够在servlet或者JSP中设置请求编码。
在WEB-INF/web.xml中设置context参数:
<context-param>
<param-name>PARAMETER_ENCODING</param-name>
<param-value>UTF-8</param-value>
</context-param>

在读取前面的JSP文件之前加入下面的代码:
String paramEncoding = application.getInitParameter("PARAMETER_ENCODING");
request.setCharacterEncoding(paramEncoding);
在一个servlet中,你可以在servlet初始化时读取参数并在处理参数之前设置编码。
public class MsgHandler extends HttpServlet {   
private String encoding;
public void init() throws ServletException {
ServletConfig config = getServletConfig();
encoding = config.getInitParameter("PARAMETER_ENCODING");
}
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (encoding != null) {
request.setCharacterEncoding(encoding);
}
...
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}

现在,无论你GET或POST数据,你的JSP或servlet都能正确读取参数,因为你已经明确告诉web服务器处理请求时使用哪种字符编码。

[b]数据库设置Database Settings[/b]
假如你的数据存活时间很长,经过了浏览器编码和web服务器处理,那么数据库就是字符数据转换的最后一个障碍了。为了安全地把数据存放到数据库并不受影响地检索它,你必须配置数据库系统使用一种被浏览器和业务逻辑层都支持的且能表示所有字符集来存储数据。因为UTF-8已使用至今,所以在数据库中选择它是有意义的。否则,当你存储了数据库的编码不支持文本数据时,数据将会造成不可逆的丢失。
下面的图展示了这样的数据丢失(当数据库字符编码不是浏览器和中间层字符集的超集)。在这种情况下,浏览器和中间层使用UTF-8,但是数据库配置为ASCII。

[align=center][img]http://dl.iteye.com/upload/attachment/474628/21515dd5-b41d-39b3-9a36-75eb8c864c1f.png[/img]

Figure 6. Database Configured for ASCII[/align]
一旦你把数据库字符集修改为UTF-8,不管怎么样,数据从浏览器迁移到数据库,并再次回到都完整的保真度。

[align=center][img]http://dl.iteye.com/upload/attachment/474630/40dfa61f-5bb9-32d2-a797-1607f9688129.png[/img]

Figure 7. Database Configured for UTF-8[/align]

现代多数数据库系统都支持Unicode。两个流行的关系数据库产品,Oracle和MySQL,在他们最新的版本中都完全支持UTF-8和UTF-16编码。如果你有机会创建你应用的数据库shemale,认真考虑下把UTF-8作为你数据库的字符编码。如果你的数据使用了一个不同、很有限的字符编码,且你已经经历了前面所描述的数据丢失,你应该考虑迁到UTF-8编码了。

[b]总结 Summary[/b]
从浏览器到数据库的旅程有几个潜在的数据丢失点。文本数据通常经过了三次甚至更多的字符编码转换,每次转换都能引起不可逆的数据丢失。为了避免这个问题,遵循这些规则:
[list]
[*]总是在你的HTML或JSP页面中通过使用HTML<mata>标签或JSP@page提供字符编码信息。这个信息告诉浏览器如何解析页面的文本。
[*]使用一种能够处理所有你的多语言和写脚本需要的HTML字符编码。UTF-8一个适应多种情景的好选择,因为它能表示一个范围很广的脚本。
[*]告诉web服务器当处理请求参数时选择什么字符编码。可通过使用servlet初始化参数获知context参数来实现它。
[*]为你的数据库选择一种你应用中任何编码的超集,比如UTF-8或者UTF-16。
[/list]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值