C语言实现http请求器

C语言实现http请求器

项目介绍

本项目完成一个http客户端请求器,该请求器往服务器发送请求,并接受服务器发来的响应数据。

程序执行流程

  1. 建立TCP连接
  2. 在TCP连接获得的socket的基础上,发送http协议请求
  3. 服务器通过TCP连接的socket,返回http响应报文(response)

前置知识

http请求报文格式

C语言实现http请求器_第1张图片

http响应报文的格式

C语言实现http请求器_第2张图片

报文中各种空格和换行都是非常严格要求的。

程序实现拆解

假如我们要访问百度的根目录,即主页,则应该:

  1. 将 www.baidu.com 转换为对应的IP地址(DNS)
  2. TCP连接第一步获得的IP地址和端口(http在80端口)
  3. 发送http请求报文
  4. 接受服务器返回的响应报文

通过域名查询IP地址

这个功能的实现,我们使用Linux提供的gethostbyname函数来进行实现,功能和我们前面写的DNS请求器功能差不多。

//函数原型
struct hostent *gethostbyname(const char *name);

struct hostent {
               char  *h_name;            /* official name of host */
               char **h_aliases;         /* alias list */
               int    h_addrtype;        /* host address type */
               int    h_length;          /* length of address */
               char **h_addr_list;       /* list of addresses */
           }

通过域名获得IP地址是网络字节序,所以要转换为点分十进制,供后面socket来进行TCP连接使用,具体实现为

char * host_to_ip(const char *hostname)
{
    struct hostent *host_entry = gethostbyname(hostname);

    if(host_entry)
    {
        //h_addr_list其实是一个指针数组,数组中每个元素char*都是in_addr型指针(都是指针当然可转换)
        //host_entry->h_addr_list类型为 char **, 表明是char*类型数组
        //*host_entry->h_addr_list类型为 char *,即第一个ip地址(数组的第一个元素)
        //网络字节序地址(大端)转换为点分十进制地址(如0x13131313 -> 19.19.19.19)
        return inet_ntoa(*(struct in_addr *)*host_entry->h_addr_list);
    }

    return NULL;
}

这里struct in_addr中in_addr_t一般为32位的unsigned int,用于表示IPV4地址

typedef uint32_t in_addr_t;
struct in_addr {

    in_addr_t s_addr;

};

通过TCP SOCKET连接到服务器上

这是一个客户端连接到服务端的过程,其中的步骤基本上是套路,自从开发出这些函数以来都是这么个使用流程:

int http_create_socket(char *ip)
{
    //http使用TCP协议
    //1.创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //2.设置地址的相关参数(IP PORT PROTOCOL)
    struct sockaddr_in sin = {0};
    sin.sin_family = AF_INET;
    sin.sin_port = htons(80); //http默认端口号为80
    sin.sin_addr.s_addr = inet_addr(ip);//点分十进制地址转为网络字节序地址

    //3.connect
    //connect成功返回0
    if(0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in)))
    {
        return -1;
    }

    //socket设置为非阻塞,保证read没有读出数据时也能够立马返回
    //而不会一直阻塞导致后面的代码不执行
    fcntl(sockfd, F_SETFL, O_NONBLOCK);

    return sockfd;
}

通过socket获得的文件描述符来发送http请求并接受响应报文

这里的主要步骤如下:

  1. 通过域名获得IP地址(host_to_ip)
  2. 创建TCPsocket(socket)
  3. 组织请求的报文
    这里报文的格式必须严格按照标准,不能多一个空格也不能少一个空格。
  4. 发送报文(send)
  5. 重点)使用select循环检测前面获得的socket的fd是否有事件被触发可读

代码实现如下:

char * http_send_request(const char *hostname, const char *resource)
{
    //1.通过域名查询获得ip地址
    char *ip = host_to_ip(hostname);
    
    //2.创建socket(采用TCP连接)
    int sockfd = http_create_socket(ip);

    //3.组织请求报文
    char buffer[BUFFER_SIZE] = {0};// 或者memset清零
    //字符串不在同一行的时候每行结尾要加反斜杠
    //这里格式一定要注意,报文中空格不能多也不能少
    sprintf(buffer,
    "GET %s %s\r\n\
Host: %s\r\n\
%s\r\n\
\r\n",
    resource, HTTP_VERSION,
    hostname,
    CONNECTION_TYPE); //CONNECTION_TYPE我们设置为close


    //4.发送http请求报文
    //最后一个参数为0,表示为阻塞式发送
    //即送不成功会一直阻塞,直到被某个信号终端终止,或者直到发送成功为止。
    send(sockfd, buffer, strlen(buffer), 0);

    //不能简单使用recv()接受响应报文,因为我们创建的socket是非阻塞
    //如果使用recv可能没收到数据也返回了。

    //5.用select实现多路复用IO,循环检测是否有可读事件到来,从而进行recv
    fd_set fdread; // 可读fd的集合
    FD_ZERO(&fdread); //清零
    FD_SET(sockfd, &fdread);//将sockfd添加到待检测的可读fd集合

    struct timeval tv;
    tv.tv_sec = 5;
    tv.tv_usec = 0;


    char *result = malloc(sizeof(int));
    memset(result, 0x00, sizeof(int));
    while(1)
    {
        //第一参数:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1
        //第二参数:可读文件描述符的集合[in/out]
        //第三参数:可写文件描述符的集合[in/out]
        //第四参数:出现错误文件描述符的集合[in/out]
        //第五参数:轮询的间隔时间
        //返回值: 成功返回发生变化的文件描述符的个数
        //0:等待超时,没有可读写或错误的文件
        //失败返回-1, 并设置errno值.
        int selection = select(sockfd + 1,&fdread, NULL, NULL, &tv);
        
        //FD_ISSET判断fd是否在集合中
        if(!selection || !FD_ISSET(sockfd, &fdread))
        {
            break;
        }
        else
        {
            memset(buffer, 0x00, BUFFER_SIZE);
            //返回接受到的字节数
            int len = recv(sockfd, buffer, BUFFER_SIZE, 0);
            if(len == 0)
            {
                //disconnect
                break;
            }
            //如果是扩大内存操作会把 result 指向的内存中的数据复制到新地址
            result = realloc(result, (strlen(result) + len + 1) * sizeof(char));
            strncat(result, buffer, len);
        }
    }

    return result;
}

