在文章《用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服務器程序 》。