附录国际化问题

 

目前很多网站都需要同时提供多种语言,以供不同地区的用户访问。kangaroo-egg从一开始就考虑到这样的国际化问题,为此提供了适用于国际化的方法。在介绍如何编写国际化的网站前有必要了解一下编译时的字符集。

 

 

F2.1 编译和执行时的字符集

附录1详细介绍了动态文件编译原理,本节则补充介绍一下编译时字符集。如图F1-1-1所示编译时动态文件首先要转换成标准的java程序文件,那么在转换过程中又是采用何种字符集呢?其实转换过程中采用是由dataEnc(参见2.1节)所指定的字符集。比如dataEnc所指定的字符集为GBK,则DQM容器首先按GBK字符集读取动态文件,按GBK字符集转换完后再按GBK字符集保存转化完后的标准java程序文件。但是如果此时动态文件中含有超出GBK字符则在读取动态文件时就会将那些超出的字符转换成问号,所以最终编译后执行的结果也是那些超出GBK字符集的字符变成了问号。

我们来做个实验,首先将webconfig.xmlsystemSet元素中的dataEnc值改为US-ASCII,这样在编译过程中只能识别英文字符,而所有的中文字符将被转换成问号。

如源程序如下的动态文件:

1

2

3

4

<pre>

<%="你好"%>

<%="Hello"%>

</pre>

 

dataEnc值为US-ASCII的情况下编译后执行结果为:

????

Hello

可以看到上面的执行结果中原本应该显示“你好”字符,可是显示了四个问号。

而在dataEnc值为GBK的情况下编译后执行结果为:

你好

Hello

 

到这里又有一个疑问,如果编译的时候dataEnc指定的字符集为GBK,而执行的时候再将dataEnc指定的字符集改称US-ASCII,那么执行结果会是怎么样的呢?其实也是会变成问号的,为什么呢,4.1节中介绍过print(String content)方法也是按dataEnc指定的字符集输出的,所以即使编译的时候是GBK字符集,然而在执行时改为了US-ASCII,则在执行时其实就是用out.print("你好", "US-ASCII")这个方法输出,中文字符强行被转换成英文字符自然变成了问号。

所以如果动态文件源代码如下:

1

2

3

4

<pre>

<%out.print("你好", "GBK");%>

<%="Hello"%>

</pre>

 

那么只要在编译的时候dataEnc指定的字符集为GBK,即使在执行时dataEnc指定的字符集为US-ASCII(或者其他任何字符集)也能正确显示“你好”。

可是在运行需要多国语言支持的网站时也不可能将dataEnc的值改来改去,也只能指定一种字符集,于是我们假定我们将dataEnc指定在US-ASCII上,那么如何才能编写出支持中文的动态文件呢?为了能使在US-ASCII字符集下也能正确编译出中文字符我们可以将中文字符变成unicode表示格式,当然不只中文字符,任何非ASCII字符都应改为它们等价的unicode表示格式。比如“你”的unicode表示格式为“\u4f60”,而“好”的unicode表示格式为“\u597d”,于是动态文件源程序就改成了如下:

1

2

3

4

<pre>

<%out.print("\u4f60\u597d", "GBK");%>

<%="Hello"%>

</pre>

 

这样不管dataEnc指定的是何种字符集,编译和执行都可以显示出正确的“你好”。不过我怎么知道哪些字符应该转换成uniocde格式呢,毕竟有些字符是不需要转换的,同时我该如何转换呢?JDK本身提供了一个很好的工具native2ascii,利用这个工具可以很容易转换。它的使用方法为:

native2ascii a.dqm b.dqm

a.dqm是指包含普通字符的文件,b.dqm是指将a.dqm中可以转换的字符转换成uniocde格式后保存的文件。利用这个工具就可以轻松将字符转换成unicode格式了。

  你也可以用-reverse选项来进行逆向转换:

native2ascii –reverse b.dqm a.dqm

并且可以用-encoding选项来设定另一种编码:

native2ascii -encoding BIG5 a.dqm b.dqm

 

 

F2.2 读取客户端请求时的字符集

