使用 acl_cpp 庫中的 http_request 類實現一個 HTTP 客戶端請求的例子

 

      以前寫過幾篇如何使用 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

相關文章
相關標籤/搜索