我的上一篇关于 从 applet 中执行 POST 操作的技巧在读者中引出了许多问题。其中最突出的问题是:“如何显示由 Web 服务器上的 POST CGI-bin 处理程序返回的 HTML 文档?”。在这篇技巧中,我们将探索这一问题的解决方法,并深入研究几个很棒的服务器端的 Java 问题。
注意:本技巧假定您知道读者提出的有关通过 Java 执行 POST 操作的一些基本问题。如果您还不熟悉这些概念,请参考“Java 技巧 34”。
那么,我们如何显示来自 applet 的 POST 的结果呢?这一问题有四个答案。按照难易程度递增的顺序排列,依次为:
无法显示。
别执行 POST 操作。
使用 bean。
作弊。
正如在“Java 技巧 34”中讨论的那样,浏览器目前所用的安全性管理器不允许在浏览器中显示由 applet 生成的 HTML;浏览器仅允许我们将它指向 URL,URL 将代表我们将它显示出来。这种情况难以令人满意!
我们通过 -- 难以置信! -- 不使用 POST 就可以避开显示 POST 结果的这种限制。我们可以将一些信息编码在 URL 中,然后再将编码后的 URL 提供给 showDocument() 方法。这些信息可作为 GET 请求的参数传递给 Web 服务器。不幸的是,这存在一些缺陷:只能传输数量有限的数据 -- 此外,在这个过程中 URL 被更改。这样做相当笨拙。稍后我们会看到采用这种编码方式的一个示例。
最近,Sun 的 JavaSoft 分公司发布了 HTML renderer bean。(还有几个别的商用软件。)这样,将这个 bean 作为 applet 的一部分并用它来显示网页就成为可能。有什么缺点?大小、兼容性和成本。这个 bean 当然不小,它需要支持 bean 的浏览器,而且不是免费的。当然,我们完全可以花时间来编写自己的翻译组件,但那是一种愚蠢的做法。
这个问题的一种有趣而有建设性的解决方案就是作弊。在这个特例中,我们让服务器端的代码(例如, CGI-bin 脚本)与我们的 applet 共同作弊。基本思想很简单:将 POST 与随后的 GET 结合起来使用。这个过程如下所示:
applet 仍然通过 POST 操作将信息发送给服务器。
服务器利用 POST 信息生成 HTML。
服务器将 HTML 保存到 Web 服务器上的文件中。
服务器向 applet 返回一个魔力键。
applet 将这个键编码在 URL 中并返回给服务器。
applet 通过在 showDocument() 调用中使用生成的 URL 来通知浏览器显示网页。
服务器接收 GET 请求并提取魔力键参数。
服务器检索与此魔力键相关的文件。
服务器将文件中的 HTML 内容返回给浏览器。
浏览器将 HTML 内容显示出来。
这种返回处理无疑比其他解决方案更复杂,但现在这种处理适用于客户机和服务器的广泛组合方式。这种处理的缺点在于,完成一个完整的事务必须执行多个 HTTP 请求。我们必须在多个请求的之间维护“状态”信息,以便能跟踪正在进行的事务(回忆一下,HTTP 是一种无状态的请求/响应协议)。稳健地处理这些必要的状态信息可能相当具有挑战性。Tcl 脚本语言及 Sprite 分布式操作系统之父 John Ousterhout 曾经说过:“在分布式计算中,状态是第二麻烦的问题。不,它是最麻烦的问题。”
服务器部分最复杂,所以让我们先来看一下 applet。:-) 这个 applet 与以前的“Java 技巧 34”中所用的 Happy applet 仅有几点区别。到服务器的 POST 操作是相同的,但我们必须修改读取服务器响应的的部分:input = new DataInputStream (urlConn.getInputStream ());
String str = null;
String firstLine = null;
while (null != ((str = input.readLine())))
{
if (null == firstLine)
firstLine = str;
System.out.println (str);
textArea.appendText (str + "\n");
}
input.close ();
经过许可,服务器返回魔力键作为第一行。魔力键是一段状态信息,此信息用来唯一标识 applet 所涉及的、与此服务器有关的事务。 如果在处理 POST 请求的过程中遇到任何问题,服务器通过以下方式将这一情况通知 applet:返回 "nil" 字符串,并紧接着返回这一问题的文本描述。applet 现在所要做的唯一操作就是构建 URL,并调用 showDocument() 来显示 HTML:if (null != firstLine)
{
url = new URL ("http://" +
((getCodeBase()).getHost()).toString() +
"/poster?" + firstLine);
(getAppletContext()).showDocument (url, "_blank");
}
一定要注意,URL 参数必须是 URL 编码的。在上面的代码段中,因为来自服务器的魔力键已被安全编码,所以我们只需添加问号将基准 URL 与所传递的参数分隔开。
现在我们已讨论了 applet 部分,下面该研究服务器了。在以前有关 POST 的一篇 Java 技巧中,服务器端的代码是用 Perl 编写的传统 CGI-bin 脚本。Perl 是一种不错的解决方案,但是您难道不想用 Java 编写服务器端的代码吗?我们可以用 Java 编写 CGI-bin 脚本(请参阅参考资源部分),但还有更好的解决办法:那就是作为 Web 服务器本身一部分的 Java。这种服务器端的 Java 称为 servlet。本文所提供的解决方案将是一个 servlet,是按照 Java Servlet API 编写的 -- 尽管通过 CGI-bin 脚本(用 Perl、Tcl、Java 或其他语言编写)也能实现同样的解决方案。
请注意,对 servlet 编程和管理的介绍不属于本文的讨论范围;我们仅讨论与 POST 解决方案有直接关系的主要问题。
PosterServlet 代码包含大量的注释,以便指导您阅读代码。代码包含大量的错误处理和额外检查,用来处理过多的可能出现的问题、拒绝服务攻击等。但是多数时候您可以忽略这些代码。(稍后我会更深入地讨论安全性问题。)这个 servlet 是针对 Java 1.1.x API 编写的(而 applet 代码是针对 Java 1.0.2 编写的)。
doPost() 方法处理 POST 请求 -- 即,doPost() 负责前三种服务器职责:它根据通过 POST 发送来信息生成 HTML 文档,然后将文档保存到一个临时的磁盘文件中,并将魔力键返回给 applet,魔力键用来标识 HTML 文档,并适合直接嵌入随后的 GET 请求中。
下面是代码的核心部分。(要查看完整的代码,请参阅实际的源文件。)实现说明:魔力键实际上是为此事务生成的 HTML 文档的文件名。 文件名是使用 java.util.Random 类生成的一个 Long 值。 protected void doPost (HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
response.setContentType ("text/plain");
// 构建输出文件名。
String fileName = (new Long (randomizer.nextLong())).toString();
File file = null;
try
{
file = new File (posterTempDir + File.separator +
fileName + posterTempExt);
}
catch (Exception e)
{
sendPostFailure (response,
"Unable to build output file path!");
return;
}
// 打开输出文件。
PrintWriter output = null;
try
{
output = new PrintWriter
( new BufferedWriter
( new FileWriter (file)));
}
catch (IOException e)
{
sendPostFailure (response, "Unable to open output file!");
return;
}
output.println ("< html>");
output.print ("< head>< title>Poster Servlet Generated Output");
output.println ("< /title>< /head>");
output.println ("< body>");
// 现在,循环检查请求标头并将它们写入文件中。
String headerName = null;
Enumeration headers = request.getHeaderNames();
if (headers.hasMoreElements())
{
output.println ("< h1>CGI headers:< /h1>< hr>");
output.println ("< ul>");
while (headers.hasMoreElements())
{
headerName = (String) headers.nextElement();
output.print ("< li>< b>");
output.print (headerName);
output.print (" = ");
output.print (request.getHeader (headerName));
output.println ("< /b>< /li>< br>");
}
output.println ("< /ul>< hr>< br>");
}
// 处理 POST 内容。
if (0 < request.getContentLength())
{
String line = null;
// 将所有输入字节转换为字符。
BufferedReader in = new BufferedReader
( new InputStreamReader (request.getInputStream()));
output.println ("< h1>POST contents:< h1>< hr>");
output.println ("< p>< pre>");
// 读取输入的每一行,并将其写入输出文件中。
HttpUtils httpUtils = new HttpUtils();
try
{
while (null != (line = in.readLine()))
{
try
{
Hashtable data = httpUtils.parseQueryString (line);
String keyName = null;
Enumeration keys = data.keys();
while (keys.hasMoreElements())
{
String[] values = null;
keyName = (String) keys.nextElement();
output.print (keyName);