当客户端向服务器发出请求时,服务器必须首先读取客户端请求数据,之后才能给与回应,于是问题就来了,客户端种类很多而且还分地区,比如中国大陆地区的某些浏览器发送请求的数据中如含有中文字符,则这些中文字符是按GBK字符集编码后发送的,那么服务器必须以GBK字符集读取才能准确获得请求中的中文字符,那么台湾地区呢?含有繁体中文字符的请求会按BIG-5字符集编码后发送,那么作为服务器必须以BIG-5字符集读取才能准确获得请求中的繁体中文字符。那么kangaroo-egg服务器在读取客户请求时是使用哪种字符集呢?是使用webconfig.xmlsystemSet元素中的urlEnc值指定的字符集来读取的(urlEnc参见2.1节),而urlEnc只能设定一种字符集,所以不可能同时能够读取多种字符集。那么岂不是无法支持国际化了?其实HTTP协议也考虑到了这个问题,为此建议客户端在发出请求的HTTP头中如含有非ASCII字符,则将这些字符按UTF-8字符集进行URL编码(URL编码请参见2.1节的urlEnc项)。可是HTTP协议只是建议这样,各种客户端并不需要完全遵循,比如中文版的IE默认就会将非ASCII字符按UTF-8字符集进行URL编码后发送,但中文版的FireFox默认会将非ASCII字符按GBK字符集进行URL编码后发送,而有些客户端比如下载工具更本就不进行URL编码,直接将非ASCII字符以某种字符集编码后发送。那么我们该怎么办呢?幸好客户端一般不会发送含有非ASCII字符的请求信息,那么我们就来看看什么时候会发送这些非ASCII字符呢?通常有以下几种情况:

 

1.当请求一个非ASCII字符的资源时。比如用户在服务器上有“file你好.dqm”这个文件,那么当访问这个文件时客户端就会含有中文字符,如果此时你的urlEnc值为GBK字符集那么是没有问题的,可是服务器上如果还有BIG5的繁体中文的文件呢?这个时候该怎么办,你不可能为urlEnc同时设置多个字符集。为此请在国际化的环境中使用只包含ASCII字符的文件名。

 

2.服务器端给客户端设置的信息中含有非ASCII字符,并且以后还需要从客户端读取的。比如cookie就是一个例子(cookie请参见第7章),当向客户端写入一个cookie,那么以后客户端就会将这个cookie内容包含在其请求中以便服务器端再次读取。如果向客户端写入非ASCII字符,那么服务器端读取客户端返回的这些非ASCII字符就有可能遇到问题,因为国际化有可能需要同时向客户端写入中文、韩文等,但是服务器端只能以一种字符集读取客户端请求中的字符。那么我们该如何进行类似于cookie的国际化读写呢?还记得2.1节的urlEnc项中提到的URL编码吗?URL编码可以很容易将非ASCII字符编码成只含有ASCII字符的形式,这样不管服务器端设置为哪种字符集读取请求都没问题,因为所有字符集都能够读取ASCII字符。我们来举个例子,首先将urlEnc设置为Shift_JIS,即日文字符集,至于dataEnc你可任意设置为一种字符集,接下来所要做的就是写一个含有中文字符的cookie,之后再读取这个cookie的值。因为urlEnc已经设置为日文字符集,所以为了要读取中文字符我们就必须先将中文字符先url编码,再url解码读取。

首先看写入cookie的动态文件write_nonascii_cookie.dqm,源程序如下:

1

2

3

 

4

5

6

7

<%@ page buffer="true"%>

<%

DCookie ck=new DCookie("ourset", java.net.URLEncoder.encode("\u4f60\u597d", "GBK"));

ck.setMaxAge(30*60); // 设置Cookie的存活时间为30分钟

response.addCookie(ck); // 写入客户端硬盘

out.print("Write cookie ok!");

%>

 

从上面第3行可以看到jdk本身就提供了url编码的静态方法java.net.URLEncoder.encode,此方法有2个参数,第一个是所要进行编码的字符串,这里我们是对\u4f60\u597d这个字符串进行url编码(\u4f60\u597d是“你好”的unicode表示格式)。第2个参数是对第一个参数采用哪种字符集进行url编码,我们看到这里是用GBK字符集。

当写入cookie后我们所要做的就是再正常读取此cookieread_nonascii_cookie.dqm就是用来读取cookie的动态文件,源程序如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

<%String charset = "GBK";%>

<html>

<head><title>

read non ascii cookie

</title>

<meta http-equiv=Content-Type content="text/html; charset=<%=charset%>">

</head>

 

<%

String value =request.getCookieValue("ourset");

 

if (value == null) {

  out.println("cookie not found!");

}

else {

  out.println("cookie value: " + java.net.URLDecoder.decode(value, charset), charset);

}

%></html>

 

从上面第16行可以看到jdk也提供了url解码的静态方法java.net.URLDecoder.decode,此方法也有2个参数,第一个是所要进行解码的字符串,这里我们是对读取的cookie值进行url解码。第2个参数是对第一个参数采用哪种字符集进行url解码,这里当然还是用GBK字符集,因为url编码的时候是采用GBK字符集的。

