2013年11月3日

使用i-jetty架设服务器及部署程序中的若干问题

由于索尼悄悄地在我的PS+会员到期后自动帮我续费,因此觉得不把《重力眩晕》白金了真是白白浪费一个月的会员费。1号深夜终于拿到了白金。随后而来的问题是:在我校恶劣的网络条件下,如何快速地向朋友们得瑟。

我校上网需使用客户端登陆,并且由于Web方式登陆有bug,因此学校封掉了Web方式登陆。像PSV这么小众的玩意儿是自然没有客户端可用的,要把白金奖杯的截图传输到手机上才能分享出去。又或者,我的PSV要能访问网络。首先想到的办法是用手机开代理服务器,然而我找的这个代理服务器效果很差,基本上连不上去。之后我用手机开启无线AP,共享蜂窝网络的方法使得PSV能够访问网络,发了一封以截图为附件的邮件,最后通过手机把这张图发到微博。

但是我并不想今后也这么搞,虽然每次耗的流量并不大,但操作也是比较繁琐的。如果我的手机能架设个Web服务器,那么配合宿舍里自己整的无线网络,就可以从PSV通过HTTP上传图片至手机,而且今后再有别的想法或者点子也更容易扩展,拥有更多的可能性。于是我就找到了i-jetty,而且是很轻松地就找到了,而AAMPP架构的则更多。我认为,安卓给用户的自由空间更多,更有可能取代传统PC。

i-jetty是一个运行在安卓平台的Web服务器,可以认为是jetty在安卓平台的实现。Google Play中提供已经编译好的i-jetty,可以直接下载安装。

我最初考虑使用Java的web服务器是认为安卓应用本身是Java开发的,那么运行我自己写的java代码也是轻而易举,毫无障碍。但事实证明我错了——安卓的应用确实运行在Java虚拟机中,但这个虚拟机并不能执行编译后的.class字节码。需要用安卓SDK中的dx工具来打包成dex格式。

需要指出的是i-jetty 3.1不支持最新的Servlet 3.0、不支持JSP。不过这只是Google Play上提供的这个版本的情况,如果自己从Google code上编译代码也许可以支持上述的特性。

既然服务器都在手机上架设了,那么这个打包也在手机上完成好了。Terminal IDE提供Android、Java、C++、HTML的开发环境,其中就包括所需的dx工具。但这个dx实在太糟糕,转换几个class文件还行,但如果一个jar包有100KB以上就会报OutOfMemory,而且我一直没有找到可以提高其允许使用的最大内存的方法,亏我手机还是2G运存。

无奈只好转到电脑上来。用到的代码如下:

dx --dex --no-strict --output=D:\upload\WEB-INF\lib\classes.zip D:\upload\WEB-INF\classes

网上有人说把第三方jar包跟在上述命令的最后,可以把第三方jar包也打包进dex中,否则程序运行时会因为缺少这些jar包而发生ClassNotFound的错误,再次说明:i-jetty或者说安卓应用是无法直接执行class字节码的,不管有没有打包成jar。后来我发现似乎把这些第三方jar包直接跟我自己的class文件一起放在待打包的目录中也可以。
这个命令里还有一个--no-strict参数,是我在手机上打包时发现总报包和类文件路径不匹配的错而搞的折中办法。
得到classes.zip后,把它放进WEB-INF目录下的lib子目录中,然后把整个网站目录拷到手机的/sdcard/jetty/webapps/中再启动i-jetty就可以访问我们的网站了。


接下来要说的是关于Servlet上传文件的问题。网上大多数做法是使用Apache的开源文件上传组件commons-fileupload。其功能是很强大,但到了我这儿就问题连连。在PC上部署后没有任何问题,到了i-jetty那里先是由于dex中没有包含commons-fileupload所依赖的两个jar包。

待我在PC上把这两个包打好,结果又说别的文件找不到。我不过是想搞个简单的上传而已,算了,不用你第三方的上传了!

于是继续在网上搜罗,不得不说Servlet的封装程度真的比ASP.NET低得多,真的是原生态编程,Servlet 2.5/2.4连个直接获取上传文件的方法都没有,只能自己从InputStream中读,而且上传的文件名是什么,上传的文件有多大,在请求头中是从那儿开始,到哪儿结束的统统没有现成的方法可以调用,都得自己来。

而到了Servlet 3.0,终于有了一个request.getPart(String name)可以获取上传的文件,但在使用上仍然需要注意

  1. 需要添加MultipartConfig注解(这个一定要加,不然各种报错)
  2. 从request对象中获取Part文件对象
  3. 需要支持Servlet 3.0的服务器(之前用旧版本的Tomcat就出错了)

