web 編程中實現文件上傳的服務端實例

      在文章《用C++實現相似於JAVA HttpServlet 的編程接口 》中講了如何用 acl_cpp 的 HttpServlet 等類來實現 WEB CGI 的功能,同時在文章《使用 acl_cpp 的 HttpServlet 類及服務器框架編寫WEB服務器程序 》中也舉例說明如何將基於 HttpServlet 編寫的 CGI 程序快速地轉爲服務器程序的過程。本文主要講如何用 acl_cpp 的 WEB 編程類實現 HTTP 文件上傳過程。爲了實現 HTTP 協議的文件上傳過程,引入了兩個類:http_mime 和 http_mime_node。html

      http_mime 類是有關 HTTP 協議中 mime 格式的流式解析器(即每次僅輸入部分 HTTP MIME 數據,等數據輸入完畢時,該解析器也解析完畢,流式解析的好處是它能夠適用於阻塞或非阻塞的IO模式);http_mime_node 類對象表示 http mime 數據中每個 mime 結點對象,該結點的數據多是文件內容數據,也多是參數數據。node

 

      1、http_mime 類編程

      該類通常由 HttpServletRequest 類內部自動管理(負責分配與釋放 http_mide 類對象),固然用戶能夠在測試 http_mime 類時,本身建立與釋放該類對象。下面是該類的構造函數及經常使用方法:服務器

 

/**
		 * 構建函數
		 * @param boundary {const char*} 分隔符,不能爲空
		 * @param local_charset {const char*} 本地字符集,非空時會自動將
		 *  參數內容轉爲本地字符集
		 */
		http_mime(const char* boundary, const char* local_charset  = "gb2312");

 

      尤爲須要指出的是 http mime 的 boundary(分隔符)與郵件的 mime 的分隔符規則略有不一樣,如郵件的相關頭部字段爲:Content-Type: multipart/mixed; charset="GB2312"; boundary="0_11119_1331286082",HTTP MIME 的相關頭部字段爲:Content-Type: multipart/form-data; boundary="--0_11119_1331286082"。其中,最大的區別就是在 HTTP 頭中得到的分隔符與 HTTP 數據體的分隔符(除結尾分隔符多了兩個 '-' 後綴)徹底相同,而郵件的 mime 的分隔符在頭部和 mime 體中是不同的,mime 體中的分隔符是由頭部的分隔符加兩個 '-' 做爲前導符(結尾分隔符爲頭部分隔符前面加兩個 '-',尾部加兩個 '-'),必定得注意這些不一樣。在 acl_cpp 中的 http mime 解析模塊原來主要是做郵件 mime 解析的,如今依然支持 HTTP 的 mime 解析,惟一不一樣就是區分分隔符的不一樣。(固然,郵件的 MIME 數據體還與 HTTP MIME 數據體有另一個區別:郵件的 MIME 數據通常都是要通過 BASE64 來編碼的,而 HTTP MIME 卻不多編碼)。框架

 

      http_mime 的幾個經常使用方法接口以下:ide

 

