之前写过几篇如何使用 acl 库来实现 HTTP 客户端的例子都是基于 C 语言(使用 acl 较为底层的 HTTP 协议库写 HTTP 下载客户端举例, 使用 acl 库开发一个 HTTP 下载客户端),其实在 acl 的 C++ 库(lib_acl_cpp) 中 HTTP 类功能更为强大,本节将介绍如何使用 acl::http_request 类来写一些简单的 HTTP 客户端示例。
一、 acl::http_request 类的一些常用接口
该 HTTP 请求类有两个构造函数,如下 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
http_request(socket_stream* client, int conn_timeout = 60, bool unzip = true);
http_request(const char* addr, int conn_timeout = 60, int rw_timeout = 60, bool unzip = true);
|
第一个是以已经连接成功的套接字流为参数的构造函数,该构造函数把连接 HTTP 服务器的工作交给用户来完成;第二个是以 HTTP 服务器地址为参数的构造函数,使用该构造函数,则该类对象内部会自动连接 HTTP 服务器。
下面的几个函数接口与 HTTP 发送相关:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
http_header& request_header(void);
bool request(const void* data, size_t len);
bool write_head();
bool write_body(const void* data, size_t len);
|
构建及发送 HTTP 请求的过程如下:
- 1、使用两个构造函数之一创建 acl::http_request 请求对象
- 2、调用 http_request::request_header 获得 HTTP 请求头对象的引用(http_header&),然后对该 HTTP 请求头设置 HTTP 请求的参数
- 3、http_request 类提供了两种 HTTP 请求调用 方式:
- 3.1、当 HTTP 请求方法为 HTTP GET 方法或为 HTTP POST 但数据体可以一次性写入时,可以使用 http_request::request 方法,在调用 http_request::request 时会将 HTTP 请求头及请求体一次性发给 HTTP 服务器;
- 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 请求头中的参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
http_header& set_url(const char* url);
http_header& set_host(const char* value);
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_header& add_cookie(const char* name, const char* value, const char* domain = NULL, const char* path = NULL, time_t expires = 0);
http_header& set_keep_alive(bool on);
http_header& set_content_length(long long int n);
http_header& set_content_type(const char* value);
|
以上仅列出了 http_header 类设置 HTTP 请求参数的一些常用方法,其它的方法请参考 http_header.hpp 头文件中的说明。
二、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 响应头相关的接口函数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
int http_status() const;
#ifdef WIN32 __int64 body_length(void) const; #else long long int body_length(void) const; #endif
bool keep_alive(void) const;
const char* header_value(const char* name) const;
const HttpCookie* get_cookie(const char* name, bool case_insensitive = true) const;
|
然后是与读 HTTP 响应数据体相关的接口函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
|
bool body_finish() const;
bool get_body(xml& out, const char* to_charset = NULL);
bool get_body(json& out, const char* to_charset = NULL);
bool get_body(string& out, const char* to_charset = NULL);
int read_body(char* buf, size_t size);
int read_body(string& out, bool clean = false, int* real_size = NULL);
bool body_gets(string& out, bool nonl = true, size_t* size = NULL);
|
虽然上面提供了多个读 HTTP 响应体数据的方法,但可以分为两大类:1、一次性读所有的数据体;2、以流式方式循环读数据体。 其中,对于“一次性读取所有数据体”的读方法,适合于响应数据体比较小的情形,当响应数据为 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 类中查找您所希望的功能接口。
三、示例
下面用一个简单的例子来说明上面一些方法的使用过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
|
#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_);
string ctype("text/"); ctype << stype_ << "; charset=" << charset_;
http_header& hdr = req.request_header(); hdr.set_url("/"); hdr.set_content_type(ctype); hdr.add_param("name1", "value1"); hdr.add_param("name2", "value2"); if (req.request(body.c_str(), body.length()) == false) { logger_error("send http request to %s error", server_addr_.c_str()); return false; }
const char* p = req.header_value("Content-Type"); if (p == NULL || *p == 0) { logger_error("no Content-Type"); return false; }
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: 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; }
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; }
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_; 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 请求客户端连接的重连及重试机制。
四、参考
http_request 类的头文件位置:lib_acl_cpp/include/acl_cpp/http/http_request.hpp
github:https://github.com/acl-dev/acl
gitee:https://gitee.com/acl-dev/acl