当读取cookie程序执行后你就可以看到结果是:

cookie value: 你好

 

从上面的结果看到中文字符被正确的读取,而这是在urlEnc设置为Shift_JIS字符集的情况下。你可以试着将write_nonascii_cookie.dqm中的url编码和解码语句去掉,运行的话就会发现结果是乱码。

 

注意:在初始化cookie时需要设置2个参数,cookie的名字和其对应的值,从上面例子中看到我们只对cookie的值进行了url编码,因为值中含有中文字符。那么我们是否也能将cookie的名字设置为含有中文等非ASCII的字符呢?如果这样做的话当根据名字来读取cookie的值时你会发现很多问题,所以我们推荐对于cookie的名字只包含ASCII字符。

 

 

3.请求资源时附带的数据含有非ASCII字符。我们知道url可以附带数据,比如这个http://localhost:8080/getName.dqm?name=dunne,其中问号后面的“name=dunne”就是附带的数据,这种传输数据的方式通常称之为GET方式,可以通过6.1节介绍的方法来获取。通过这种方式上传的数据是包含在HTTP请求头中的,而前面已经介绍过服务器读取HTTP头所在用的字符集是由urlEnc指定的,于是老问题又来了,urlEnc只能指定一种字符集,而GET方式在国际化时有可能需要同时传输中文、韩文等非ASCII字符,那该怎么办,哈哈聪明的你一定想到了用前面介绍的url编码,再来个例子,就像前面一样首先将urlEnc设置为Shift_JISdataEnc你任意,于是我们所要做的就是在url中附带中文字符数据,看看能否被正确读取。

首先看写一个附带中文数据的链接,write_nonascii_url.dqm源程序如下:

1

2

3

 

 

4

5

6

7

<html>

<body>

<a href="read_nonascii_url.dqm?name=<%=java.net.URLEncoder.encode("\u888b\u9f20\u86cb", "GBK")%>">

<%out.print("\u53d1\u9001url\u9644\u5e26\u7684\u6570\u636e", "GBK");%>

</a>

</body>

</html>

 

从上面第3行可以看到我们又用了java.net.URLEncoder.encode这个方法,这里我们是对\u888b\u9f20\u86cb这个字符串进行url编码(\u888b\u9f20\u86cb是“袋鼠蛋”的unicode表示格式)。\u53d1\u9001url\u9644\u5e26\u7684\u6570\u636e是“发送url附带的数据” unicode表示格式。

当打开write_nonascii_url.dqm后单击链接后就会指向read_nonascii_url.dqm,而这个动态文件就是用来读取url附带的数据,源程序如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

<%String charset = "GBK";%>

<html>

<head><title>

read non ascii url

</title>

<meta http-equiv=Content-Type content="text/html; charset=<%=charset%>">

</head>

 

<%

String value =request.getParameter("name", charset);

 

if (value == null) {

  out.println("name not found!");

}

else {

  out.println("name value: " + value, charset);

}

%></html>

 

从上面第10行可以看到用requestgetParameter带字符集参数的方法就可以直接读取url编码的数据了,这里我们当然还是采用GBK字符集来读取。

执行后你就可以看到结果是:

name value: 袋鼠蛋

 

从上面的结果看到中文字符被正确的读取,而这是在urlEnc设置为Shift_JIS字符集的情况下。你可以试着将write_nonascii_url.dqm中的url编码语句去掉,运行的话就会发现结果是乱码。

 

 

F2.3 读取数据时的字符集

在上一节中第3点已经介绍了通过url附带数据的方法,这种传输数据的方式通常称之为GET方式,然而对于量比较大的数据则可以通过POST方式上传,本节我们将介绍POST方式的国际化问题。

国际化的客户端通过POST方式可以上传多种字符集的数据,服务器必须以其对应的字符集读取才能准确获得字符。那么kangaroo-egg服务器在读取POST方式的数据时是使用哪种字符集呢?是使用webconfig.xmlsystemSet元素中的dataEnc值指定的字符集来读取的(dataEnc参见2.1节),就像上一节也遇到过的,dataEnc只能设定一种字符集,所以不可能同时能够读取多种字符集,不过我们可以使用带指定字符集的方法读取。