/**
		 * 設置 MIME 數據的存儲路徑,當分析完 MIME 數據後,若是想要
		 * 從中提取數據,則必須給出該 MIME 的原始數據的存儲位置,不然
		 * 沒法得到相應數據,即 save_xxx/get_nodes/get_node 函數均沒法
		 * 正常使用
		 * @param path {const char*} 文件路徑名, 若是該參數爲空, 則不能
		 *  得到數據體數據, 也不能調用 save_xxx 相關的接口
		 */
		void set_saved_path(const char* path);

		/**
		 * 調用此函數進行流式方式解析數據體內容
		 * @param data {const char*} 數據體(多是數據頭也多是數據體, 
		 *  而且沒必要是完整的數據行)
		 * @param len {size_t} data 數據長度
		 * @return {bool} 針對 multipart 數據, 返回 true 表示解析完畢;
		 *  對於非 multipart 文件, 該返回值永遠爲 false, 沒有任何意義, 
		 *  須要調用者本身判斷數據體的結束位置
		 * 注意: 調用完此函數後必定須要調用 update_end 函數通知解析器
		 * 解析完畢
		 */
		bool update(const char* data, size_t len);

		/**
		 * 得到全部的 MIME 結點
		 * @return {const std::list<http_mimde_node*>&}
		 */
		const std::list<http_mime_node*>& get_nodes(void) const;

		/**
		 * 根據變量名取得 HTTP MIME 結點
		 * @param param name {const char*} 變量名
		 * @return {http_mime_node*} 返回空則說明對應變量名的結點
		 *  不存在
		 */
		const http_mime_node* get_node(const char* name) const;

 

 

      2、http_mime_node 類memcached

      該類實例存儲 HTTP MIME 數據體中每一個數據結點,同時該類的實例是由 http_mime 類對象自動維護的,因此您通常沒必要關心該類對象的建立與銷燬;另外,http_mime_node 類的繼承關係爲:http_mime_node -> mime_attach -> mime_node。函數

      該類的構造函數以下:測試

 

/**
		 * 原始文件存放路徑,不能爲空
		 * @param node {MIME_NODE*} 對應的 MIME 結點,非空
		 * @param decodeIt {bool} 是否對 MIME 結點的頭部數據
		 *  或數據體數據進行解碼
		 * @param toCharset {const char*} 本機的字符集
		 * @param off {off_t} 偏移數據位置
		 */
		http_mime_node(const char* path, const MIME_NODE* node,
			bool decodeIt = true, const char* toCharset = "gb2312",
			off_t off = 0);

 

 

      該類的經常使用方法爲:ui

 

/**
		 * 得到該結點的類型
		 * @return {http_mime_t}
		 */
		http_mime_t get_mime_type(void) const;

		/**
		 * 當 get_mime_type 返回的類型爲 HTTP_MIME_PARAM 時,能夠
		 * 調用此函數得到參數值;參數名能夠經過基類的 get_name() 得到
		 * @return {const char*} 返回 NULL 表示參數不存在
		 */
		const char* get_value(void) const;

 

       http_mime_t 爲枚舉類型,如:

 

typedef enum
	{
		HTTP_MIME_PARAM,        // http mime 結點爲參數類型
		HTTP_MIME_FILE          // http mime 結點爲文件類型
	} http_mime_t;

 

      加上兩個基類的一些方法,有幾個方法也是比較經常使用的,以下:

 

 mime_node::get_name: 得到該 mime 結點的名稱

 mime_attach::get_filename: 當結點爲上傳文件類型時,此函數得到上傳文件的文件名

 

      3、示例

 

#include "lib_acl.hpp"

using namespace acl;

class http_servlet : public HttpServlet
{
public:
	http_servlet()
	{
		...
	}

	...
	// 基類虛方法:HTTP POST 方法接口
	virtual bool doPost(HttpServletRequest& req, HttpServletResponse& res)
	{
		...
		return doUpload(req, res);
	}

