• C语言实现http请求器


    C语言实现http请求器

    项目介绍

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

    程序执行流程

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

    前置知识

    http请求报文格式

    在这里插入图片描述

    http响应报文的格式

    在这里插入图片描述

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

    程序实现拆解

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

    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 */
               }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    通过域名获得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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    typedef uint32_t in_addr_t;
    struct in_addr {
    
        in_addr_t s_addr;
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    通过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;
    }
    
    • 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

    通过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;
    }
    
    • 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

    完整代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <string.h>
    #include <netdb.h>
    #include <sys/select.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #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);
    }
    
    • 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

    编译指令

    gcc -o httprequest httprequest.c
    
    • 1

    测试效果

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

    在这里插入图片描述

    问题

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

    在这里插入图片描述

    在这里插入图片描述

  • 相关阅读:
    IDEA插件开发(25)--Color Scheme Management
    Mysql分组查询每组最新的一条数据,查询用户的最新的一条记录
    java计算机毕业设计基于springboot校园新闻网站
    Python 装饰器、嵌套函数、高阶函数
    启动类的注解
    大数据库练习题目集-键值数据库-2022-2023-1-20大数据本
    vue-饼形图-详细
    JavaScript -- 06.函数知识汇总
    【HMS core】【ML Kit】机器学习服务常见问题FAQ(二)
    机试(2017 cs se)
  • 原文地址:https://blog.csdn.net/qq_42120843/article/details/125511954