完整代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define HTTP_VERSION        "HTTP/1.1"
#define CONNECTION_TYPE     "Connection: close\r\n"
//keep-alive

#define BUFFER_SIZE         4096

//通过DNS将域名转为 IP
char * host_to_ip(const char *hostname)
{
    struct hostent *host_entry = gethostbyname(hostname);

    if(host_entry)
    {
        //h_addr_list其实是一个指针数组,数组中每个元素char*都是in_addr型指针(都是指针当然可转换)
        //host_entry->h_addr_list类型为 char **, 表明是char*类型数组
        //*host_entry->h_addr_list类型为 char *,即第一个ip地址(数组的第一个元素)
        //网络字节序地址转换为点分十进制地址
        return inet_ntoa(*(struct in_addr *)*host_entry->h_addr_list);
    }

    return NULL;
}

int http_create_socket(char *ip)
{
    //客户端连接到服务端

    //http使用TCP协议
    //1.创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //2.设置地址的相关参数(IP PORT PROTOCOL)
    struct sockaddr_in sin = {0};
    sin.sin_family = AF_INET;
    sin.sin_port = htons(80); //http默认端口号为80
    sin.sin_addr.s_addr = inet_addr(ip);//点分十进制地址转为网络字节序地址

    //3.connect
    //connect成功返回0
    if(0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in)))
    {
        return -1;
    }

    //socket设置为非阻塞,保证read没有读出数据时也能够立马返回
    //而不会一直阻塞导致后面的代码不执行
    fcntl(sockfd, F_SETFL, O_NONBLOCK);

    return sockfd;
}

char * http_send_request(const char *hostname, const char *resource)
{
    //1.通过域名查询获得ip地址
    char *ip = host_to_ip(hostname);
    
    //2.创建socket(采用TCP连接)
    int sockfd = http_create_socket(ip);

    //3.组织请求报文
    char buffer[BUFFER_SIZE] = {0};// 或者memset清零
    //字符串不在同一行的时候每行结尾要加反斜杠
    //这里格式一定要注意,报文中空格不能多也不能少
    sprintf(buffer,
    "GET %s %s\r\n\
Host: %s\r\n\
%s\r\n\
\r\n",
    resource, HTTP_VERSION,
    hostname,
    CONNECTION_TYPE); //CONNECTION_TYPE我们设置为close


    //4.发送http请求报文
    //最后一个参数为0,表示为阻塞式发送
    //即送不成功会一直阻塞,直到被某个信号终端终止,或者直到发送成功为止。
    send(sockfd, buffer, strlen(buffer), 0);

    //不能简单使用recv()接受响应报文,因为我们创建的socket是非阻塞
    //如果使用recv可能没收到数据也返回了。

    //5.用select实现多路复用IO,循环检测是否有可读事件到来,从而进行recv
    fd_set fdread; // 可读fd的集合
    FD_ZERO(&fdread); //清零
    FD_SET(sockfd, &fdread);//将sockfd添加到待检测的可读fd集合

    struct timeval tv;
    tv.tv_sec = 5;
    tv.tv_usec = 0;


    char *result = malloc(sizeof(int));
    memset(result, 0x00, sizeof(int));
    while(1)
    {
        //第一参数:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1
        //第二参数:可读文件描述符的集合[in/out]
        //第三参数:可写文件描述符的集合[in/out]
        //第四参数:出现错误文件描述符的集合[in/out]
        //第五参数:轮询的时间
        //返回值: 成功返回发生变化的文件描述符的个数
        //0:等待超时,没有可读写或错误的文件
        //失败返回-1, 并设置errno值.
        int selection = select(sockfd + 1,&fdread, NULL, NULL, &tv);
        
        //FD_ISSET判断fd是否在集合中
        if(!selection || !FD_ISSET(sockfd, &fdread))
        {
            break;
        }
        else
        {
            memset(buffer, 0x00, BUFFER_SIZE);
            //返回接受到的字节数
            int len = recv(sockfd, buffer, BUFFER_SIZE, 0);
            if(len == 0)
            {
                //disconnect
                break;
            }
            //如果是扩大内存操作会把 result 指向的内存中的数据复制到新地址
            result = realloc(result, (strlen(result) + len + 1) * sizeof(char));
            strncat(result, buffer, len);
        }
    }

    return result;
}

int main(int argc, char *argv[])
{
    if(argc < 3) //要有两个参数一个域名一个请求的资源名
        return -1;

    char *response = http_send_request(argv[1], argv[2]);
    printf("response: %s\n", response);

    free(response);
}

编译指令

gcc -o httprequest httprequest.c

测试效果

1.向百度的根目录发请求

C语言实现http请求器_第3张图片

问题

向bing或者163等网站发请求,都是返回状态码301

C语言实现http请求器_第4张图片

C语言实现http请求器_第5张图片

你可能感兴趣的:(C/C++,c语言,http,网络)