Jimmy Chen

A Programmer

C++实现HTTP Get和Post请求

  因为以前用Java写Android的时候感觉Java已经封装得很好了,HTTP实现的东西都不是很了解。然后就买了本HTTP入门书籍《图解HTTP》来读,快速读完了,那就用C++实现简单的HTTP Get和Post请求吧。

  在编写代码前,还是需要将HTTP Get和Post请求的区别做下说明的,《图解HTTP》这本书毕竟是入门书籍,这两个的区别都没有说得很详细,所以这里借助互联网,参考下W3School对Get和Post方法的总结。

Get和Post请求的区别

在客户机和服务器之间进行请求-响应时,两种最常被用到的方法是:GET 和 POST。

  • GET – 从指定的资源请求数据。
  • POST – 向指定的资源提交要被处理的数据

Get方法

请注意,查询字符串(名称/值对)是在 GET 请求的 URL 中发送的:

/test/demo_form.asp?name1=value1&name2=value2

有关 GET 请求的其他一些注释:

  • GET 请求可被缓存
  • GET 请求保留在浏览器历史记录中
  • GET 请求可被收藏为书签
  • GET 请求不应在处理敏感数据时使用
  • GET 请求有长度限制
  • GET 请求只应当用于取回数据

Post方法

请注意,查询字符串(名称/值对)是在 POST 请求的 HTTP 消息主体中发送的:

POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2

有关 POST 请求的其他一些注释:

  • POST 请求不会被缓存
  • POST 请求不会保留在浏览器历史记录中
  • POST 不能被收藏为书签
  • POST 请求对数据长度没有要求

比较 GET 与 POST

下面的表格比较了两种 HTTP 方法:GET 和 POST。

GET POST
后退按钮/刷新 无害 数据会被重新提交(浏览器应该告知用户数据会被重新提交)
书签 可收藏为书签 不可收藏为书签
缓存 可被缓存 不能缓存
历史 参数保留在浏览器历史中 参数不会保存在浏览器历史中
对数据长度限制 是的。当发送数据时,GET方法向URL添加数据;URL的长度是受限制的(URL的最大长度是2048个字符) 无限制
对数据类型的限制 只允许ASCII字符 没有限制,也允许二进制数据
安全性 和POST相比,GET的安全性较差,因为所发送的数据是URL的一部分。在发送密码或其他,敏感信息时决不能使用GET POST比GET更安全,因为参数不会被保存在浏览器历史或web服务器日志中
可见性 数据在URL中所有人都是可见的 数据不会显示在URL中

下面做一些额外的补充:

  • 数据类型限制这一块,如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如:%E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII。
  • 对于数据长度的限制一部分找到其他的说法,首先是”GET方式提交的数据最多只能是2048字节”,因为GET是通过URL提交数据,那么GET可提交的数据量就跟URL的长度有直接关系了。而实际上,URL不存在参数上限的问题,HTTP协议规范没有对URL长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。

OK,概念性的东西就介绍到这里了,下面还要多介绍一个Linux函数的使用:gethostbyname

gethostbyname()函数说明

想要使用gethostbyname需要包含的头文件

#include <netdb.h>
#include <sys/socket.h>

函数原型:

struct hostent *gethostbyname(const char *name);

hostent结构体的定义如下:

struct hostent
{
    char    *h_name;               
    char    **h_aliases;
    int     h_addrtype;
    int     h_length;
    char    **h_addr_list;
    #define h_addr h_addr_list[0]
};

下面是结构体各个字段的解析:

  • hostent->h_name:表示的是主机的规范名。例如www.google.com的规范名其实是www.l.google.com。

  • hostent->h_aliases:表示的是主机的别名.www.google.com就是google他自己的别名。有的时候,有的主机可能有好几个别名,这些,其实都是为了易于用户记忆而为自己的网站多取的名字。

  • hostent->h_addrtype:表示的是主机ip地址的类型,到底是ipv4(AF_INET),还是pv6(AF_INET6)

  • hostent->h_length:表示的是主机ip地址的长度

  • hostent->h_addr_lisst:表示的是主机的ip地址,注意,这个是以网络字节序存储的。千万不要直接用printf带%s参数来打这个东西,会有问题的哇。所以到真正需要打印出这个IP的话,需要调用inet_ntop()。