但在具体实践中,还是有一些细节处理,诸如设置上传文件的最大值,上传文件的保存路径。

需要熟悉MultipartConfig注解,标注在@WebServlet之上,具有以下属性:

属性名
类型
是否可选
描述
fileSizeThreshold
int
当数据量大于该值时,内容将被写入文件。
location
String
存放生成的文件地址。
maxFileSize
long
允许上传的文件最大值。默认值为 -1,表示没有限制。
maxRequestSize
long
针对该 multipart/form-data 请求的最大数量,默认值为 -1,表示没有限制。

一些实践建议:

  1. 若是上传一个文件,仅仅需要设置maxFileSize熟悉即可。
  2. 上传多个文件,可能需要设置maxRequestSize属性,设定一次上传数据的最大量。
  3. 上传过程中无论是单个文件超过maxFileSize值,或者上传总的数据量大于maxRequestSize值都会抛出IllegalStateException异常;
  4. location属性,既是保存路径(在写入的时候,可以忽略路径设定),又是上传过程中临时文件的保存路径,一旦执行Part.write方法之后,临时文件将被自动清除。
  5. Servlet 3.0规范同时也说明,不提供获取上传文件名的方法,尽管我们可以通过part.getHeader("content-disposition")方法间接获取得到。
  6. 如何读取MultipartConfig注解属性值,API没有提供直接读取的方法,只能手动获取。

好了,现在我们终于可以让程序运行起来了,当然Servlet 3.0的这种写法在i-jetty上是不行的。另外,我还发现在保存上传的文件到服务器中时,获取完整的路径不能使用request.getServletContext().getRealPath(),而必须重写Servlet的init(ServletConfig config)方法,并从config.getServletContext()方法中获取ServletContext。

最后是我的代码:

/**  
 * Copyright ? 2013NCEPU. All rights reserved.
 *
 * @Title: UploadServlet.java
 * @Prject: Upload
 * @Package: servlet
 * @ClassName: UploadServlet
 * @Description: TODO
 * @author: ReiJi  
 * @email: joecaisc#gmail.com
 * @date: 2013年11月3日 下午7:11:42
 * @version: V1.0  
 */

package servlet;

import java.io.*;
import java.util.List;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.fileupload.*;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

/**
 * Servlet implementation class UploadServlet
 */
public class UploadServlet extends HttpServlet {
 private static final long serialVersionUID = 1L;

 private ServletContext context;

 /**
  * @see HttpServlet#HttpServlet()
  */
 public UploadServlet() {
  super();
  // TODO Auto-generated constructor stub
 }

 @Override
 public void init(ServletConfig config) throws ServletException {
  super.init(config);
  context = config.getServletContext();
 }

 /**
  * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
  *      response)
  */
 protected void doGet(HttpServletRequest request,
   HttpServletResponse response) throws ServletException, IOException {
  PrintWriter out = response.getWriter();
  String path = context.getRealPath("/");
  out.write("You get it!!!\nPath:" + path);
  out.flush();
  out.close();
 }

 /**
  * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
  *      response)
  */
 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  request.setCharacterEncoding("utf-8");
  String path = context.getRealPath("/upload");
  // saveAs(path,request,response);