我们来看个例子,首先将dataEnc设置为US-ASCII,即英文字符集,至于urlEnc你可任意设置为一种字符集,然后运行6.1节曾使用过的post.htmpost.dqm,你会发现post.htm没有问题,而当通过post.htmpost.dqm传送数据时post.dqm显示的是乱码,首先我们按F2.1节介绍的方法将所有中文字符变成unicode形式后按GBK字符集输出,改变后的post.dqm源代码如下:

1

2

3

4

5

6

 

7

 

8

9

10

11

12

13

14

 

15

16

17

 

18

19

20

 

21

 

22

 

23

24

25

26

27

28

29

30

<%@page import="java.util.*"%>

<pre><%

String charset = "GBK";

//读取所有非多分数据项的名称

Set nameSet = request.getParameterNameSet();

if (nameSet.size() == 0) {

  out.println("\u6ca1\u6709\u4e0a\u4f20\u4efb\u4f55\u975e\u591a\u5206\u6570\u636e\uff01", "charset");

}

else {

  Iterator<String> nameItor = nameSet.iterator();

  while (nameItor.hasNext()) {

    String tempName = nameItor.next();

    out.println("------------------------------------------------------");

    out.println("\u4e0a\u4f20\u975e\u591a\u5206\u9879\u540d\u79f0\uff1a" + tempName, charset);

 

    //getParameter方法

    out.println("\u672c\u975e\u591a\u5206\u9879\u7684\u503c\uff1a" + request.getParameter(tempName), charset);

 

    //getParameterValues方法

    String tempValues[] = request.getParameterValues(tempName);

    out.println("\u672c\u975e\u591a\u5206\u9879\u603b\u5171\u6709\u51e0\u4e2a\u503c\uff1a" + tempValues.length, charset);

    out.print("\u672c\u975e\u591a\u5206\u9879\u7684\u6240\u6709\u503c\uff1a", charset);

    for (int i = 0; i < tempValues.length; i++) {

      out.print(tempValues[i] + ", ", charset);

    }

    out.println("");

  }

}

 

%></pre>

 

再次通过post.htmpost.dqm传送数据时你会发现信息字符已经没有问题了,但是如果传送的是中文则显示为问号,英文就没有问题。比如我们在post.htmkey1栏中写“data”,在key2栏中写“数据”,那么执行结果如下:

------------------------------------------------------

上传非多分项名称:key1

本非多分项的值:data

本非多分项总共有几个值:2

本非多分项的所有值:data, ????,

------------------------------------------------------

上传非多分项名称:B1

本非多分项的值:????

本非多分项总共有几个值:1

本非多分项的所有值:????,

------------------------------------------------------

上传非多分项名称:key2

本非多分项的值:kangaroo-egg

本非多分项总共有几个值:1

本非多分项的所有值:kangaroo-egg,

 

 

从上面结果可以看到,英文的“data”正常显示了,而中文的“数据”变成了四个问号,为什么呢?因为request.getParameter方法如果不指定字符集则按dataEnc设置的字符集读取,而dataEnc此时被我们设置为了US-ASCII,所以当然无法正常读取中文字符,于是我们要使用request.getParameter带有字符集的方法(request.getParameter方法参见6.1节),post.dqm的源程序在前面的基础上再修改如下:

1

2

3

4

5

6

 

7

 

8

9

10

11

12

13

14

 

15

16

17

 

18

19

20

 

21

 

22

 

23

24

25

26

27

28

29

30

<%@page import="java.util.*"%>

<pre><%

String charset = "GBK";

//读取所有非多分数据项的名称

Set nameSet = request.getParameterNameSet();

if (nameSet.size() == 0) {

  out.println("\u6ca1\u6709\u4e0a\u4f20\u4efb\u4f55\u975e\u591a\u5206\u6570\u636e\uff01", "charset");

}

else {

  Iterator<String> nameItor = nameSet.iterator();

  while (nameItor.hasNext()) {

    String tempName = nameItor.next();

    out.println("------------------------------------------------------");

    out.println("\u4e0a\u4f20\u975e\u591a\u5206\u9879\u540d\u79f0\uff1a" + tempName, charset);

 

    //getParameter方法

    out.println("\u672c\u975e\u591a\u5206\u9879\u7684\u503c\uff1a" + request.getParameter(tempName, charset), charset);

 

    //getParameterValues方法

    String tempValues[] = request.getParameterValues(tempName, charset);

    out.println("\u672c\u975e\u591a\u5206\u9879\u603b\u5171\u6709\u51e0\u4e2a\u503c\uff1a" + tempValues.length, charset);

    out.print("\u672c\u975e\u591a\u5206\u9879\u7684\u6240\u6709\u503c\uff1a", charset);

    for (int i = 0; i < tempValues.length; i++) {

      out.print(tempValues[i] + ", ", charset);

    }

    out.println("");

  }

}

 