下面通过一个代码讲解一下这个函数的使用:

#include <iostream>
#include <netdb.h>
#include <arpa/inet.h>

using namespace std;

// 该示例需要转换的Host name
const string kTestHostName1 = "www.blog4jimmy.com";
const string kTestHostName2 = "www.baidu.com";

int main()
{
    struct hostent *hostent1, *hostent2;
    char **pptr;
    char str[32];

    hostent1 = gethostbyname(kTestHostName1.c_str());
    if(hostent1 == nullptr) {
        cout << "gethostbyname error for host:" << kTestHostName1 << endl;
    }
    else
    {
        cout << "For " << kTestHostName1 << ": " << endl;
        // 打印主机的规范名
        cout << "h_name: " << hostent1->h_name << endl;

        // 打印主机的别名
        pptr = hostent1->h_aliases;
        for(;*pptr != nullptr; pptr++)
            cout << "h_aliases: " << *pptr << endl;

        // 打印主机的IP地址
        switch(hostent1->h_addrtype) {
        case AF_INET:
        case AF_INET6:
            pptr = hostent1->h_addr_list;
            for(; *pptr != nullptr; pptr++) {
                cout << "IP Addr: " << inet_ntop(hostent1->h_addrtype, *pptr, str, sizeof(str)) << endl;
            }
        }

        // 打印主机的首先IP地址
        cout << "First address: " << inet_ntop(hostent1->h_addrtype, hostent1->h_addr, str, sizeof(str)) << endl;
    }

    // 打印的内容和上面的一样,这里不多做讲解
    hostent2 = gethostbyname(kTestHostName2.c_str());
    if(hostent2 == nullptr) {
        cout << "gethostbyname error for host:" << kTestHostName1 << endl;
    }
    else
    {
        cout << endl;
        cout << "For " << kTestHostName2 << ": " << endl;
        cout << "h_name: " << hostent2->h_name << endl;
        pptr = hostent2->h_aliases;
        for(;*pptr != nullptr; pptr++)
            cout << "h_aliases: " << *pptr << endl;

                switch(hostent1->h_addrtype) {
        case AF_INET:
        case AF_INET6:
            pptr = hostent1->h_addr_list;
            for(; *pptr != nullptr; pptr++) {
                cout << "IP Addr: " << inet_ntop(hostent1->h_addrtype, *pptr, str, sizeof(str)) << endl;
            }
        }

        cout << "First address: " << inet_ntop(hostent1->h_addrtype, hostent1->h_addr, str, sizeof(str)) << endl;
    }

    return 0;
}

最后运行打印的结果如下:

For www.blog4jimmy.com: 
h_name: www.blog4jimmy.com
IP Addr: 166.62.28.146
First address: 166.62.28.146

For www.baidu.com: 
h_name: www.a.shifen.com
h_aliases: www.baidu.com
IP Addr: 14.215.177.39
IP Addr: 14.215.177.38
First address: 14.215.177.39

获取到了IP地址后,其实可以直接通过IP地址来访问baidu,但是因为我的博客使用共享主机,所以直接通过IP地址访问好像有问题,具体原因博主也不是很清楚。但是访问通过IP地址来访问baidu是没问题的,大家可以试一试。

C++实现GET和POST请求

HttpGetPostMethod头文件如下:

#ifndef HTTPGETPOSTMETHOD_H
#define HTTPGETPOSTMETHOD_H
#include <iostream>

class HttpGetPostMethod
{
    public:
        HttpGetPostMethod();
        virtual ~HttpGetPostMethod();
        // Http GET 请求
        int HttpGet(std::string host, std::string path, std::string get_content);
        // Http POST 请求
        int HttpPost(std::string host, std::string path, std::string post_content);
        std::string get_request_return();
        std::string get_main_text();
        int get_return_status_code();

    protected:

