我的上一篇关于从applet中执行POST操作的技巧在读者中引出了许多问题。其中最突出的问题是:“如何显示由Web服务器上的POSTCGI-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分公司发布了HTMLrendererbean。(还有几个别的商用软件。)这样,将这个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分布式操作系统之父JohnOusterhout曾经说过:“在分布式计算中,状态是第二麻烦的问题。不,它是最麻烦的问题。”
服务器部分最复杂,所以让我们先来看一下applet。:-)这个applet与以前的“Java技巧34”中所用的Happyapplet仅有几点区别。到服务器的POST操作是相同的,但我们必须修改读取服务器响应的的部分:input=newDataInputStream(urlConn.getInputStream());
Stringstr=null;
StringfirstLine=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=newURL("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,是按照JavaServletAPI编写的--尽管通过CGI-bin脚本(用Perl、Tcl、Java或其他语言编写)也能实现同样的解决方案。
请注意,对servlet编程和管理的介绍不属于本文的讨论范围;我们仅讨论与POST解决方案有直接关系的主要问题。
PosterServlet代码包含大量的注释,以便指导您阅读代码。代码包含大量的错误处理和额外检查,用来处理过多的可能出现的问题、拒绝服务攻击等。但是多数时候您可以忽略这些代码。(稍后我会更深入地讨论安全性问题。)这个servlet是针对Java1.1.xAPI编写的(而applet代码是针对Java1.0.2编写的)。
doPost()方法处理POST请求--即,doPost()负责前三种服务器职责:它根据通过POST发送来信息生成HTML文档,然后将文档保存到一个临时的磁盘文件中,并将魔力键返回给applet,魔力键用来标识HTML文档,并适合直接嵌入随后的GET请求中。
下面是代码的核心部分。(要查看完整的代码,请参阅实际的源文件。)实现说明:魔力键实际上是为此事务生成的HTML文档的文件名。文件名是使用java.util.Random类生成的一个Long值。protectedvoiddoPost(HttpServletRequestrequest,
HttpServletResponseresponse)
throwsServletException,IOException
{
response.setContentType("text/plain");
//构建输出文件名。
StringfileName=(newLong(randomizer.nextLong())).toString();
Filefile=null;
try
{
file=newFile(posterTempDir File.separator
fileName posterTempExt);
}
catch(Exceptione)
{
sendPostFailure(response,
"Unabletobuildoutputfilepath!");
return;
}
//打开输出文件。
PrintWriteroutput=null;
try
{
output=newPrintWriter
(newBufferedWriter
(newFileWriter(file)));
}
catch(IOExceptione)
{
sendPostFailure(response,"Unabletoopenoutputfile!");
return;
}
output.println("<html>");
output.print("<head><title>PosterServletGeneratedOutput");
output.println("</title></head>");
output.println("<body>");
//现在,循环检查请求标头并将它们写入文件中。
StringheaderName=null;
Enumerationheaders=request.getHeaderNames();
if(headers.hasMoreElements())
{
output.println("<h1>CGIheaders:</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())
{
Stringline=null;
//将所有输入字节转换为字符。
BufferedReaderin=newBufferedReader
(newInputStreamReader(request.getInputStream()));
output.println("<h1>POSTcontents:<h1><hr>");
output.println("<p><pre>");
//读取输入的每一行,并将其写入输出文件中。
HttpUtilshttpUtils=newHttpUtils();
try
{
while(null!=(line=in.readLine()))
{
try
{
Hashtabledata=httpUtils.parseQueryString(line);
StringkeyName=null;
Enumerationkeys=data.keys();
while(keys.hasMoreElements())
{
String[]values=null;
keyName=(String)keys.nextElement();
output.print(keyName);