4.4.3 提交表单数据
在上一节中,我们介绍了如何从Web服务器读取数据。现在,我们将介绍如何让程序再将数据反馈回Web服务器和那些被Web服务器调用的程序。
为了将信息从Web浏览器发送到Web服务器,用户需要填写一个类似图4-8中所示的表单。
图4-8 HTML表单
当用户点击提交按钮时,文本框中的文本以及复选框和单选按钮的设定值都被发送到了Web服务器。此时,Web服务器调用程序对用户的输入进行处理。
有许多技术可以让Web服务器实现对程序的调用。其中最广人所知的是Java Servlet、JavaServer Face、微软的ASP(Active Server Pages,动态服务器主页)以及CGI(Common Gateway Interface,通用网关接口)脚本。
服务器端程序用于处理表单数据并生成另一个HTML页,该页会被Web服务器发回给浏览器,这个操作过程我们在图4-9中作了说明。返回给浏览器的响应页可以包含新的信息(例如,信息检索程序中的响应页)或者仅仅只是一个确认。之后,Web浏览器将显示响应页。
图4-9 执行服务器端脚本过程中的数据流
我们不会在本书中介绍应该如何实现服务器端程序,而是将侧重点放在如何编写客户端程序使之与已有的服务器端程序进行交互。
当表单数据被发送到Web服务器时,数据到底由谁来解释并不重要,可能是Servlet或CGI脚本,也可能是其他服务器端技术。客户端以标准格式将数据发送给Web服务器,而Web服务器则负责将数据传递给具体的程序以产生响应。
在向Web服务器发送信息时,通常有两个命令会被用到:GET和POST。
在使用GET命令时,只需将参数附在URL的结尾处即可。这种URL的格式如下:
其中,每个参数都具有“名字=值”的形式,而这些参数之间用&字符分隔开。参数的值将遵循下面的规则,使用URL编码模式进行编码:
·保留字符A到Z、a到z、0到9,以及.-~_。
·用+字符替换所有的空格。
·将其他所有字符编码为UTF-8,并将每个字节都编码为%后面紧跟一个两位的十六进制数字。
例如,若要发送街道名San Francisco,CA,可以使用San+Francisco%2c+CA,因为十六进制数2c(即十进制数44)是“,”的UTF-8码值。
这种编码方式使得在任何中间程序中都不会混入空格,并且也不需要对其他特殊字符进行转换。
例如,就在写作本书的时候,Google Map网站(www.google.com/maps)可以接受带有两个名为q和h1参数的查询请求,这两个参数分别表示查询的位置和响应中所使用的人类语言。为了得到1 Market Street,San Franciso,CA的地图,并且让响应使用德语,只需访问下面的URL即可:
在浏览器中出现很长的查询字符串很让人郁闷,而且老式的浏览器和代理对在GET请求中能够包含的字符数量做出了限制。正因为此,POST请求经常用来处理具有大量数据的表单。在POST请求中,我们不会在URL上附着参数,而是从URLConnection中获得输出流,并将名/值对写入到该输出流中。我们仍旧需要对这些值进行URL编码,并用&字符将它们隔开。
下面,我们将详细介绍这个过程。在提交数据给服务器端程序之前,首先需要创建一个URLConnection对象。
然后,调用setDoOutput方法建立一个用于输出的连接。
接着,调用getOutputStream方法获得一个流,可以通过这个流向服务器发送数据。如果要向服务器发送文本信息,那么可以非常方便地将流包装在PrintWriter对象中。
现在,可以向服务器发送数据了。
之后,关闭输出流。
最后,调用getInputStream方法读取服务器的响应。
下面我们来实际操作一个例子。地址为https://www.usps.com/zip4的网站包含一个用于查找街道地址的邮政编码的表单(见图4-8)。要想在Java程序中使用这个表单,需要知道POST请求的URL和参数。
你可以通过查看这个表单的HTML源码来获取这些信息,但是通常用网络监视器来“窥视”发出的请求会更容易一些。作为其开发工具包的组成部分,大多数浏览器都具有网络监视器。例如,图4-10展示了Firefox网络监视器向我们的示例网站提交数据时的截屏。你可以发现其中的提交URL以及参数名和参数值。
图4-10 一个HTML表单
在提交表单数据时,HTTP头包含了内容类型和内容长度:
你还可以以其他格式提交表单。例如,发送用JavaScript对象表示法(JSON)表示的数据,将内容类型设置为application/json。
POST的头还必须包括内容长度,例如:
程序清单4-7用于将POST数据发送给任何脚本,它将数据放在如下的.properties文件:
这个程序移除了url项,并将其他内容都发送到了doPost方法。
在doPost方法中,我们首先打开连接、调用setDoOutput(true)并打开输出流。然后,枚举Map对象中的所有键和值。对每一个键-值对,我们发送key、=字符、value和&分隔符:
在从写出请求切换到读取响应的任何部分时,就会发生与服务器的实际交互。Content-Length头被设置为输出的尺寸,而Content-Type头被设置为application/x-www-form-urlencoded,除非指定了不同的内容类型。这些头信息和数据都被发送给服务器,然后,响应头和服务器响应会被读取,并可以被查询。在我们的示例程序中,这种切换发生在对connection.getContentEncoding()的调用中。
在读取响应过程中会碰到一个问题。如果服务器端出现错误,那么调用connection.getInputStream()时就会抛出一个FileNotFoundException异常。但是,此时服务器仍然会向浏览器返回一个错误页面(例如,常见的“错误404-找不到该页”)。为了捕捉这个错误页,可以调用getErrorStream方法:
注意:getErrorStream方法与这个程序中的许多其他方法一样,属于URLConnection类的子类HttpURLConnection。如果要创建以http://或https://开头的URL,那么可以将所产生的连接对象强制转型为HttpURLConnection。
在将POST数据发送给服务器时,服务器端程序产生的响应可能是redirect:,后面跟着一个完全不同的URL,该URL应该被调用以获取实际的信息。服务器可以这么做,因为这些信息位于他处,或者提供了一个可以作为书签标记的URL。HttpURLConnection类在大多数情况下可以处理这种重定向。
注意:如果cookie需要在重定向中从一个站点发送给另一个站点,那么你可以像下面这样配置一个全局的cookie处理器:
然后,cookie就可以被正确地包含在重定向请求中了。
尽管重定向通常是自动处理的,但是有些情况下,你需要自己完成重定向。例如,在HTTP和HTTPS之间的自动重定向因为安全原因而不被支持。重定向还会因更细微的原因而失败。例如,邮政编码服务在User-Agent请求参数包含字符串Java时无法工作,这可能是因为邮政局不想为程序自动产生的请求服务。尽管可以在最初的请求中将用户代理设置为其他的字符串,但是这项设置在自动重定向中并没有用到。自动重定向总是会发送包含单词Java的通用用户代理字符串。
在这些情况下,可以人工实现重定向。在连接到服务器之前,将自动重定向关闭:
在发送请求之后,获取响应码:
检查它是否是下列值之一:
如果是这些值之一,那么获取Location响应头,以获得重定向的URL。然后,断开连接,并创建到新的URL的连接:
每当需要从某个现有的Web站点查询信息时,该程序所展示的处理技术就会显得很有用。只需找出需要发送的参数,然后从回复信息中剔除HTML和其他不必要的信息。
注意:正如你所看到的,可以使用Java库的类来与网页交互,但是用起来并非特别方便。可以考虑使用其他的库,例如Apach HttpClient(http://hc.apache.org/httpcomponents-client-ga)。
程序清单4-7 post/PostTest.java
java.net.HttpURLConnection 1.0
·InputStream getErrorStream()
返回一个流,通过这个流可以读取Web服务器的错误信息。
java.net.URLEncoder 1.0
·static String encode(String s,String encoding)1.4
采用指定的字符编码模式(推荐使用“UTF-8”)对字符串s进行编码,并返回它的URL编码形式。在URL编码中,'A'-'Z','a'-'z','0'-'9','-','_','.'和'*'等字符保持不变,空格被编码成'+',所有其他字符被编码成"%XY"形式的字节序列,其中0xXY为该字节十六进制数。
java.net.URLDecoder 1.2
·static string decode(String s,String encoding)1.4
采用指定编码模式对已编码字符串s进行解码,并返回结果。