    private:
        // 记录请求返回的状态码
        int return_status_code_;
        // 记录请求返回所有数据
        std::string request_return_;
        // 记录请求返回的报文部分
        std::string main_text_;
        // HTTP请求过程中使用到的Socket通信部分
        std::string HttpSocket(std::string host, std::string request_str);
        // 将HTTP请求返回的数据分解
        void AnalyzeReturn(void);
};

#endif // HTTPGETPOSTMETHOD_H

源码文件如下:

#include "../include/HttpGetPostMethod.h"

#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string>
#include <cstring>
#include <netdb.h>
#include <sstream>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

using namespace std;

HttpGetPostMethod::HttpGetPostMethod() : return_status_code_(0), request_return_(""), main_text_("")
{

}

HttpGetPostMethod::~HttpGetPostMethod()
{

}

// 分解HTTP请求返回的数据
void HttpGetPostMethod::AnalyzeReturn(void)
{
    size_t position1 = request_return_.find_first_of(" ", 0);
    size_t position2 = request_return_.find(" ", position1+1);
    return_status_code_ = atoi(request_return_.substr(position1+1, position2).c_str());

    position1 = request_return_.find("\r\n\r\n", position2);
    main_text_ = request_return_.substr(position1+4);
}

// HTTP GET请求
int HttpGetPostMethod::HttpGet(std::string host, std::string path, std::string get_content)
{
    stringstream request_str;
    request_str << "GET " << path << (path=="/" ? "" : "?") << get_content << " HTTP/1.1" << "\r\n";
    request_str << "Host: " << host << "\r\n";
//    request_str << "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
//                << "Ubuntu Chromium/62.0.3202.75 Chrome/62.0.3202.75 Safari/537.36 " << "\r\n";
    request_str << "Content-Type: text/html\r\n";
    request_str << "Content-Length: 0\r\n";
    // 对于现在写的方法,这里必须设置为close,以配合HttpSocket中使用的select
    request_str << "Connection: close\r\n";
    request_str << "\r\n";

#if 0
    cout << endl;
    cout << "dump request string: " << endl;
    cout << request_str.str() << endl;
#endif

    request_return_ = HttpSocket(host, request_str.str());
    if(request_return_ == "") {
        cout << "Http Socket error!" << endl;
        return -1;
    }

    AnalyzeReturn();

    return 0;
}

// HTTP POST请求
int HttpGetPostMethod::HttpPost(std::string host, std::string path, std::string post_content)
{
    stringstream request_str;
    request_str << "POST " << path << " HTTP/1.1" << "\r\n";
    request_str << "Host: " << host << "\r\n";
//    request_str << "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
//                << "Ubuntu Chromium/62.0.3202.75 Chrome/62.0.3202.75 Safari/537.36 " << endl;
    request_str << "Content-Type: application/x-www-form-urlencoded" << "\r\n";
    request_str << "Content-Length: " << post_content.length() << "\r\n";
    // 对于现在写的方法,这里必须设置为close,以配合HttpSocket中使用的select
    request_str << "Connection:close" << "\r\n";
    request_str << "\r\n";
    request_str << post_content;

#if 0
    cout << endl;
    cout << "dump request string: " << endl;
    cout << request_str.str() << endl;
#endif

    request_return_ = HttpSocket(host, request_str.str());
    if(request_return_ == "") {
        cout << "Http Post error!" << endl;
        return -1;
    }

    AnalyzeReturn();

    return 0;
}

