请稍等ManixChen正在解析过程中………



Servlet乱码分析



我们知道,web浏览器会将form中的内容打包成HTTP请求体,然后发送到服务端,服务端对请求体解析后可以得到传递的数据。这当中包含两个过程:encodedecode

HTTP

我们使用ServerSocket搭建一个小服务器来看清http请求的全貌, 该服务器只有一个功能, 就是打印请求体。

public class HttpPrint {
    private ServerSocket serverSocket;

    public HttpPrint() throws IOException {
        serverSocket = new ServerSocket(8080);
    }

    public void show() throws IOException{
        while(true){
            Socket socket = serverSocket.accept();
            byte[] buf = new byte[1024];
            InputStream is =  socket.getInputStream();
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            int n = 0;
            while ((n = is.read(buf)) > -1){
                os.write(buf,0,n);
            }
            os.close();
            is.close();
            socket.close();
            System.out.println(os);
        }
    }

    public static void main(String[] args) throws IOException {
        new HttpPrint().show();
    }
}

用html页面来发送get与post请求

<a href="http://localhost:8080/hsp?param=你好全世界">Test</a>
<form action="http://localhost:8080/hsp" method="post">
   <input type="text" name="param" value="你好全世界"/>
   <input type="submit"/>
</form>

启动服务器后,查看打印内容,在我的机器上,请求内容如下:

get

get

post

post

从post中的Content-Type:application/x-www-form-urlencoded可以看到,虽然数据为中文,但是在传递的时候,经过了一次urlEncode,这样一来,在数据交换层面就可以屏蔽编码的不一致性。

UrlEncode

urlEncode的任务是将form中的数据进行编码, 编码过程非常简单, 任何字符只要不是ASCII码, 它们都将被转换成字节形式, 每个字节都写成这种形式:一个 “%” 后面跟着两位16进制的数值。 urlEncode只能识别ASCII码,可以想象的是,那些urlEncode不能识别的字符,也就是十六进制数,一定是依赖于特定的字符集产生的, 字符集包括unicode,iso等。

那么浏览器用的是什么字符集呢? 答案是:默认与contentType相同, form可以通过属性accept-charset指定。

例如我们通常可以在jsp中看到这样的设置:

<%@page contentType="text/html;charset=UTF-8" %>

或者在html中这样设置:

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

这表示浏览器得到响应流之后,用contentType指定的字符集,将流中的字节转换为字符,同样地,也会用这个字符集将页面中字符转换为字节。

关于浏览器设定字符集的问题,我们不过多讨论,现在只需要知道有这么个过程就行了, 需要注意的是,无论浏览器使用什么字符集,服务端都是无法获知的。 这里需要换位考虑一下,浏览器是一个客户端,应该让客户端 “迁就” 服务端, 所以浏览器请求一个服务的时候,应该让浏览器考虑服务端支持什么字符集, 得到了响应后, 用服务端告诉浏览器的字符集进行解析。

UrlDecode

现在我们将目光转向Servlet, 并使用上面的html来请求服务,请确保请求的字符集为unicode, 应用服务器使用tomcat6。

public class HttpServletPrint extends HttpServlet{
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(req.getParameter("param"));
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req,resp);
    }
}

get与post结果如下,果然不负众望地乱码了(如果不乱码,我还写个毛?)。

param

现在我们从Servlet中看看请求体, 修改上面的Servlet代码如下:

public class HttpServletPrint extends HttpServlet{
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //System.out.println(req.getParameter("param"));
        byte[] buf = new byte[1024];
        int n = 0;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        InputStream is = req.getInputStream();
        while ((n = is.read(buf, 0, buf.length))>-1){
            bos.write(buf, 0, n);
        }
        String param = bos.toString();
        String s = URLDecoder.decode(param,"utf-8");
        System.out.println(s);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req,resp);
    }
}

get与post结果如下,Servlet将http头部解析完成后,将请求体留了下来供应用程序使用, 这是考虑到http请求可能有多种 enctype , 请求体的结构可能不同, 例如,multipart/form-data就不是这样的key=value结构,关于multipart/form-data,我在这篇 fileupload 中曾有过简要分析。