%></pre>

 

再次执行你会发现中文字符已经没有任何问题了。从上面源代码红色部分看出,我们在读取数据时使用了requestgetParametergetParameterValues带字符集参数的方法。在前面6.7中介绍的多分数据中也有指定字符集的读取方法,用法和功能也是相同的,这里就不再复述了。

 

 

F2.4 设置http头时的字符集

前面5.2节中曾介绍response.setHeader方法,其中还举了一个httphead.dqm的例子,当执行httphead.dqm后浏览器会提示下载“http压缩.txt”这个文件,不过并不是所有情况下都会如此正常执行,现在我们就将webconfig.xmlsystemSet元素中的dataEnc值改为US-ASCIIdataEnc参见2.1节),再次访问httphead.dqm后浏览器还是会提示下载文件,只不过提示下载的文件名不是“http压缩.txt”,而是变成了其它乱码,这是为什么呢?5.2节中已经提到过了setHeader(String tag, String value)方法将会采用systemSet元素中的dataEnc的值进行编码,而现在dataEnc的值被改成了US-ASCII,所以当执行

response.setHeader("Content-Disposition", "attachment;filename=http压缩.txt")

这个方法时,所有的中文字符都会按US-ASCII字符集输出,这样中文字符变成了乱码,所以浏览器提示下载的文件名也变成了乱码。

那么在提供多种语言浏览时该怎么办呢?systemSet元素中的dataEnc值只能设置为一种字符集,于是我们就要用到5.2节的介绍到的

setHeader(String tag, String valueString charsetName)

方法,这个方法能在输出http头时不默认采用dataEnc值编码,而是由用户指定编码字符集。于是将5.2节中的httphead.dqm修改成:

1

2

 

3

4

5

6

<%@page buffer="true"%>

<%response.setHeader("Content-Disposition", "attachment;filename=http压缩.txt", "GBK");%>

 

什么是HTTP压缩?

 

HTTP压缩(或叫HTTP内容编码)作为一种网站和网页相关的标准,存在已久了,只是最近几年才引起大家的注意。HTTP压缩的基本概念就是采用标准的gzip压缩或者deflate编码方法,来处理HTTP响应,在网页内容发送到网络上之前对源数据进行压缩。有趣的是,在版本4IENetScape中就早已支持这个技术,但是很少有网站真正使用它。Port80软件公司做的一项调查显示,财富1000强中少于5%的企业网站在服务器端采用了HTTP压缩技术。不过,在具有领导地位的网站,如GoogleAmazon、和Yahoo!等,HTTP内容编码技术却是普遍被使用的。考虑到这种技术会给大型的网站们带来带宽上的极大节省,用于突破传统的系统管理员都会积极探索并家以使用HTTP压缩技术。

 

于是无论dataEnc设置为何值,此条http头都会按GBK字符集输出,不过这样是不是就没有问题了呢?的确在某些情况下还是有问题的(请参见F2.1节),那么怎么办呢,最保险的方法就是将非ASCII字符都改为它们等价的unicode表示格式,在这里“压缩”的unicode表示格式为“\u538b\u7f29”(如何转换请参见F2.1节),于是我们修改httphead.dqm为:

 

 

1

2

 

3

4

5

6

<%@page buffer="true"%>

<%response.setHeader("Content-Disposition", "attachment;filename=http\u538b\u7f29.txt", "GBK");%>

 

什么是HTTP压缩?

 

HTTP压缩(或叫HTTP内容编码)作为一种网站和网页相关的标准,存在已久了,只是最近几年才引起大家的注意。HTTP压缩的基本概念就是采用标准的gzip压缩或者deflate编码方法,来处理HTTP响应,在网页内容发送到网络上之前对源数据进行压缩。有趣的是,在版本4IENetScape中就早已支持这个技术,但是很少有网站真正使用它。Port80软件公司做的一项调查显示,财富1000强中少于5%的企业网站在服务器端采用了HTTP压缩技术。不过,在具有领导地位的网站,如GoogleAmazon、和Yahoo!等,HTTP内容编码技术却是普遍被使用的。考虑到这种技术会给大型的网站们带来带宽上的极大节省,用于突破传统的系统管理员都会积极探索并家以使用HTTP压缩技术。

 

这样就能确保浏览器提示下载文件名是正确的。同时5.7节所介绍的

public void outBinFile(File out_bin_file, String saveAsNameStr, String charsetName)

方法也是使用这个原理。