std::string HttpGetPostMethod::HttpSocket(std::string host, std::string request_str)
{
    const unsigned int kBufferSize = 1024*1024;
    struct sockaddr_in server_addr;
    hostent *server_hostent = nullptr;
    int client_fd;
    char recv_buf[kBufferSize];
    char ip_str[32];
    bzero(recv_buf, kBufferSize);
    bzero(&server_addr, sizeof(sockaddr_in));
    bzero(ip_str, 32);
    int ret = 0;
    fd_set client_fd_set;
    stringstream result_string;
    int h = 0;

    // 根据host name获取host的ip地址
    server_hostent = gethostbyname(host.c_str());
    if(server_hostent == nullptr) {
        cout << "get host ip address error!" << endl;
        return "";
    }

    client_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(client_fd < 0) {
        cout << "create socket fd error!" << endl;
        return "";
    }

    server_addr.sin_family = AF_INET;
    memcpy((char *)&server_addr.sin_addr.s_addr, (char *)*server_hostent->h_addr_list, server_hostent->h_length);
    server_addr.sin_port = htons(80);

    ret = connect(client_fd, (struct sockaddr *) &server_addr, sizeof(sockaddr_in));
    if(ret < 0) {
        cout << "connect to server error!" << endl;
        return "";
    }

    // 将POST或者GET请求发送出去
    ret = send(client_fd, request_str.c_str(), request_str.length(), 0);
    if(ret != request_str.length()) {
        cout << "Send request unfinish!" << endl;
        return "";
    }

    FD_ZERO(&client_fd_set);
    FD_SET(client_fd, &client_fd_set);

    while(1) {
        // HTTP请求返回的内容可能会分成几个Socket包,所以这里使用select方法,将返回的内容合并起来
        h = select(client_fd + 1, &client_fd_set, NULL, NULL, NULL);
        if(h == -1) {
            cout << "read select failed!" << endl;
            break;
        }
        if(FD_ISSET(client_fd, &client_fd_set)) {
            bzero(recv_buf, kBufferSize);
            ret = read(client_fd, recv_buf, kBufferSize);
            // 因为已经在请求中设置了‘connection:close’,所以这里肯定会返回的。
            if(ret == 0) {
                cout << "server has closed this connect!" << endl;
                break;
            }
            result_string << recv_buf;
        }
    }
    shutdown(client_fd, SHUT_RDWR);
    close(client_fd);

    return result_string.str();
}

string HttpGetPostMethod::get_request_return()
{
    return request_return_;
}

string HttpGetPostMethod::get_main_text()
{
    return main_text_;
}

int HttpGetPostMethod::get_return_status_code()
{
    return return_status_code_;
}

上面的代码暂时就这样子处理了,使用方法也很简单,如下是使用一个易源数据的开放API查询基金相关的信息,大家也可以使用这个来试一试上面的代码:

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string>
#include <cstring>
#include <memory>

#include "include/HttpGetPostMethod.h"

int main()
{
    //shared_ptr http_request(new HttpGetPostMethod());
    shared_ptr http_request = make_shared();

    // 现在使用的是自己的API key,免费的所以频率只能是一秒钟一次,大家可以测试下
    int ret  = http_request->HttpGet("route.showapi.com", "/902-1", "showapi_appid=52875&showapi_sign=59c54e39583740bf9708c645c389c9ec&fundCode=519185&needDetails=1");
    if(ret == -1) {
        cout << "Http Socket error!" << endl;
    }
    if(http_request->get_return_status_code() != 200) {
        cout << "Http get status code was: " << http_request->get_return_status_code() << endl;
    }
    string request_return = http_request->get_request_return();
    string main_text = http_request->get_main_text();

    cout << "Return Status Code: " << http_request->get_return_status_code() << endl;
    cout << "Request Return: " << endl << request_return << endl;
    cout << "Main Text: " << endl << main_text << endl;


    sleep(5);

    ret = http_request->HttpGet("route.showapi.com", "/902-1", "showapi_appid=52875&showapi_sign=59c54e39583740bf9708c645c389c9ec&fundCode=000478&needDetails=1");

    if(ret == -1) {
        cout << "Http Socket error!" << endl;
    }
    if(http_request->get_return_status_code() != 200) {
        cout << "Http get status code was: " << http_request->get_return_status_code() << endl;
    }
    request_return = http_request->get_request_return();
    main_text = http_request->get_main_text();

    cout << "Return Status Code: " << http_request->get_return_status_code() << endl;
    cout << "Request return: " << endl << request_return << endl;
    cout << "Main Text: " << endl << main_text << endl;
}
  1. 木愚说道:

    十分感谢你的文章
    有个小瑕疵,gethostbyname用法介绍,关于kTestHostName2的代码中,有些地方hostent2写成了hostent1

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 博主赞过: