以前寫過幾篇如何使用 acl 庫來實現 HTTP 客戶端的例子都是基於 C 語言(使用 acl 較爲底層的 HTTP 協議庫寫 HTTP 下載客戶端舉例, 使用 acl 庫開發一個 HTTP 下載客戶端),其實在 acl 的 C++ 庫(lib_acl_cpp) 中 HTTP 類功能更爲強大,本節將介紹如何使用 acl::http_request 類來寫一些簡單的 HTTP 客戶端示例。node
1、 acl::http_request 類的一些經常使用接口git
該 HTTP 請求類有兩個構造函數,以下 :github
/** * 構造函數:經過該構造函數傳入的 socket_stream 流對象並 * 不會被關閉,須要調用者本身關閉 * @param client {socket_stream*} 數據鏈接流,非空, * 在本類對象被銷燬時該流對象並不會被銷燬,因此用戶需自行釋放 * @param conn_timeout {int} 若是傳入的流關閉,則內部會 * 自動重試,此時須要該值表示鏈接服務器的超時時間(秒), * 至於重連流的 IO 讀寫超時時間是從 輸入的流中繼承的 * @param unzip {bool} 是否對服務器響應的數據自動進行解壓 * 注:當該類實例被屢次使用時,用戶應該在每次調用前調用 * request_header::http_header::reset() */ http_request(socket_stream* client, int conn_timeout = 60, bool unzip = true); /** * 構造函數:該構造函數內部建立的 socket_stream 流會自行關閉 * @param addr {const char*} WEB 服務器地址 * @param conn_timeout {int} 遠程鏈接服務器超時時間(秒) * @param rw_timeout {int} IO 讀寫超時時間(秒) * @param unzip {bool} 是否對服務器響應的數據自動進行解壓 */ http_request(const char* addr, int conn_timeout = 60, int rw_timeout = 60, bool unzip = true);
第一個是以已經鏈接成功的套接字流爲參數的構造函數,該構造函數把鏈接 HTTP 服務器的工做交給用戶來完成;第二個是以 HTTP 服務器地址爲參數的構造函數,使用該構造函數,則該類對象內部會自動鏈接 HTTP 服務器。web
下面的幾個函數接口與 HTTP 發送相關:json
/** * 得到 HTTP 請求頭對象,而後在返回的 HTTP 請求頭對象中添加 * 本身的請求頭字段或 http_header::reset()重置請求頭狀態, * 參考:http_header 類 * @return {http_header&} */ http_header& request_header(void); /** * 向 HTTP 服務器發送 HTTP 請求頭及 HTTP 請求體,同時從 * HTTP 服務器讀取 HTTP 響應頭,對於長鏈接,當鏈接中斷時 * 會再重試一次,在調用下面的幾個 get_body 函數前必須先 * 調用本函數(或調用 write_head/write_body); * 正常狀況下,該函數在發送完請求數據後會讀 HTTP 響應頭, * 因此用戶在本函數返回 true 後能夠調用:get_body() 或 * http_request::get_clinet()->read_body(char*, size_t) * 繼續讀 HTTP 響應的數據體 * @param data {const void*} 發送的數據體地址,非空時自動按 * POST 方法發送,不然按 GET 方法發送 * @param len {size_} data 非空時指定 data 數據長度 * @return {bool} 發送請求數據及讀 HTTP 響應頭數據是否成功 */ bool request(const void* data, size_t len); /** * 當採用流式寫數據時,須要首先調用本函數發送 HTTP 請求頭 * @return {bool} 是否成功,若是成功才能夠繼續調用 write_body */ bool write_head(); /** * 當採用流式寫數據時,在調用 write_head 後,能夠循環調用本函數 * 發送 HTTP 請求體數據;當輸入的兩個參數爲空值時則表示數據寫完; * 當發送完數據後,該函數內部會自動讀取 HTTP 響應頭數據,用戶可 * 繼續調用 get_body/read_body 獲取 HTTP 響應體數據 * @param data {const void*} 數據地址指針,當該值爲空指針時表示 * 數據發送完畢 * @param len {size_t} data 非空指針時表示數據長度 * @return {bool} 發送數據體是否成功 * 注:當應用發送完數據後,必須再調用一次本函數,同時將兩個參數都賦空 */ bool write_body(const void* data, size_t len);
構建及發送 HTTP 請求的過程以下:服務器
一、使用兩個構造函數之一建立 acl::http_request 請求對象cookie
二、調用 http_request::request_header 得到 HTTP 請求頭對象的引用(http_header&),而後對該 HTTP 請求頭設置 HTTP 請求的參數dom
三、http_request 類提供了兩種 HTTP 請求調用 方式:socket
3.一、當 HTTP 請求方法爲 HTTP GET 方法或爲 HTTP POST 但數據體能夠一次性寫入時,可使用 http_request::request 方法,在調用 http_request::request 時會將 HTTP 請求頭及請求體一次性發給 HTTP 服務器;svn
3.2 若是爲 HTTP POST 請求方法,且 HTTP 數據體內容是流式的(即每次只是要發送部分數據),則應該使用 http_request::write_head 和 http_request::write_body 兩個函數,即便用流式方式發送數據時,應首先調用 http_request::write_head 發送 HTTP 請求頭,當該函數返回成功後,能夠循環調用 http_request::write_body 來發送 HTTP 請求數據體,爲了表示 HTTP 請求體數據完畢,必須最後調用一次 http_request::write_body 且兩個參數爲 0 時以表示數據體發送完畢。
在調用以上 3.1 或 3.2 過程成功發送完 HTTP 請求數據後,這兩個過程內部會自動讀取 HTTP 服務器發來的 HTTP 響應頭。
在上面的步驟 2 得到 HTTP 請求頭對象(http_header)後,應該先調用下面的方法設置 HTTP 請求頭中的參數:
/** * 設置請求的 URL,url 格式示例以下: * 一、http://www.test.com/ * 二、/cgi-bin/test.cgi * 三、http://www.test.com/cgi-bin/test.cgi * 三、http://www.test.com/cgi-bin/test.cgi?name=value * 四、/cgi-bin/test.cgi?name=value * 五、http://www.test.com * 若是該 url 中有主機字段,則內部自動添加主機; * 若是該 url 中有參數字段,則內部自動進行處理並調用 add_param 方法; * 調用該函數後用戶仍能夠調用 add_param 等函數添加其它參數; * 當參數字段只有參數名沒有參數值時,該參數將會被忽略,因此若是想 * 單獨添加參數名,應該調用 add_param 方法來添加 * @param url {const char*} 請求的 url,非空指針 * @return {http_header&} 返回本對象的引用,便於用戶連續操做 */ http_header& set_url(const char* url); /** * 設置 HTTP 請求頭的 HOST 字段 * @param value {const char*} 請求頭的 HOST 字段值 * @return {http_header&} 返回本對象的引用,便於用戶連續操做 */ http_header& set_host(const char* value); /** * 向請求的 URL 中添加參數對,當只有參數名沒有參數值時則: * 一、參數名非空串,但參數值爲空指針,則 URL 參數中只有:{name} * 二、參數名非空串,但參數值爲空串,則 URL參數中爲:{name}= * @param name {const char*} 參數名,不能爲空指針 * @param value {const char*} 參數值,當爲空指針時,僅添加參數名, * @return {http_header&} 返回本對象的引用,便於用戶連續操做 */ http_header& add_param(const char* name, const char* value); http_header& add_int(const char* name, short value); http_header& add_int(const char* name, int value); http_header& add_int(const char* name, long value); http_header& add_int(const char* name, unsigned short value); http_header& add_int(const char* name, unsigned int value); http_header& add_int(const char* name, unsigned long value); http_header& add_format(const char* name, const char* fmt, ...) ACL_CPP_PRINTF(3, 4); /** * 向 HTTP 頭中添加 cookie * @param name {const char*} cookie 名 * @param value {const char*} cookie 值 * @param domain {const char*} 所屬域 * @param path {const char*} 存儲路徑 * @param expires {time_t} 過時時間,當該值爲 0 時表示不過時, * > 0 時,則從如今起再增長 expires 即爲過時時間,單位爲秒 * @return {http_header&} 返回本對象的引用,便於用戶連續操做 */ http_header& add_cookie(const char* name, const char* value, const char* domain = NULL, const char* path = NULL, time_t expires = 0); /** * 設置 HTTP 頭中的 Connection 字段,是否保持長鏈接 * 不過,目前並未真正支持長鏈接,即便設置了該標誌位, * 則獲得響應數據後也會主動關閉鏈接 * @param on {bool} 是否保持長鏈接 * @return {http_header&} 返回本對象的引用,便於用戶連續操做 */ http_header& set_keep_alive(bool on); /** * 設置 HTTP 頭中的 Content-Length 字段 * @param n {long long int} 設置值 * @return {http_header&} 返回本對象的引用,便於用戶連續操做 */ http_header& set_content_length(long long int n); /** * 設置 HTTP 頭中的 Content-Type 字段 * @param value {const char*} 設置值 * @return {http_header&} 返回本對象的引用,便於用戶連續操做 */ http_header& set_content_type(const char* value);
以上僅列出了 http_header 類設置 HTTP 請求參數的一些經常使用方法,其它的方法請參考 http_header.hpp 頭文件中的說明。
2、acl::http_request 類得到 HTTP 服務器響應數據的經常使用方法
上面介紹了使用 acl::http_request 構建 HTTP 請求頭及發送請求的接口方法,下面介紹使用 acl::http_request 類中的方法來接收 HTTP 服務器響應過程,在調用 http_request 類中的 request 或 write_body 成功發送完請求數據後,該類對象在這兩個方法內部會首先自動接收 HTTP 服務器的響應頭數據,若接收過程失敗,這兩個方法也會返回 false 表示失敗,若返回成功,則能夠調用 http_request 類對象的 http_status 方法得到 HTTP 服務器的響應狀態碼(2xx, 3xx, 4xx, 5xx),還可調用 body_length 方法得到 HTTP 響應數據體的長度(當 HTTP 服務器返回的數據格式爲 HTTP 塊傳輸時,該函數會返回 -1,因此通常不用顯示調用該方法)。下面介紹了主要的與 HTTP 響應相關的方法:
首先是與 HTTP 響應頭相關的接口函數,以下:
/** * 當發送完請求數據後,內部會自動調用讀 HTTP 響應頭過程,能夠經過此函數得到服務端 * 響應的 HTTP 狀態字(2xx, 3xx, 4xx, 5xx); * 其實該函數內部只是調用了 http_client::response_status 方法 * @return {int} */ int http_status() const; /** * 得到 HTTP 響應的數據體長度 * @return {int64) 返回值若爲 -1 則代表 HTTP 頭不存在或沒有長度字段 */ #ifdef WIN32 __int64 body_length(void) const; #else long long int body_length(void) const; #endif /** * HTTP 數據流(響應流是否容許保持長鏈接) * @return {bool} */ bool keep_alive(void) const; /** * 得到 HTTP 響應頭中某個字段名的字段值 * @param name {const char*} 字段名 * @return {const char*} 字段值,爲空時表示不存在 */ const char* header_value(const char* name) const; /** * 得到服務器返回的 Set-Cookie 設置的某個 cookie 對象 * @param name {const char*} cookie 名 * @param case_insensitive {bool} 是否區分大小寫,true 表示 * 不區分大小寫 * @return {const HttpCookie*} 返回 NULL 表示不存在 */ const HttpCookie* get_cookie(const char* name, bool case_insensitive = true) const;
而後是與讀 HTTP 響應數據體相關的接口函數:
/** * 是否讀完了數據體 * @return {bool} */ bool body_finish() const; /** * 當調用 request 成功後調用本函數,讀取服務器響應體數據 * 並將結果存儲於規定的 xml 對象中 * @param out {xml&} HTTP 響應體數據存儲於該 xml 對象中 * @param to_charset {const char*} 當該項非空,內部自動 * 將數據轉成該字符集存儲於 xml 對象中 * @return {bool} 讀數據是否成功 * 注:當響應數據體特別大時不該用此函數,以避免內存耗光 */ bool get_body(xml& out, const char* to_charset = NULL); /** * 當調用 request 成功後調用本函數,讀取服務器響應體數據 * 並將結果存儲於規定的 json 對象中 * @param out {json&} HTTP 響應體數據存儲於該 json 對象中 * @param to_charset {const char*} 當該項非空,內部自動 * 將數據轉成該字符集存儲於 json 對象中 * @return {bool} 讀數據是否成功 * 注:當響應數據體特別大時不該用此函數,以避免內存耗光 */ bool get_body(json& out, const char* to_charset = NULL); /* * 當調用 request 成功後調用本函數,讀取服務器所有響應數據 * 存儲於輸入的緩衝區中 * @param out {string&} 存儲響應數據體 * @param to_charset {const char*} 當該項非空,內部自動 * 將數據轉成該字符集存儲於 out 對象中 * 注:當響應數據體特別大時不該用此函數,以避免內存耗光 */ bool get_body(string& out, const char* to_charset = NULL); /* * 當調用 request 成功後調用本函數,讀取服務器響應數據並 * 存儲於輸入的緩衝區中,能夠循環調用本函數,直至數據讀完了, * @param buf {char*} 存儲部分響應數據體 * @param size {size_t} buf 緩衝區大小 * @return {int} 返回值 == 0 表示正常讀完畢,< 0 表示服務器 * 關閉鏈接,> 0 表示已經讀到的數據,用戶應該一直讀數據直到 * 返回值 <= 0 爲止 * 注:該函數讀到的是原始 HTTP 數據體數據,不作解壓和字符集 * 解碼,用戶本身根據須要進行處理 */ int read_body(char* buf, size_t size); /** * 當調用 request 成功後調用本函數讀 HTTP 響應數據體,能夠循環調用 * 本函數,本函數內部自動對壓縮數據進行解壓,若是在調用本函數以前調用 * set_charset 設置了本地字符集,則還同時對數據進行字符集轉碼操做 * @param out {string&} 存儲結果數據 * @param clean {bool} 每次調用本函數時,是否要求先自動將緩衝區 out * 的數據清空 * @param real_size {int*} 當該指針非空時,存儲解壓前讀到的真正數據 * 長度,若是在構造函數中指定了非自動解壓模式且讀到的數據 > 0,則該 * 值存儲的長度值應該與本函數返回值相同;當讀出錯或未讀到任何數據時, * 該值存儲的長度值爲 0 * @return {int} == 0 表示讀完畢,可能鏈接並未關閉;>0 表示本次讀操做 * 讀到的數據長度(當爲解壓後的數據時,則表示爲解壓以後的數據長度, * 與真實讀到的數據不一樣,真實讀到的數據長度應該經過參數 real_size 來 * 得到); < 0 表示數據流關閉,此時若 real_size 非空,則 real_size 存 * 儲的值應該爲 0 */ int read_body(string& out, bool clean = false, int* real_size = NULL); /** * 當調用 request 成功後調用本函數來從 HTTP 服務端讀一行數據,能夠循環調用 * 本函數,直到返回 false 或 body_finish() 返回 true 爲止; * 本函數內部自動對壓縮數據進行解壓,若是在調用本函數以前調用 set_charset 設置了 * 本地字符集,則還同時對數據進行字符集轉碼操做 * @param out {string&} 存儲結果數據 * @param nonl {bool} 讀到的一行數據是否自動去掉尾部的 "\r\n" 或 "\n" * @param size {size_t*} 該指針非空時存放讀到的數據長度 * @return {bool} 是否讀到了一行數據:當返回 true 時表示讀到了一行數據,能夠 * 經過 body_finish() 是否爲 true 來判斷是否讀數據體已經結束,當讀到一個空行 * 且 nonl = true 時,則 *size = 0;當返回 false 時表示未讀完整行且讀完畢, * *size 中存放着讀到的數據長度 */ bool body_gets(string& out, bool nonl = true, size_t* size = NULL);
雖然上面提供了多個讀 HTTP 響應體數據的方法,但能夠分爲兩大類:一、一次性讀全部的數據體;二、以流式方式循環讀數據體。 其中,對於「一次性讀取全部數據體」的讀方法,適合於響應數據體比較小的情形,當響應數據爲 xml 或 json 格式時,還提供了直接將響應數據體轉爲 xml 或 json 對象的讀方法;若是響應數據體很是大(如幾兆甚至幾十兆以上)則應該採用流式方法循環讀數據體。
有一點須要注意,除了 " int read_body(char* buf, size_t size);" 能夠直接讀原生的響應數據體外,其它的讀方法會將讀到數據體自動進行解壓、字符集轉換操做後將最終結果返回調用者。
此外,爲了方便一些文本類應用,在 http_request 類中還提供了 body_gets 方法,用來以行爲單位讀取 HTTP 響應數據體(當服務器也是以行爲單位發送響應數據時纔可以使用 body_gets 方法)。
acl::http_request 類除了以上接口外,還提供了其它豐富的接口(如:支持 HTTP 斷點續傳的 Range 相關的方法),若是您以爲這些接口依然不能知足要求,不妨經過 "http_request::get_client" 得到 acl::http_client 類對象(該類對象是 acl 有關 http 協議處理中比較基礎的 HTTP 通訊類),而後再在 acl::http_client 類中查找您所但願的功能接口。
3、示例
下面用一個簡單的例子來講明上面一些方法的使用過程:
// http_servlet.cpp : 定義控制檯應用程序的入口點。 // #include <assert.h> #include <getopt.h> #include "acl_cpp/lib_acl.hpp" using namespace acl; ////////////////////////////////////////////////////////////////////////// class http_request_test { public: http_request_test(const char* server_addr, const char* file, const char* stype, const char* charset) { server_addr_= server_addr; file_ = file; stype_ = stype; charset_ = charset; to_charset_ = "gb2312"; } ~http_request_test() {} bool run(void) { string body; if (ifstream::load(file_, &body) == false) { logger_error("load %s error", file_.c_str()); return false; } http_request req(server_addr_); // 添加 HTTP 請求頭字段 string ctype("text/"); ctype << stype_ << "; charset=" << charset_; http_header& hdr = req.request_header(); // 請求頭對象的引用 hdr.set_url("/"); hdr.set_content_type(ctype); // 發送 HTTP 請求數據 if (req.request(body.c_str(), body.length()) == false) { logger_error("send http request to %s error", server_addr_.c_str()); return false; } // 取出 HTTP 響應頭的 Content-Type 字段 const char* p = req.header_value("Content-Type"); if (p == NULL || *p == 0) { logger_error("no Content-Type"); return false; } // 分析 HTTP 響應頭的數據類型 http_ctype content_type; content_type.parse(p); // 響應頭數據類型的子類型 const char* stype = content_type.get_stype(); bool ret; if (stype == NULL) ret = do_plain(req); else if (strcasecmp(stype, "xml") == 0) ret = do_xml(req); else if (strcasecmp(stype, "json") == 0) ret = do_json(req); else ret = do_plain(req); if (ret == true) logger("read ok!\r\n"); return ret; } private: // 處理 text/plain 類型數據 bool do_plain(http_request& req) { string body; if (req.get_body(body, to_charset_) == false) { logger_error("get http body error"); return false; } printf("body:\r\n(%s)\r\n", body.c_str()); return true; } // 處理 text/xml 類型數據 bool do_xml(http_request& req) { xml body; if (req.get_body(body, to_charset_) == false) { logger_error("get http body error"); return false; } xml_node* node = body.first_node(); while (node) { const char* tag = node->tag_name(); const char* name = node->attr_value("name"); const char* pass = node->attr_value("pass"); printf(">>tag: %s, name: %s, pass: %s\r\n", tag ? tag : "null", name ? name : "null", pass ? pass : "null"); node = body.next_node(); } return true; } // 處理 text/json 類型數據 bool do_json(http_request& req) { json body; if (req.get_body(body, to_charset_) == false) { logger_error("get http body error"); return false; } json_node* node = body.first_node(); while (node) { if (node->tag_name()) { printf("tag: %s", node->tag_name()); if (node->get_text()) printf(", value: %s\r\n", node->get_text()); else printf("\r\n"); } node = body.next_node(); } return true; } private: string server_addr_; // web 服務器地址 string file_; // 本地請求的數據文件 string stype_; // 請求數據的子數據類型 string charset_; // 本地請求數據文件的字符集 string to_charset_; // 將服務器響應數據轉爲本地字符集 }; ////////////////////////////////////////////////////////////////////////// static void usage(const char* procname) { printf("usage: %s -h[help]\r\n", procname); printf("options:\r\n"); printf("\t-f request file\r\n"); printf("\t-t request stype[xml/json/plain]\r\n"); printf("\t-c request file's charset[gb2312/utf-8]\r\n"); } int main(int argc, char* argv[]) { int ch; string server_addr("127.0.0.1:8888"), file("./xml.txt"); string stype("xml"), charset("gb2312"); while ((ch = getopt(argc, argv, "hs:f:t:c:")) > 0) { switch (ch) { case 'h': usage(argv[0]); return 0; case 'f': file = optarg; break; case 't': stype = optarg; break; case 'c': charset = optarg; break; default: usage(argv[0]); return 0; } } log::stdout_open(true); // 容許日誌輸出至屏幕上 http_request_test test(server_addr, file, stype, charset); test.run(); // 開始運行 return 0; }
上面的例子來自於 lib_acl_cpp/samples/http_request。
若是查看 http_request::request 源碼實現,會發現 try_open()、reuse_conn、need_retry_ 等方法或變量來表示 HTTP 客戶端鏈接的重試過程,這是由於 http_request 類的設計是支持長鏈接及可重用的,對於 HTTP 客戶端鏈接池來講這些功能很是重要,在下一節介紹使用 acl 的 http 客戶端鏈接池功能類時將會用到 http 請求客戶端鏈接的重連及重試機制。
4、參考
http_request 類的頭文件位置:lib_acl_cpp/include/acl_cpp/http/http_request.hpp
HTTP 協議簡介:http://zsxxsz.iteye.com/blog/568250
acl 庫下載:http://sourceforge.net/projects/acl/
svn:svn checkout svn://svn.code.sf.net/p/acl/code/trunk acl-code
github:https://github.com/zhengshuxin/acl
acl 的編譯與使用:http://zsxxsz.iteye.com/blog/1506554
qq 羣:242722074