	// 處理文件上傳的函數
	bool doUpload(HttpServletRequest& req, HttpServletResponse& res)
	{
		// 先得到 Content-Type 對應的 http_ctype 對象
		http_mime* mime = req.getHttpMime();
		if (mime == NULL)
		{
			logger_error("http_mime null");
			return false;
		}

		// 得到數據體的長度
		long long int len = req.getContentLength();
		if (len <= 0)
		{
			logger_error("body empty");
			return false;
		}

		// 得到輸入流
		istream& in = req.getInputStream();
		char  buf[8192];
		int   ret;
		bool  n = false;

		const char* filepath = "./var/mime_file";
		ofstream out;
		// 只寫方式打開存儲上傳文件的臨時文件句柄
		out.open_write(filepath);

		// 設置原始文件存入路徑
		mime->set_saved_path(filepath);

		// 讀取 HTTP 客戶端請求數據
		while (len > 0)
		{
			// 從 HTTP 輸入流中讀取數據
			ret = in.read(buf, sizeof(buf), false);
			if (ret == -1)
			{
				logger_error("read POST data error");
				return false;
			}
			// 將數據寫入臨時文件中
			out.write(buf, ret);
			len -= ret;

			// 將讀獲得的數據輸入至解析器進行解析
			if (mime->update(buf, ret) == true)
			{
				n = true;
				break;
			}
		}
		out.close();

		if (len != 0 || n == false)
			logger_warn("not read all data from client");

		string path;

		// 遍歷全部的 MIME 結點,找出其中爲文件結點的部分進行轉儲
		const std::list<http_mime_node*>& nodes = mime->get_nodes();
		std::list<http_mime_node*>::const_iterator cit = nodes.begin();
		for (; cit != nodes.end(); ++cit)
		{
			// HTTP MIME 結點的變量名
			const char* name = (*cit)->get_name();

			// HTTP MIME 結點的類型
			http_mime_t mime_type = (*cit)->get_mime_type();
			if (mime_type == HTTP_MIME_FILE)
			{
				// 當該結點爲文件數據結點時
				// 取得上傳文件名
				const char* filename = (*cit)->get_filename();
				if (filename == NULL)
				{
					logger("filename null");
					continue;
				}

				if (strcmp(name, "file1") == 0)
					file1_ = filename;
				else if (strcmp(name, "file2") == 0)
					file2_ = filename;
				else if (strcmp(name, "file3") == 0)
					file3_ = filename;

				// 將文件內容轉存
				path.format("./var/%s", filename);
				(void) (*cit)->save(path.c_str());
			}
		}

		// 查找上載的某個文件並轉儲
		const http_mime_node* node = mime->get_node("file1");
		if (node && node->get_mime_type() == HTTP_MIME_FILE)
		{
			const char* ptr = node->get_filename();
			if (ptr)
			{
				path.format("./var/1_%s", ptr);
				(void) node->save(path.c_str());
			}
		}

		// 刪除臨時文件
		:unlink(filepath);

		// 發送 http 響應頭
		if (res.sendHeader() == false)
			return false;
		// 發送 http 響應體
		if (res.getOutputStream().write("ok") == -1)
			return false;
		return true;
	}

private:
	const char* file1_;
	const char* file2_;
	const char* file3_;
};

int main(void)
{
#ifdef WIN32
	acl::acl_cpp_init();
#endif

	// 開始運行
	http_servlet servlet;
	servlet.doRun("127.0.0.1:11211"); // 開始運行,並假設 memcached 監聽於 127.0.0.1:11211
	return 0;
}

 

      與上面例子對應的 HTML 頁面以下:

<html>
<head>
<meta content="text/html; charset=gb2312" http-equiv="Content-Type">
</head>
<body>
<form enctype="multipart/form-data" method=POST action="/cgi-bin/test/upload?name1=中國人">
<input type=hidden name="name2" value="美國人"><br>
<input type=hidden name="name3" value="英國人"><br>
<input type=submit name="submit", value="提交"><br>
文件一:<input type=file name="file1" value=""><br>
文件二:<input type=file name="file2" value=""><br>
文件三:<input type=file name="file3" value=""><br>
</form>
</body>
</html>

 

      上面例子比較簡單地說明了若是使用 acl_cpp 中的 HttpServlet/http_mime 等類來實現文件上傳的功能,完整的例子請參考:acl_cpp/samples/cig_upload。該例子雖然是一個 CGI 程序,但您依然能夠不費吹灰之力將其改變成一個服務器程序,轉換方法可參考:《使用 acl_cpp 的 HttpServlet 類及服務器框架編寫WEB服務器程序 》。

 

 

      我的微博:http://weibo.com/zsxxsz

       原文地址

       acl_cpp 下載

       acl_cpp 的編譯與使用

       更多文章

       bbs:http://www.aclfans.com

相關文章
相關標籤/搜索