文件上傳的祕密(一)造自己的工具

RFC1867文檔對WEB表單上傳文件做了詳細的描述,但J2EE的Servlet規範中卻沒有針對此功能規定一個API,沒有接口也沒有抽象類,更不要說一個具體類了。幸好,著名的開源組織Apache的官網上有一個Common File Upload這個項目,給廣大的J2EE開發者解決了這個比較麻煩的問題。會用Common File Upload這個開源組件解決表單文件上傳問題是一回事,能知道這個組件的優缺點是另外一回事,如果能知道RFC1867文檔中對於表單上傳文件的規定、並實現文件上傳的功能,是另外一回事。


幹嘛幹嘛,你這不是閒的蛋疼嘛,有現成的輪子不用,非要再造一個相同的輪子呢?聽起來很有道理,可是呢,只能這麼說,自行車輪子是不裝在汽車上的,福特車的車輪子裝不上奧迪車的,爲了造出最好的車,就得要親子動手再造一個最合適的輪子,寫軟件也是同樣的道理。另外,‭ ‬作爲一個想成爲優秀程序員的人來說,決不會因爲使用了Common File Upload而感到自豪。掌握某種技術的原理,並有所創新,纔是程序員的王道。本文就是要從零開始,實現文件上傳。


好了,廢話了這麼多,就先看看這個表單上傳文件究竟是個什麼東東。RFC1867文檔規定了HTTP表單上傳文件的方式,簡明扼要的說,表單的ENCTYPE必須是multipart/form-data,‭ ‬必須是POST方式提交。每個文件的內容經瀏覽器編碼後,使用一個不會和文件內容重複的串把多個文件或者輸入域分割開來,這個串叫boundary。爲了便於理解RFC1867文檔對於文件上傳時,瀏覽器是如何對文件編碼的,我們把一個含有文件輸入和文本輸入的表單,經瀏覽器編碼後,發送到服務器端的請求dump下來,看看究竟。

-----------------------------168072824752491622650073

Content-Disposition: form-data; name="_file1"; filename="Image023.jpg"

Content-Type: image/jpeg



ˇÿˇ‡  JFIF          ˇ·
çExif  II*       (                           :           I
      ˇÿˇ€ C
…
…

-----------------------------168072824752491622650073

Content-Disposition: form-data; name="_text1.text1"



some text in mulitpart form

-----------------------------168072824752491622650073--
 


經瀏覽器編碼後,請求的內容被boundary分割,如果是文件,Content‭-‬Disposition內容中會有文件的名字,緊跟其後,Content‭-‬Type內容包含了文件類型的信息,如果只是一個文本輸入域,在boundary後,既沒有文件名,也沒有Content‭-‬Type內容,當然,文本輸入域是肯定不會有文件名的。請求內容的最後,一個boundary串加上兩個”-”,表示表單的內容結束。其實,RFC1867文檔規定表單文件上傳的內容比這個要複雜、詳細,有興趣可以參考官方文檔和百度文庫中的文檔。
http‭://‬www.ietf.org/rfc/rfc1867‭.‬txt
http‭://‬wenku.baidu.com/view/3438982458fb770bf78a5573‭.‬html


在初步搞清楚表單文件上傳編碼後內容,其實接下來的問題比較明確,就是根據boundary串,分割請求內容,解析出上傳文件的內容。是的,這是一條正確的線路,然,事情就是如此想象的簡單?
雖然可以在請求header中首先拿到boundary,但是boundary並沒有告訴開發者,一個文件內容的區塊是從哪裏開始,到哪裏結束。


先廢話一句,經編碼後的內容是二進制的,把二進制的內容轉化成字符串,再查找boundary串,這種做法的效率是有問題的,而且,java的字符串處理的效率本身就不高,所以,此路不通。那如何在一大塊二進制的數據中查找到一小塊連續的二進制數據呢?‭ ‬貌似沒有現成的方法,經過考量,發現字符串查找的方法可以借鑑,字符串本質也是二進制,用字符查找算法來查找二進制的內容理論上不存在問題。‭ ‬


針對boundary本身內容的特性--基本無重複和中等長度,這裏選擇Boyer和Moore在80年代發明的字符串查找算法,簡稱BM算法,BM算法的複雜度是O(M+N‭)‬,效率極高,不過BM查找算法是針對ASCII碼中那些常見的字母和一些符號,而這裏是要查找二進制,有一些區別,需要對原查算法稍作改進,以後再專門寫一篇博客詳細說明,現在的假定情況是改進的BM算法能在一大塊的二進制數據區內的任意開始位置查找一個特定的小的二進制數據塊,如果能查找到,返回該數據塊出現的起始位置,否則返回-1,這個函數用靜態的方法來表示,

/**
	 * Returns the index within this string of the first occurrence of the
	 * specified substring. If it is not a substring, return -1.
	 * 
	 * @param text
	 *            The string to be scanned
	 * @param word
	 *            The target string to search
	 * @return The start index of the substring
	 */

public static int indexOf(byte[] text, byte[] word, int start)
 


有了這個強力的算法後,接下來的事情開始變得簡單,需要做的事情只有三點
1)    拿到boundary的值,查找這個boundary在整個提交上來的請求中的位置
2)    根據boundary後的一些內容,把識別爲文件的數據塊寫入到文件中
3)    重複這個過程直到請求的輸入流結束