  byte[] body = readBody(request);
  // 取得所有Body内容的字符串表示
  String textBody = new String(body, "ISO-8859-1");
  // 取得上传的文件名称
  String fileName = getFileName(textBody);
  // 取得文件开始与结束位置
  Position p = getFilePosition(request, textBody);
  // 输出至文件
  writeTo(path+"/"+fileName, body, p);
  response.sendRedirect(context.getContextPath()+"/success.html");
 }

 /**
  * 封装上传文件的起止位置
  */
 class Position {
  int begin;
  int end;
  public Position(int begin, int end) {
   this.begin = begin;
   this.end = end;
  }
 }

 /**
  * 
  * @Title: readBody
  * @Description: 从request中读取上传的文件
  * @param request
  * @return
  * @throws IOException
  */
 private byte[] readBody(HttpServletRequest request) throws IOException {
  // 获取请求文本字节长度
  int formDataLength = request.getContentLength();
  // 取得ServletInputStream输入流对象
  DataInputStream dataStream = new DataInputStream(
    request.getInputStream());
  byte body[] = new byte[formDataLength];
  int totalBytes = 0;
  while (totalBytes < formDataLength) {
   int bytes = dataStream.read(body, totalBytes, formDataLength);
   totalBytes += bytes;
  }
  return body;
 }

 /**
  * 
  * @Title: getFilePosition
  * @Description: 取得文件区段边界信息
  * @param request
  * @param textBody
  * @return
  * @throws IOException
  */
 private Position getFilePosition(HttpServletRequest request, String textBody) throws IOException {
  String contentType = request.getContentType();
  String boundaryText = contentType.substring(
    contentType.lastIndexOf("=") + 1, contentType.length());
  // 取得实际上传文件的起始与结束位置
  int pos = textBody.indexOf("filename=\"");
  pos = textBody.indexOf("\n", pos) + 1;
  pos = textBody.indexOf("\n", pos) + 1;
  pos = textBody.indexOf("\n", pos) + 1;
  int boundaryLoc = textBody.indexOf(boundaryText, pos) - 4;
  int begin = ((textBody.substring(0, pos)).getBytes("ISO-8859-1")).length;
  int end = ((textBody.substring(0, boundaryLoc)).getBytes("ISO-8859-1")).length;

  return new Position(begin, end);

 }

 /**
  * 
  * @Title: getFileName
  * @Description: 获取上传文件的文件名
  * @param requestBody
  * @return
  */
 private String getFileName(String requestBody) {
  String fileName = requestBody.substring(requestBody.indexOf("filename=\"") + 10);
  fileName = fileName.substring(0, fileName.indexOf("\n"));
  fileName = fileName.substring(fileName.indexOf("\n") + 1, fileName.indexOf("\""));
  return fileName;
 }

 /**
  * 
  * @Title: writeTo
  * @Description: 将上传的文件写入磁盘
  * @param fileName
  * @param body
  * @param p
  * @throws IOException
  */
 private void writeTo(String fileName, byte[] body, Position p) throws IOException {
  FileOutputStream fileOutputStream = new FileOutputStream(fileName);
  fileOutputStream.write(body, p.begin, (p.end - p.begin));
  fileOutputStream.flush();
  fileOutputStream.close();
 }

 /**
  * 
  * @Title: writeTo
  * @Description: 使用apache的commons-fileupload组件获取、保存上传的文件(本项目中并未使用)
  * @param path
  * @param request
  * @param response
  * @throws IOException
  */
 private void writeTo(String path, HttpServletRequest request, HttpServletResponse response) throws IOException {
  DiskFileItemFactory factory = new DiskFileItemFactory();
  factory.setRepository(new File(path));
  // 设置 缓存的大小,当上传文件的容量超过该缓存时,直接放到 暂时存储室
  factory.setSizeThreshold(1024 * 1024);
  ServletFileUpload upload = new ServletFileUpload(factory);
  try {
   // 可以上传多个文件
   List<fileitem> list = (List<fileitem>) upload.parseRequest(request);
   for (FileItem item : list) {
    // 获取表单的属性名字
    String name = item.getFieldName();
    // 如果获取的 表单信息是普通的 文本 信息
    if (item.isFormField()) {
     // 获取用户具体输入的字符串 ,名字起得挺好,因为表单提交过来的是 字符串类型的
     String value = item.getString();
     request.setAttribute(name, value);
    }
    // 对传入的非 简单的字符串进行处理 ,比如说二进制的 图片,电影这些
    else {
     /**
      * 以下三步,主要获取 上传文件的名字
      */
     // 获取路径名
     String value = item.getName();
     // 索引到最后一个反斜杠
     int start = value.lastIndexOf("\\");
     // 截取 上传文件的 字符串名字,加1是 去掉反斜杠,
     String filename = value.substring(start + 1);
     request.setAttribute(name, filename);

     // 真正写到磁盘上
     // 它抛出的异常 用exception 捕捉

      //item.write( new File(path,filename) );//第三方提供的

     // 手动写的
     OutputStream out = new FileOutputStream(new File(path,
       filename));
     System.out
       .println("path:" + path + "\nfilname:" + filename);
     InputStream in = item.getInputStream();
     int length = 0;
     byte[] buf = new byte[1024];
     System.out.println("获取上传文件的总共的容量:" + item.getSize());
     // in.read(buf) 每次读到的数据存放在 buf 数组中
     while ((length = in.read(buf)) != -1) {
      // 在 buf 数组中 取出数据 写到 (输出流)磁盘上
      out.write(buf, 0, length);
     }
     in.close();
     out.close();
    }
   }
   response.sendRedirect(request.getContextPath() + "/success.html");
  }
  catch (FileUploadException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
   PrintWriter out = response.getWriter();
   out.write("Failed!");
   out.flush();
   out.close();
  }
 }
}

没有评论:

发表评论