使用Javascript XMLHttpRequest模拟表单(Form)提交上传文件

客户端用Javascript XMLHttpRequest在Mozilla平台上实现,服务器环境是Tomcat + Spring,以前实现过多次文件上载功能,但是以前的实现有个特点:要么客户端手工编程,要么服务器侧手工编程,但是从来没有两边都手工编程的。例如,在客户端,直接用浏览器的Form提交,不用管底层是怎样将文件上传的,如果手工编程的话就需要使用XMLHttpRequest对象模拟浏览器的表单提交过程。而在服务器测直接使用Spring的org.springframework.web.servlet.mvc.SimpleFormController类,可以不管Spring是怎样解析MultipartHttpServletRequest类型的上传请求的,如果是手工编程就需要手工辨别MultipartHttpServletRequest请求类型,然后调用MultipartHttpServletRequest.getFileNames()方法将上传的文件名和文件内容分别提取出来进行处理。

此次两边同时手工做,心里没有底,出了问题不知道哪一侧不对。偏偏出了问题, 服务器总是抛出异常

org.apache.commons.fileupload.FileUploadException: Read timed out

实际上在编程之前就心里开始打鼓,因为根据规范,HTTP中传送的内容是ASCII码的,要上传二进制文件,是否要手工将二进制用7bit编码?另外,模拟表单提交上传文件都是模拟一个multipart/form-data消息,根据HTML规范,使用边界(boundary)字符串将各个字段分割开,上传文件时既有描述边界格式的文本内容,也有二进制文件内容,怎样使用XMLHttpRequest.send()函数发送这种混合的内容?

带着这些问题,先上网找几个例子,先阅读了XMLHttpRequest模拟表单上传文件,照着做了出现上述异常,继续网上冲浪,奋战一天多,找了几个好的文章:一个详细的例子中文的例子和评论。异常仍然不能排除,后来将重点放在服务器侧,搜索read timed out有关的内容,很多遇到这样问题的帖子,但是没有一个合适的,后来在一个论坛帖子中,有人认为

having trouble streaming the file contents into the multi-part form post

于是将重点再次转回客户端,好像是这个原因,因为如果不用文件streaming(同时使用了nsIStringInputStream, nsIFileInputStream, nsIBinaryInputStream,nsIMultiplexInputStream等Mozilla的XPCOM对象),而是直接将内容写入,是可以的。尝试了无数次,人越着急脑子越不好使,暂时放弃了。因为不是必须实现的功能(在网页抓取/数据抽取/异构数据对象搜索引擎工具包MetaSeeker V4版本中这是可选功能),放弃后思想没有负担了,今天中午突然想到可能是因为XMLHttpRequest中读了两次文件,而nsIFileInputStream设定成指针不能回零而且读到EOF就关闭流,所以第二次读时就读超时了。使用XMLHttpRequest.send()有可能发送两次HTTP消息,因为服务器使用了HTTP Digest鉴权,所以第一次读文件流时已经读完了,第二次没的读了。下午一试即灵,这才想起中文的例子和评论那篇文章,在真正上传文件之前先发一个内容为空的用于鉴权的消息。

主要问题解决了,还有一个小问题,就是上述引用的这些例子都不能在我的环境中用,发上去的消息中无法正确解析出上传的文件来。实际上他们的程序都不符合HTML规范,详细说明如下:

在程序中定义了一个变量boundary,大部分例子中这个boundary变量的起始字符是"--",因为在HTTP消息体中要使用"--",但是,如果这样定义,那么调用

xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);

设置HTTP消息头的时候,就将"--"也声明进去了,我发现在我的运行环境中这样不行,如果定义的变量boundary中没有起始字符"--"就可以了。当然此时要注意在整个消息的结尾需要在boundary字符串前后都加"--",可以参照HTML规范构造符合规范的消息体。难道运行平台不一样使用方法就不一样吗?

另外还有一个疑问,程序运行结束后是否应该将打开的流都调用close()?上面的例子没有一个调用的。