爲了扶助這個過程的順利進行,需要定義一個表示上傳文件的類,這裏也叫MultipartFile,當初始化該類的實例時,會新建一個文件輸出流,用來寫入從上傳請求輸入流讀取的文件數據塊,同時該類也提供關閉這個文件輸出流的方法。

class MultiPartFile {
	private String name;
	private int start, end;

	public MultiPartFile(String name) throws IOException {
		super();
		this.name = name;
		fos = new FileOutputStream(name);
	}

	public void append(byte[] buff, int off, int len) throws IOException {
		fos.write(buff, off, len);
	}

	public void append(byte[] buff) throws IOException {
		fos.write(buff);
	}

	public void close() throws IOException {
		fos.flush();
		fos.close();
	}
}

 

FileUploadPharser是真正執行對請求數據分析上傳文件的類,在parse方法中,分析Request請求後,並返回MultiPartFile數組。該類的主要代碼如下

public class FileUploadParser {

	private static final String _ENCTYPE = "multipart/form-data";
	private static final String _FILE_NAME_KEY = "filename";

	private static final byte[] _CTRF = { 0X0D, 0X0A };
	
	private int bufferSize = 0x20000;

	private byte[] boundary;

	private HttpServletRequest request;

	private String dir;
	
	private String encoding;

	public FileUploadParser(HttpServletRequest request, String dir) {
		this.request = request;
		this.dir = dir;
	}

        public List<MultiPartFile> parse() throws IOException {
		List<MultiPartFile> files = new ArrayList<MultiPartFile>();
		this.parseEnctype();
		byte[] buffer = new byte[bufferSize];
		int c = 0;
		boolean hasFile = false;
		boolean end = true;
		while ((c = request.getInputStream().read(buffer)) != -1) {
			boolean isNewSegment = true;
			int index = 0;
			while ((index = BoyerMoore.indexOf(buffer, boundary, index)) != -1) {
				if (end) {
					MultiPartFile mpf = parseFile(buffer, index);
					if (mpf != null) {
						files.add(mpf);
						index = mpf.getStart();
						end = false;
						hasFile = true;
					} else {
						hasFile = false; 
						end = true;
						index += boundary.length;
					}
				} else if (hasFile) {
					// write buffer to last opening file if current index identifies the start of boundary.
					// and close the file.
					MultiPartFile writer = files.get(files.size() - 1);
					if (isNewSegment) {
						writer.append(buffer, 0, index - 4);
					} else {
						int off = writer.getStart();
						writer.append(buffer, off, index - off - 4);
					}
					writer.close();

					// start a new parse action
					MultiPartFile next = parseFile(buffer, index);
					if (next != null) {
						files.add(next);
						index = next.getStart();
						end = false;
						hasFile = true;
					}
					else {
						hasFile = false;
						end = true;
						index += boundary.length;
					}

				}
				isNewSegment = false;

				/*
				 * // create a new MultiPartFile object if found the boundary //
				 * firstly if (files.size() == 0) { MultiPartFile mpf =
				 * parseFile(buffer, index); if (mpf != null) { files.add(mpf);
				 * index = mpf.getStart(); newSegment = false; hasFile = true;
				 * end = false; } else { hasFile = false; continue; // skip next
				 * boundary } } // append the buffer into exists MultiPartFile
				 * object and // then parse next part of content else {
				 * MultiPartFile last = files.get(files.size() - 1); if (hasFile
				 * && newSegment) { last.append(buffer, 0, index - 4);
				 * last.close(); end = true; } else if (hasFile) { int s =
				 * last.getStart(); last.append(buffer, s, index - s - 4);
				 * last.close(); end = true; } else { continue; } newSegment =
				 * false; hasFile = false; index += boundary.length;
				 * 
				 * MultiPartFile next = parseFile(buffer, index); if (next !=
				 * null) { files.add(next); index = next.getStart(); hasFile =
				 * true; } else { hasFile = false; continue; // skip next
				 * boundary } }
				 */

			}
			// not found boundary, append the buffer into file
			if (!end) {
				MultiPartFile writer = files.get(files.size() - 1);
				if (isNewSegment) {
					writer.append(buffer, 0, c);
				} else {
					int off = writer.getStart();
					writer.append(buffer, off, c - off);
				}
			}
		}
		return files;
	}
 ....
// 其他輔助函數略
 }
 

至此,解析上傳文件內容並保存的工作就完成了,但是事情還是沒有結束, 瀏覽器在向服務器端發送數據時,會對發送的內容進行編碼,這些編碼的內容需要一個解碼的過程,特別是需要處理中文的web應用。


<原創內容,版權所有,如若轉載,請註明出處,不勝感謝!儀山湖>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章