之前写过几篇如何使用 acl 库来实现 HTTP 客户端的例子都是基于 C 语言(使用 acl 较为底层的 HTTP 协议库写 HTTP 下载客户端举例, 使用 acl 库开发一个 HTTP 下载客户端),其实在 acl 的 C++ 库(lib_acl_cpp) 中 HTTP 类功能更为强大,本节将介绍如何使用 acl::http_request 类来写一些简单的 HTTP 客户端示例。
一、 acl::http_request 类的一些常用接口
该 HTTP 请求类有两个构造函数,如下 :
/**
* 构造函数:通过该构造函数传入的 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 服务器。
下面的几个函数接口与 HTTP 发送相关:
/**
* 获得 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 请求的过程如下:
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 请求头中的参数:
/**
* 设置请求的 URL,url 格式示例如下:
* 1、http://www.test.com/
* 2、/cgi-bin/test.cgi
* 3、http://www.test.com/cgi-bin/test.cgi
* 3、http://www.test.com/cgi-bin/test.cgi?name=value
* 4、/cgi-bin/test.cgi?name=value
* 5、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 中添加参数对,当只有参数名没有参数值时则:
* 1、参数名非空串,但参数值为空指针,则 URL 参数中只有:{name}
* 2、参数名非空串,但参数值为空串,则 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 头文件中的说明。
二、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 响应体数据的方法,但可以分为两大类: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 类中查找您所希望的功能接口。
三、示例
下面用一个简单的例子来说明上面一些方法的使用过程:
// 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);
hdr.add_param("name1", "value1");
hdr.add_param("name2", "value2");
// 发送 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 请求客户端连接的重连及重试机制。
四、参考
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/acl-dev/acl
acl 的编译与使用:http://zsxxsz.iteye.com/blog/1506554
qq 群:242722074
微博:http://weibo.com/zsxxsz/