body-utf

从图中可以看到, 设置了正确的字符集后, 服务端将能够正确地解析, get部分什么都没有,这是因为get没有请求体。

让我们换种字符集试试, 比如, 将servet中的”utf-8”换成”iso-8859-1”。

body-iso

嘿,果然如我所料,而且这个字符串好像很眼熟,和req.getParameter(“param”)结果是一样的,没错,事实上,tomcat默认的字符集就是iso-8859-1, 我们从中可以得到一个推论,tomcat使用默认的字符集,对http请求进行过一次decode。

方案

urlDecode的任务是将请求中的百分号码转换成字符,显而易见的是,使用与urlEncode时相同的字符集才能成功转换。通常的做法是,让服务端支持涵盖多国语言的”utf-8”,然后让客户端也用”utf-8”请求服务。

指定服务端字符集的方式有两种,一是修改应用服务器的默认编码,二是添加一个过滤器进行编码转换, 方法一最方便, 但是影响了程序的可移植性, 方法二可移植, 它只需要做一件事:requet.setCharacterEncoding("UTF-8");, 实际上,该过滤器并没有进行任何编码转换的工作,它仅仅只是一个配置,该配置项将被后续程序使用,这些后续程序包括web服务器内置的解析程序,以及第三方解析工具等。

需要注意的是,requet.setCharacterEncoding(“UTF-8”);,只对请求体有效,也就是说,请求头不归它管,而是由web服务器采用自己配置的字符编码进行解析,此时如果url中包含中文(如get请求的参数),那么将不可避免地出现字符丢失。 解决办法是在客户端对url进行encodeURI两次, 然后再在服务端URLDecoder.decode(param,"utf-8");

为什么要 encodeURI 两次?talk is cheap, let’s code!

encodeURI

注意观察这张图片,从中发现了什么? 没错,第一次encodeURI生成了HTTP一节的示例中一样的结果。 我们在浏览器窗口中输入 “http://localhost:8080/hsp?param=%E4%BD%A0%E5%A5%BD%E5%85%A8%E4%B8%96%E7%95%8C”, 会发现它变成了 “http://localhost:8080/hsp?param=你好全世界”, 在url里,浏览器认为%是个转义字符,浏览器会把%与%之间的编码,两位两位取出后进行decode, 也就是变回 “你好全世界”, 然后再用这个url发送请求, 最终实际发送的内容实际上还是%E4%BD%A0%E5%A5%BD%E5%85%A8%E4%B8%96%E7%95%8C。 换言之,以明文传递的这种url会被浏览器否决一次,再换言之,在js中进行一次encodeURI等于什么都没做。

再注意观察第2和第3个输出,有什么规律? 是的,从第二次开始encodeURI只是将%变成了%25, 根据我们刚才总结出的规律可知,在encodeURI两次的情况下,最后发送到浏览器中的数据为%25E4%25BD%25A0%25E5%25A5%25BD%25E5%2585%25A8%25E4%25B8%2596%25E7%2595%258C, 理所当然的,web服务器将使用默认的字符集对其decode, 然而, 无论选择哪种字符集, 将%25转换成%总是不会出错的, decode之后,%E4%BD%A0%E5%A5%BD%E5%85%A8%E4%B8%96%E7%95%8C 将完整地送到Servlet手上。

System.out.println(URLDecoder.decode(req.getParameter("param"),"utf-8"));
window.location.href="http://localhost:8080/hsp?param=" + encodeURI(encodeURI('你好全世界'));

world

servlet

encode

Java

markdown自动生成github博客(前篇)

RMI

spring security 探秘

Bean Validation

Java中用js解析json

SQL 拼接

WebService

Java8 新特性

Java Concurrent

Java虚拟机

Effective Java

超类中的泛型

Flex

Custom Fileupload

可重入锁

web servlet encode Java Mar 17, 2015